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. Исходный код может храниться в памяти только в течение короткого периода времени, но любой, у кого есть отладчик, навыки и много терпения, может в конечном итоге восстановить вашу программу.
Тем не менее, есть несколько шагов, которые можно предпринять, чтобы затруднить задачу потенциального взломщика. Самое важное: напишите свой фильтр дешифровки на C и статически свяжите модуль дешифровки с бинарником Perl. Для получения дополнительных советов по затруднению работы потенциального взломщика см. файл decrypt.pm в дистрибутиве фильтров исходного кода.
Создание фильтра исходного кода как отдельного исполняемого файла
Альтернатива написанию фильтра на C заключается в создании отдельного исполняемого файла на языке вашего выбора. Отдельный исполняемый файл читает со стандартного ввода, выполняет необходимые действия обработки и записывает отфильтрованные данные в стандартный вывод. Filter::cpp является примером фильтра исходного кода, реализованного как отдельный исполняемый файл — это препроцессор C, поставляемый вместе с вашей компилятором C.
В дистрибутиве фильтров исходного кода есть два модуля, которые упрощают эту задачу: Filter::exec и Filter::sh. Оба позволяют запускать любые внешние исполняемые файлы. Оба используют сопроцессор для управления потоком данных в внешнее исполняемый файл. (Для получения подробных сведений о сопроцессорах см. Stephens, WR, «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() возвращает значение состояния больше нуля и добавляет строку в $_. Значение состояния ноль указывает на конец файла, значение меньше нуля — на ошибку. Ожидается, что сама функция фильтра вернёт своё состояние аналогичным образом и поместит отфильтрованную строку, которую она хочет записать в поток исходного кода, в $_. Использование $_ объясняет краткость большинства фильтров исходного кода на 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 Использование контекста: фильтр отладки
Пример 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 Вот полный фильтр отладки:
package Debug;
use strict;
use warnings;
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, фильтр отладки не знает Perl. Его довольно легко обмануть:
print <<EOM;
##DEBUG_BEGIN
EOM Несмотря на это, вы можете видеть, что с помощью небольшого количества кода можно добиться многого.
ЗАКЛЮЧЕНИЕ
Теперь вы лучше понимаете, что такое фильтр исходного кода, и, возможно, вы даже найдёте для него применение. Если вы хотите поиграть с фильтрами исходного кода, но вам нужна немного вдохновения, вот несколько дополнительных функций, которые можно добавить к фильтру отладки.
Сначала — лёгкая задача. Вместо того, чтобы код отладки был либо включён, либо отключён, гораздо полезнее иметь возможность контролировать включение конкретных блоков кода отладки. Попробуйте расширить синтаксис блоков отладки, чтобы позволить каждой из них иметь идентификатор. Содержимое переменной среды DEBUG можно использовать для управления включением блоков.
После того как вы сможете идентифицировать отдельные блоки, попробуйте позволить им быть вложенными. Это тоже не сложно.
Вот интересная идея, которая не связана с фильтром отладки. В настоящее время подпрограммы Perl имеют довольно ограниченную поддержку формальных списков параметров. Можно указать количество параметров и их тип, но вам всё равно нужно вручную извлечь их из массива @_ самим. Напишите фильтр исходного кода, который позволит использовать список параметров с именами. Такой фильтр преобразует:
sub MySub ($first, $second, @rest) { ... } в это:
sub MySub($$@) {
my ($first) = shift;
my ($second) = shift;
my (@rest) = @_;
...
} Наконец, если вы хотите сложную задачу, попробуйте написать полный макрос-препроцессор Perl в качестве фильтра исходного кода. Заимствуйте полезные функции из C-препроцессора и любых других макропроцессоров, которые вы знаете. Сложная часть заключается в выборе того, насколько глубоким знанием синтаксиса Perl должен обладать ваш фильтр.
ОГРАНИЧЕНИЯ
Фильтры исходного кода работают только на уровне строк, поэтому их возможности по изменению исходного кода на лету очень ограничены. Он не может обнаруживать комментарии, строки в кавычках, heredocs, он не заменяет собой настоящий парсер. Единственное стабильное использование фильтров исходного кода — это шифрование, сжатие или загрузчик байтов для перевода двоичного кода обратно в исходный код.
Например, обратите внимание на ограничения в Switch, который использует фильтры исходного кода и поэтому не работает внутри string 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>
Авторские права
Первая версия этой статьи изначально появилась в журнале The Perl Journal #11 и является авторским правом 1998 года The Perl Journal. Она опубликована по любезности Jon Orwant и The Perl Journal. Этот документ может распространяться на тех же условиях, что и сам Perl.
© 1993–2021 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.34.0/perlfilter