perlfilter
СОДЕРЖАНИЕ
- ИМЯ
- ОПИСАНИЕ
- ПОНЯТИЯ
- Использование фильтров
- Создание фильтра исходного кода
- Создание фильтра исходного кода на C
- Создание фильтра исходного кода как отдельной исполняемой программы
- Создание фильтра исходного кода на Perl
- Использование контекста: фильтр отладки
- ЗАКЛЮЧЕНИЕ
- ОГРАНИЧЕНИЯ
- ВАЖНЫЕ МЫСЛИ
- ТРЕБОВАНИЯ
- АВТОР
- Авторские права
ИМЯ
perlfilter - Фильтры исходного кода
ОПИСАНИЕ
Эта статья посвящена малоизвестной функции Perl – фильтрам исходного кода. Фильтры исходного кода изменяют текст программы модуля перед тем, как Perl его увидит, так же, как препроцессор C изменяет исходный текст программы C перед тем, как компилятор его увидит. Эта статья расскажет вам больше о том, что такое фильтры исходного кода, как они работают и как создать свой.
Первоначальная цель фильтров исходного кода заключалась в шифровании исходного кода вашей программы, чтобы предотвратить случайное чтение. Но они могут делать и многое другое, как вы скоро узнаете. Но сначала – основы.
ПОНЯТИЯ
Перед тем, как интерпретатор Perl сможет выполнить скрипт Perl, он должен сначала прочитать его из файла в память для обработки и компиляции. Если этот скрипт сам включает другие скрипты с оператором use или require, то каждый из этих скриптов также должен быть прочитан из соответствующих файлов.
Теперь представьте каждую логическую связь между парсером Perl и отдельным файлом как поток исходного кода. Поток исходного кода создается, когда парсер Perl открывает файл, он продолжает существовать, пока исходный код читается в память, и он уничтожается, когда Perl закончит обработку файла. Если парсер встречает оператор require или use в потоке исходного кода, создается новый и отдельный поток только для этого файла.
Диаграмма ниже представляет собой один поток исходного кода, сопоставляя поток данных из файла скрипта Perl слева в парсер Perl справа. Так Perl обычно работает.
file -------> parser Есть две важные точки, которые следует запомнить:
-
Несмотря на то, что в любой момент времени может существовать любое количество потоков исходного кода, только один будет активным.
-
Каждый поток исходного кода связан только с одним файлом.
Фильтр исходного кода – это специальный модуль Perl, который перехватывает и изменяет поток исходного кода перед тем, как он достигнет парсера. Фильтр исходного кода изменяет нашу диаграмму следующим образом:
file ----> filter ----> parser Если это не очень понятно, рассмотрите аналогию конвейера команд. Предположим, у вас есть скрипт оболочки, сохраненный в сжатом файле trial.gz. Простая команда конвейера ниже выполняет скрипт без необходимости создания временного файла для хранения распакованного файла.
gunzip -c trial.gz | sh В этом случае поток данных из конвейера можно представить следующим образом:
trial.gz ----> gunzip ----> sh С помощью фильтров исходного кода вы можете хранить текст вашего скрипта в сжатом виде и использовать фильтр исходного кода для его распаковки для парсера Perl:
compressed gunzip
Perl program ---> source filter ---> parser Использование фильтров
Итак, как использовать фильтр исходного кода в скрипте Perl? Выше я сказал, что фильтр исходного кода – это просто особый вид модуля. Как и все модули Perl, фильтр исходного кода вызывается с помощью оператора use.
Допустим, вы хотите передать свой исходный код Perl через препроцессор C перед выполнением. Так получилось, что в дистрибутиве фильтров исходного кода есть модуль фильтра препроцессора C, называемый Filter::cpp.
Ниже приведен пример программы, cpp_test, которая использует этот фильтр. Номера строк добавлены для удобства ссылки на конкретные строки.
1: use Filter::cpp;
2: #define TRUE 1
3: $a = TRUE;
4: print "a = $a\n"; При выполнении этого скрипта Perl создает поток исходного кода для файла. Прежде чем парсер обработает какие-либо строки из файла, поток исходного кода выглядит так:
cpp_test ---------> parser Строка 1, use Filter::cpp, включает и устанавливает модуль фильтра cpp. Все фильтры исходного кода работают таким образом. Оператор use компилируется и выполняется во время компиляции, прежде чем будет прочитан любой другой фрагмент файла, и он подключает фильтр cpp к потоку исходного кода за кулисами. Теперь поток данных выглядит так:
cpp_test ----> cpp filter ----> parser По мере того, как парсер считывает вторую и последующие строки из потока исходного кода, он передает эти строки через фильтр исходного кода cpp перед обработкой. Фильтр cpp просто передает каждую строку через настоящий препроцессор C. Выходные данные препроцессора C затем вставляются обратно в поток исходного кода фильтром.
.-> cpp --.
| |
| |
| <-'
cpp_test ----> cpp filter ----> parser Затем парсер видит следующий код:
use Filter::cpp;
$a = 1;
print "a = $a\n"; Давайте рассмотрим, что происходит, когда отфильтрованный код включает другой модуль с оператором use:
1: use Filter::cpp;
2: #define TRUE 1
3: use Fred;
4: $a = TRUE;
5: print "a = $a\n"; Фильтр cpp не применяется к тексту модуля Fred, только к тексту файла, который его использовал (cpp_test). Хотя оператор use в строке 3 пройдет через фильтр cpp, модуль, который подключается (Fred), не будет. Потоки исходного кода выглядят так после обработки строки 3 и перед обработкой строки 4:
cpp_test ---> cpp filter ---> parser (INACTIVE)
Fred.pm ----> parser Как видите, для чтения исходного кода из Fred.pm был создан новый поток. Этот поток будет активным до тех пор, пока весь Fred.pm не будет обработан. Поток исходного кода для cpp_test по-прежнему будет существовать, но неактивен. После того, как парсер закончит чтение Fred.pm, поток исходного кода, связанный с ним, будет уничтожен. Затем поток исходного кода для cpp_test снова станет активным, и парсер прочитает строку 4 и последующие строки из cpp_test.
Вы можете использовать более одного фильтра исходного кода для одного файла. Аналогично, вы можете повторно использовать один и тот же фильтр в любом количестве файлов.
Например, если у вас есть закодированный и сжатый исходный файл, можно наложить фильтр uudecode и фильтр распаковки:
use Filter::uudecode; use Filter::uncompress;
M'XL(".H<US4''V9I;F%L')Q;>7/;1I;_>_I3=&E=%:F*I"T?22Q/
M6]9*<IQCO*XFT"0[PL%%'Y+IG?WN^ZYN-$'J.[.JE$,20/?K=_[>
... После обработки первой строки поток данных будет выглядеть так:
file ---> uudecode ---> uncompress ---> parser
filter filter Данные проходят через фильтры в том же порядке, в котором они появляются в исходном файле. Фильтр uudecode появился перед фильтром распаковки, поэтому исходный файл будет декодирован uudecode перед распаковкой.
Создание фильтра исходного кода
Существует три способа создания собственного фильтра исходного кода. Вы можете написать его на C, использовать внешнюю программу как фильтр или написать фильтр на Perl. Я не буду подробно рассматривать первые два, поэтому я их быстро рассмотрю. Написание фильтра на Perl наиболее удобно, поэтому я ему уделю больше места.
Создание фильтра исходного кода на C
Первый из трех доступных методов – написать фильтр полностью на C. Создаваемый вами внешний модуль напрямую взаимодействует с предоставленными Perl хуками для фильтров исходного кода.
Преимущества этого метода – полный контроль над реализацией фильтра. Серьезный недостаток – повышенная сложность написания фильтра: вам необходимо не только понимать хуки фильтра исходного кода, но также иметь достаточно хорошее понимание внутренностей Perl. Один из немногих случаев, когда стоит приложить эти усилия, – это создание шифратора исходного кода. Фильтр decrypt (который расшифровывает исходный код перед его обработкой Perl) из дистрибутива фильтров исходного кода – пример фильтра исходного кода на C (см. Фильтры дешифрования ниже).
- Фильтры дешифрования
-
Все фильтры дешифрования работают на принципе «безопасности через неочевидность». Независимо от того, насколько хорошо вы написали фильтр дешифрования и насколько сильным является ваш алгоритм шифрования, любой, кто достаточно усерден, может восстановить исходный исходный код. Причина довольно проста: для выполнения вашей программы Perl должен обработать исходный код. Это означает, что Perl должен иметь всю информацию, необходимую для расшифровки вашей программы, а это означает, что эта информация также доступна любому, кто может запустить программу.
Тем не менее, можно предпринять ряд шагов, чтобы затруднить задачу потенциальному читателю. Самое главное: напишите свой фильтр дешифрования на C и статически скомпилируйте модуль дешифрования в двоичный файл Perl. Для получения дополнительных советов по затруднению потенциального читателя см. файл decrypt.pm в дистрибутиве фильтров исходного кода.
Создание фильтра исходного кода как отдельной исполняемой программы
Альтернатива написанию фильтра на C – создание отдельной исполняемой программы на языке вашего выбора. Отдельная исполняемая программа читает данные со стандартного ввода, выполняет необходимые действия обработки и записывает отфильтрованные данные в стандартный вывод. Filter::cpp – пример фильтра исходного кода, реализованного как отдельная исполняемая программа – эта исполняемая программа – препроцессор C, поставляемый вместе с вашей компилятором C.
Дистрибутив фильтров исходного кода содержит два модуля, которые упрощают эту задачу: Filter::exec и Filter::sh. Оба позволяют запускать любую внешнюю исполняемую программу. Оба используют копроцесс для управления потоком данных в и из внешней исполняемой программы. (Для получения подробной информации о копроцессах см. Stephens, W.R., «Advanced Programming in the UNIX Environment». Addison-Wesley, ISBN 0-210-56317-7, страницы 441–445.) Разница между ними заключается в том, что Filter::exec запускает внешнюю команду непосредственно, в то время как Filter::sh запускает оболочку для выполнения внешней команды. (Unix использует оболочку Bourne; NT использует оболочку cmd.) Запуск оболочки позволяет использовать метасимволы оболочки и средства перенаправления.
Вот пример скрипта, использующего Filter::sh.
use Filter::sh 'tr XYZ PQR';
$a = 1;
print "XYZ a = $a\n"; Вывод, который вы получите при выполнении скрипта:
PQR a = 1 Создание фильтра исходного кода как отдельной исполняемой программы работает нормально, но при этом возникает небольшая потеря производительности. Например, если вы выполните приведенный выше небольшой пример, будет создан отдельный подпроцесс для выполнения Unix-команды tr. Каждый вызов фильтра требует своего собственного подпроцесса. Если создание подпроцессов дорогостояще для вашей системы, вы можете рассмотреть другие варианты создания фильтров исходного кода.
Создание фильтра исходного кода на Perl
Самый простой и наиболее портативный способ создания собственного фильтра исходного кода – написание его полностью на Perl. Чтобы отличить этот метод от двух предыдущих, я буду называть его Perl-фильтром исходного кода.
Чтобы понять, как написать Perl-фильтр исходного кода, нам нужен пример для изучения. Вот завершенный фильтр исходного кода, выполняющий декодирование rot13. (Rot13 — это очень простая схема шифрования, используемая в публикациях Usenet для сокрытия содержимого оскорбительных сообщений. Она сдвигает каждую букву на тринадцать позиций вперед, так что A становится N, B становится O, а Z становится M.)
package Rot13;
use Filter::Util::Call;
sub import {
my ($type) = @_;
my ($ref) = [];
filter_add(bless $ref);
}
sub filter {
my ($self) = @_;
my ($status);
tr/n-za-mN-ZA-M/a-zA-Z/
if ($status = filter_read()) > 0;
$status;
}
1; Все Perl-фильтры исходного кода реализуются как Perl-классы и имеют ту же базовую структуру, что и пример выше.
Сначала мы включаем модуль Filter::Util::Call, который экспортирует ряд функций в пространство имен вашего фильтра. Приведенный выше фильтр использует две из этих функций, filter_add() и filter_read().
Далее, мы создаем объект фильтра и связываем его с потоком исходного кода, определяя функцию import. Если вы достаточно хорошо знаете Perl, вы знаете, что import вызывается автоматически каждый раз, когда модуль включается с помощью оператора use. Это делает import идеальным местом для создания и установки объекта фильтра.
В примере фильтра объект ($ref) благословляется, как и любой другой Perl-объект. В нашем примере используется анонимный массив, но это не обязательно. Поскольку этому примеру не нужно хранить никакую контекстную информацию, мы могли бы использовать скалярную или хеш-ссылку с тем же успехом. Следующий раздел демонстрирует данные контекста.
Связь между объектом фильтра и потоком исходного кода осуществляется с помощью функции filter_add(). Она принимает объект фильтра в качестве параметра ($ref в этом случае) и устанавливает его в потоке исходного кода.
Наконец, есть код, который фактически выполняет фильтрацию. Для этого типа Perl-фильтра исходного кода вся фильтрация выполняется в методе filter(). (Также возможно написать Perl-фильтр исходного кода с использованием замыкания. См. страницу справки Filter::Util::Call для получения дополнительной информации.) Он вызывается каждый раз, когда Perl-парсеру требуется следующая строка исходного кода для обработки. Метод filter(), в свою очередь, считывает строки из потока исходного кода, используя функцию filter_read().
Если строка была доступна из потока исходного кода, filter_read() возвращает значение статуса, большее нуля, и добавляет строку в $_. Значение статуса 0 указывает конец файла, значение меньше нуля — ошибку. Функция фильтра сама ожидается, чтобы вернуть свой статус аналогичным образом и поместить фильтрованную строку, которую она хочет записать в поток исходного кода, в $_. Использование $_ учитывает краткость большинства Perl-фильтров исходного кода.
Для использования фильтра rot13 нам нужен способ кодирования исходного файла в формате rot13. Скрипт ниже, mkrot13, делает именно это.
die "usage mkrot13 filename\n" unless @ARGV;
my $in = $ARGV[0];
my $out = "$in.tmp";
open(IN, "<$in") or die "Cannot open file $in: $!\n";
open(OUT, ">$out") or die "Cannot open file $out: $!\n";
print OUT "use Rot13;\n";
while (<IN>) {
tr/a-zA-Z/n-za-mN-ZA-M/;
print OUT;
}
close IN;
close OUT;
unlink $in;
rename $out, $in; Если мы зашифруем это с помощью mkrot13:
print " hello fred \n"; результатом будет это:
use Rot13;
cevag "uryyb serq\a"; Запуск этого скрипта даёт следующий вывод:
hello fred ИСПОЛЬЗОВАНИЕ КОНТЕКСТА: ФИЛЬТР DEBUG
Пример rot13 был тривиальным примером. Вот ещё одна демонстрация, которая показывает несколько дополнительных функций.
Предположим, вы хотели включить много отладочного кода в свой скрипт Perl во время разработки, но не хотели, чтобы он был доступен в релизе. Фильтры исходного кода предлагают решение. Для упрощения примера предположим, что отладочный вывод должен контролироваться переменной окружения DEBUG. Отладочный код включён, если переменная существует, иначе он выключен.
Два специальных маркера будут обрамлять отладочный код, например:
## DEBUG_BEGIN
if ($year > 1999) {
warn "Debug: millennium bug in year $year\n";
}
## DEBUG_END Фильтр гарантирует, что Perl обрабатывает код между маркерами <DEBUG_BEGIN> и DEBUG_END только в том случае, если переменная окружения DEBUG существует. Это означает, что когда DEBUG существует, код выше должен передаваться фильтру без изменений. Маркерные строки также могут передаваться как есть, потому что Perl-парсер увидит их как комментарии. Когда DEBUG не задана, нам нужен способ выключения отладочного кода. Простой способ достижения этого заключается в преобразовании строк между двумя маркерами в комментарии:
## DEBUG_BEGIN
#if ($year > 1999) {
# warn "Debug: millennium bug in year $year\n";
#}
## DEBUG_END Вот завершенный фильтр Debug:
package Debug;
use v5.36;
use Filter::Util::Call;
use constant TRUE => 1;
use constant FALSE => 0;
sub import {
my ($type) = @_;
my (%context) = (
Enabled => defined $ENV{DEBUG},
InTraceBlock => FALSE,
Filename => (caller)[1],
LineNo => 0,
LastBegin => 0,
);
filter_add(bless \%context);
}
sub Die {
my ($self) = shift;
my ($message) = shift;
my ($line_no) = shift || $self->{LastBegin};
die "$message at $self->{Filename} line $line_no.\n"
}
sub filter {
my ($self) = @_;
my ($status);
$status = filter_read();
++ $self->{LineNo};
# deal with EOF/error first
if ($status <= 0) {
$self->Die("DEBUG_BEGIN has no DEBUG_END")
if $self->{InTraceBlock};
return $status;
}
if ($self->{InTraceBlock}) {
if (/^\s*##\s*DEBUG_BEGIN/ ) {
$self->Die("Nested DEBUG_BEGIN", $self->{LineNo})
} elsif (/^\s*##\s*DEBUG_END/) {
$self->{InTraceBlock} = FALSE;
}
# comment out the debug lines when the filter is disabled
s/^/#/ if ! $self->{Enabled};
} elsif ( /^\s*##\s*DEBUG_BEGIN/ ) {
$self->{InTraceBlock} = TRUE;
$self->{LastBegin} = $self->{LineNo};
} elsif ( /^\s*##\s*DEBUG_END/ ) {
$self->Die("DEBUG_END has no DEBUG_BEGIN", $self->{LineNo});
}
return $status;
}
1; Главное отличие этого фильтра от предыдущего примера — использование данных контекста в объекте фильтра. Объект фильтра основан на хеш-ссылке и используется для хранения различных данных контекста между вызовами функции фильтра. Все поля хеша, кроме двух, используются для отладки ошибок. Первый из этих двух, Enabled, используется фильтром для определения того, должен ли отладочный код быть передан Perl-парсеру. Второй, InTraceBlock, равен true, когда фильтр встретил строку DEBUG_BEGIN, но ещё не встретил следующую строку DEBUG_END.
Если вы проигнорируете всю проверку ошибок, которую выполняет большая часть кода, суть фильтра такова:
sub filter {
my ($self) = @_;
my ($status);
$status = filter_read();
# deal with EOF/error first
return $status if $status <= 0;
if ($self->{InTraceBlock}) {
if (/^\s*##\s*DEBUG_END/) {
$self->{InTraceBlock} = FALSE
}
# comment out debug lines when the filter is disabled
s/^/#/ if ! $self->{Enabled};
} elsif ( /^\s*##\s*DEBUG_BEGIN/ ) {
$self->{InTraceBlock} = TRUE;
}
return $status;
} Будьте осторожны: так же, как препроцессор C не знает C, фильтр Debug не знает Perl. Его можно довольно легко обмануть:
print <<EOM;
##DEBUG_BEGIN
EOM Несмотря на это, вы можете видеть, что с помощью небольшого объёма кода можно добиться многого.
ЗАКЛЮЧЕНИЕ
Теперь вы лучше понимаете, что такое фильтр исходного кода, и, возможно, вы сможете найти для него применение. Если вы хотите поиграть с фильтрами исходного кода, но нуждаетесь в вдохновении, вот несколько дополнительных функций, которые можно добавить к фильтру Debug.
Сначала — лёгкая задача. Вместо того, чтобы иметь отладочный код, который либо включен, либо выключен, было бы намного полезнее возможность управлять включением отдельных блоков отладочного кода. Попробуйте расширить синтаксис блоков отладки, чтобы каждый из них можно было идентифицировать. Содержимое переменной окружения DEBUG затем можно использовать для управления включением блоков.
После того, как вы научитесь идентифицировать отдельные блоки, попробуйте позволить им быть вложенными. Это тоже не сложно.
Вот интересная идея, не связанная с фильтром Debug. В настоящее время подпрограммы Perl имеют довольно ограниченную поддержку формальных списков параметров. Вы можете указать количество параметров и их тип, но вам всё ещё нужно вручную извлечь их из массива @_.
Напишите фильтр исходного кода, который позволит иметь список параметров с именами. Такой фильтр преобразует это:
sub MySub ($first, $second, @rest) { ... } в это:
sub MySub($$@) {
my ($first) = shift;
my ($second) = shift;
my (@rest) = @_;
...
} Наконец, если вы чувствуете, что хотите справиться с настоящей задачей, попробуйте написать полноценный препроцессор макросов Perl в виде фильтра исходного кода. Заимствуйте полезные функции из препроцессора C и других препроцессоров макросов, которые вам известны. Сложная часть будет заключаться в выборе того, сколько знаний синтаксиса Perl вы хотите, чтобы имел ваш фильтр.
ОГРАНИЧЕНИЯ
Фильтры исходного кода работают только на уровне строк, поэтому их возможности по изменению исходного кода на лету очень ограничены. Они не могут обнаруживать комментарии, строки, заключённые в кавычки, heredocs; это не заменит реальный парсер. Единственное стабильное применение фильтров исходного кода — шифрование, сжатие или загрузчик байтов для перевода двоичного кода обратно в исходный код.
См., например, ограничения в Switch, который использует фильтры исходного кода и поэтому не работает внутри оператора eval строки, наличие регулярных выражений с вложенными символами новой строки, заданных с помощью разделителей raw /.../ и не имеющих модификатора //x, неотличимы от блоков кода, начинающихся с оператора деления /. В качестве обходного пути вы должны использовать m/.../ или m?...? для таких шаблонов. Кроме того, наличие регулярных выражений, заданных с помощью разделителей raw ?...?, может привести к необъяснимым ошибкам. Обходной путь — использование m?...? вместо этого. См. https://metacpan.org/pod/Switch#LIMITATIONS.
В настоящее время содержимое блока __DATA__ не фильтруется.
В настоящее время длина внутренних буферов ограничена 32 битами.
ВАЖНЫЕ МОМЕНТЫ
-
Некоторые фильтры перезаписывают обработчик
DATA -
Некоторые фильтры исходного кода используют обработчик
DATAдля чтения вызывающей программы. При использовании этих фильтров исходного кода вы не можете полагаться на этот обработчик, а также не можете ожидать какого-либо определённого поведения при работе с ним. Фильтры, основанные на Filter::Util::Call (и, следовательно, Filter::Simple), не изменяют файл-дескрипторDATA, но, с другой стороны, полностью игнорируют текст после__DATA__.
ТРЕБОВАНИЯ
Распространение Source Filters доступно на CPAN, в
CPAN/modules/by-module/Filter Начиная с Perl 5.8, Filter::Util::Call (основная часть дистрибутива Source Filters) входит в стандартную дистрибуцию Perl. Также включен более удобный интерфейс под названием Filter::Simple, разработанный Damian Conway.
АВТОР
Paul Marquess <Paul.Marquess@btinternet.com>
Reini Urban <rurban@cpan.org>
Права на использование
Первая версия этой статьи первоначально появилась в Perl Journal #11 и защищена авторским правом 1998 года Perl Journal. Она представлена по любезности Jon Orwant и Perl Journal. Данный документ может распространяться на тех же условиях, что и Perl сам по себе.
© 1993–2023 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.38.0/perlfilter