Dear Nuns and Monks,

while playing with tr/// inside a string eval, I came across a strange feature in overload: It is possible to overload the arguments of a tr/// operator.

This feature can be used to have variable operands in tr/// without eval'ing them.

Here is a module (kind of) that overloads the arguments of tr///. If any of these starts with a $, it is taken as
a symbolic reference to an (unqualified) package variable
an identifier of a lexical scoped variable
that will act as the operand instead. A string eval is still required to compile the expression at runtime but in this case on a constant string that is not prone to code injection. It is even possible to have tr's delimiter inside the the referenced values.

What is your opinion on this? Is it secure? Is it useful?

Update: Incorporated ikegami's advice.

#!/usr/bin/perl -T package Syntax::Tr::Ref; use v5.26; use warnings; use overload; use Carp; use PadWalker qw(peek_my peek_our); use experimental 'signatures'; sub ovl_tr ($,$str,$context) { # pass everything unmodified that is not an operand of tr/// return $str unless $context eq 'tr'; # <original> ## get symbol table entry name #my ($name) = $str =~ /^\$(.*)/; ## pass regular operands #return $str unless defined $name; ## dereference the name #my $caller = caller(1); #no strict 'refs'; #my $ret = ${"${caller}::$name"}; # </original> # <update> # pass regular operands return $str unless $str =~ /^\$/; # search for a variable named $str defined as "my" or "our" in the # caller my $my = peek_my(1); my $our = peek_our(1); my $ret = exists $my->{$str} ? $my->{$str}->$* : exists $our->{$str} ? $our->{$str}->$* : undef; # </update> # illegal argument croak qq(symbolic reference "\$${caller}::$name" not found) unless defined $ret; $ret; } sub import { overload::constant q => \&ovl_tr; } sub unimport { overload::remove_constant q => \&ovl_tr; } 1; package Foo; use v5.26; use warnings; use Carp::Always; # emulate "use Syntax::Tr::Ref;" BEGIN { Syntax::Tr::Ref->import; } # create a tainted empty string my $tainted = substr $ENV{PATH}, 0, 0; # a "bad" searchlist: tainted and contains the tr///-delimiter # <original> ## must be an alias to a package variable #local our $abc = '%/cde' . $tainted; # </original> # <update> my $abc = '%/cde' . $tainted; # </update> # the tr/// subject my $s = '%abcde/'; # eval on fixed string at runtime # almost equivalent to tr{%/cde}{@|fgh} eval 'tr/$abc/@|fgh/; 1' or warn $@ for $s; say $s; __DATA__ @abfgh|

Greetings,
🐻

$gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$

Replies are listed 'Best First'.
Re: Overloading tr/// operands
by ikegami (Patriarch) on Jan 27, 2025 at 14:35 UTC

    Use PadWalker so you don't have to use package vars.

      Use PadWalker

      Thanks! Updated.

      Greetings,
      🐻

      $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$

      See follow-up

      Greetings,
      🐻

      $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
Re: Overloading tr/// operands
by jo37 (Curate) on Feb 02, 2025 at 15:52 UTC

    Using PadWalker in a overload sub turned out to be problematic. The sub that overloads tr's operands cannot access the content of $x in such situation:

    { my $x = 'abc; my $s = 'edcba'; eval '$s =~ tr/$x/123/'; } { my $x = 'ABC'; }

    peek_my(1)->{'$x'} then gives \undef. Though I could work around this, I abandoned this approach and came to something similar: A generator for a sub, that can be used like tr/$x/$y/$opt without eval'ing $x and $y.

    After I put this together I realized that something similar already exists: Regexp::Tr. However, the tr-arguments are eval'ed there.

    Here is demo with all parts in a single file:

    #!/usr/bin/perl use strict; use warnings; ### Internal only package String::Compile::Tr::Overload; use overload; sub _ovl_tr { our ($search, $replace); return $_[1] unless $_[2] eq 'tr'; return "$search" if $_[1] eq ':search:'; return "$replace" if $_[1] eq ':replace:'; $_[1]; } sub import { overload::constant q => \&_ovl_tr; } sub unimport { overload::remove_constant q => \&_ovl_tr; } ### The module itself package String::Compile::Tr; *search = *String::Compile::Tr::Overload::search; *replace = *String::Compile::Tr::Overload::replace; BEGIN { String::Compile::Tr::Overload->import; } sub trgen { local our ($search, $replace); my $options; ($search, $replace, $options) = @_; $options = '' unless defined $options; my ($opt) = $options =~ /^([cdsr]*)$/; $opt = '' unless defined $opt; my $template = <<'EOS'; sub { local *_ = \$_[0] if @_; tr/:search:/:replace:/%s; } EOS my $code = sprintf $template, $opt; eval $code; } BEGIN { String::Compile::Tr::Overload->unimport; } ### main package main; use feature 'say'; *trgen = *String::Compile::Tr::trgen; my $x = 'abc'; my $y = '123'; # compile a sub my $tr = trgen($x, $y); my $str = 'fedcba'; # like $str =~ tr/abc/123/: $tr->($str); say $str; # fed321 my @list = qw(axy bxy cxy); # like tr/abc/123/ $tr->() for @list; say "(@list)"; # (1xy 2xy 3xy);

    Greetings,
    🐻

    $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$

      Uploaded String::Compile::Tr.

      Greetings,
      🐻

      $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$