Here's a very straightforward solution. Instead of %, it uses _ as the escape character, since I think _ should be safe just about anywhere. (If not, maybe -?)
Anyway, my thinking is, escape almost everything (including the escape character, of course). :)
sub encode_title {
my($title) = @_;
$title =~ s/([^A-Za-z0-9])/sprintf '_%02X', ord $1/ge;
return $title;
}
sub decode_title {
my($title) = @_;
if ($title =~ /[^A-Za-z0-9_]/ or
$title =~ /_(?![A-F0-9]{2})/) {
return undef;
}
$title =~ s/_(..)/chr hex $1/ge;
return $title;
}