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"; Давайте рассмотрим, что происходит, когда отфильтрованный код включает другой модуль с использованием:
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. Оба позволяют запускать любую внешнюю исполняемую программу. Оба используют сопроцесс для управления потоком данных во внешний исполняемый файл. (Подробные сведения о сопроцессах см. в работе Стивенса, У.Р., «Программирование для UNIX». 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 указывает конец файла, меньше 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, наличие регулярных выражений с вложенными переносами строк, которые указаны с помощью исходных /.../ разделителей и не имеют модификатора //x, неотличимы от блоков кода, начинающихся с оператора деления /. В качестве обходного пути вам необходимо использовать m/.../ или m?...? для таких шаблонов. Кроме того, наличие регулярных выражений, заданных с помощью исходных ?...? разделителей, может привести к таинственным ошибкам. Обходной путь — использовать 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.36.0/perlfilter