Spec-Zone.ru › Perl 5.32

perlfilter

СОДЕРЖАНИЕ

  • НАЗВАНИЕ
  • ОПИСАНИЕ
  • КОНЦЕПЦИИ
  • ИСПОЛЬЗОВАНИЕ ФИЛЬТРОВ
  • СОЗДАНИЕ ФИЛЬТРА ИСТОЧНИКА
  • СОЗДАНИЕ ФИЛЬТРА ИСТОЧНИКА НА C
  • СОЗДАНИЕ ФИЛЬТРА ИСТОЧНИКА КАК ОТДЕЛЬНОГО ИСПОЛНЯЕМОГО ФАЙЛА
  • СОЗДАНИЕ ФИЛЬТРА ИСТОЧНИКА НА PERL
  • ИСПОЛЬЗОВАНИЕ КОНТЕКСТА: ФИЛЬТР DEBUG
  • ЗАКЛЮЧЕНИЕ
  • ОГРАНИЧЕНИЯ
  • ВАЖНЫЕ МОМЕНТЫ
  • ТРЕБОВАНИЯ
  • АВТОР
  • Авторские права

НАЗВАНИЕ

perlfilter - Фильтры источника

ОПИСАНИЕ

Эта статья посвящена малоизвестной функции Perl — фильтрам источника. Фильтры источника изменяют текст программы модуля перед тем, как Perl увидит его, подобно тому, как препроцессор C изменяет исходный текст программы C перед тем, как компилятор увидит его. В этой статье вы узнаете больше о том, что представляют собой фильтры источника, как они работают и как создать свой собственный.

Изначально фильтры источника предназначались для шифрования исходного кода программы, чтобы предотвратить несанкционированный доступ. Но они способны на гораздо большее, как вы вскоре узнаете. Но сначала — основы.

КОНЦЕПЦИИ

Перед тем как интерпретатор Perl сможет выполнить скрипт Perl, он должен сначала прочитать его из файла в память для анализа и компиляции. Если этот скрипт сам включает в себя другие скрипты с инструкцией use или require, то каждый из этих скриптов также должен быть прочитан из соответствующих файлов.

Теперь представьте каждую логическую связь между анализатором Perl и отдельным файлом как поток источника. Поток источника создается, когда анализатор Perl открывает файл, он продолжает существовать по мере чтения исходного кода в память, и он уничтожается, когда Perl завершает синтаксический анализ файла. Если анализатор встречает инструкцию require или use в потоке источника, создается новый и отдельный поток только для этого файла.

Диаграмма ниже представляет собой один поток источника, с потоком исходного кода из файла скрипта Perl слева в анализатор Perl справа. Вот как обычно работает Perl.

file -------> parser

Важно запомнить две вещи:

  1. Несмотря на то, что в любой момент времени может существовать любое количество потоков источника, только один из них будет активным.

  2. Каждый поток источника связан только с одним файлом.

Фильтр источника — это особый модуль 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, 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

ИСПОЛЬЗОВАНИЕ КОНТЕКСТА: ФИЛЬТР ОТЛАДКИ

Пример 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, истиннен, когда фильтр встретил строку 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://search.cpan.org/perldoc?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@btinternet.com>

Рени Урбан <rurban@cpan.org>

Авторские права

Первая версия этой статьи первоначально появилась в The Perl Journal #11 и является авторским правом 1998 The Perl Journal. Она представлена по любезности Джона Орванта и The Perl Journal. Этот документ может быть распространяется на тех же условиях, что и сам Perl.

© 1993–2020 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.32.0/perlfilter

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API