File Coverage

lib/App/Muter.pm
Criterion Covered Total %
statement 504 547 92.1
branch 107 136 78.6
condition 33 46 71.7
subroutine 109 115 94.7
pod n/a
total 753 844 89.2


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             # ABSTRACT: tool to convert between various formats and encodings
3             #
4             # muter - a data transformation tool
5             #
6             # Copyright © 2016–2017 brian m. carlson
7             #
8             # Permission is hereby granted, free of charge, to any person obtaining a copy
9             # of this software and associated documentation files (the "Software"), to deal
10             # in the Software without restriction, including without limitation the rights
11             # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12             # copies of the Software, and to permit persons to whom the Software is
13             # furnished to do so, subject to the following conditions:
14             #
15             # The above copyright notice and this permission notice shall be included in
16             # all copies or substantial portions of the Software.
17             #
18             # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19             # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20             # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21             # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22             # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23             # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24             # THE SOFTWARE.
25             package App::Muter;
26             $App::Muter::VERSION = '0.003000';
27             require 5.010001;
28              
29 4     4   266872 use strict;
  4         37  
  4         183  
30 4     4   23 use warnings;
  4         8  
  4         124  
31 4     4   21 use feature ':5.10';
  4         6  
  4         520  
32              
33             my $experimental;
34             BEGIN {
35 4 50   4   97 $experimental = 1 if exists $warnings::Offsets{'experimental::smartmatch'};
36             }
37 4     4   1345 no if $experimental, warnings => 'experimental::smartmatch';
  4         37  
  4         18  
38              
39              
40             ## no critic(ProhibitMultiplePackages)
41             package App::Muter::Main;
42             $App::Muter::Main::VERSION = '0.003000';
43 4     4   1476 use App::Muter::Backend ();
  4         10  
  4         71  
44 4     4   1031 use App::Muter::Chain ();
  4         10  
  4         85  
45 4     4   26 use FindBin ();
  4         10  
  4         63  
46 4     4   1884 use Getopt::Long ();
  4         36220  
  4         143  
47 4     4   363 use IO::Handle ();
  4         4265  
  4         59  
48 4     4   864 use IO::File ();
  4         4348  
  4         76  
49              
50 4     4   1114 use File::stat;
  4         19698  
  4         15  
51              
52             sub script {
53 0     0   0 my (@args) = @_;
54              
55 0         0 my $chain = '';
56 0         0 my $help;
57             my $verbose;
58 0         0 my $reverse;
59 0 0       0 Getopt::Long::GetOptionsFromArray(
60             \@args,
61             'chain|c=s' => \$chain,
62             'verbose|v' => \$verbose,
63             'reverse|r!' => \$reverse,
64             'help' => \$help
65             ) or
66             return usage(1);
67              
68 0         0 load_backends();
69              
70 0 0       0 return usage(0, $verbose) if $help;
71 0 0       0 return usage(1) unless $chain;
72              
73 0         0 run_chain($chain, $reverse, load_handles(\@args), \*STDOUT);
74              
75 0         0 return 0;
76             }
77              
78             sub _uniq { ## no critic(RequireArgUnpacking)
79 0     0   0 my %seen;
80 0         0 return grep { !$seen{$_}++ } @_;
  0         0  
81             }
82              
83             sub load_backends {
84 0     0   0 App::Muter::Registry->instance->load_backends();
85 0         0 return;
86             }
87              
88             sub load_handles {
89 0     0   0 my ($files) = @_;
90 0         0 my @handles = map { IO::File->new($_, 'r') } @$files;
  0         0  
91 0 0       0 @handles = (\*STDIN) unless @handles;
92 0         0 return \@handles;
93             }
94              
95             sub run_chain {
96 15528     15528   13824777 my ($chain, $reverse, $handles, $stdout, $blocksize) = @_;
97              
98 15528         69863 $chain = App::Muter::Chain->new($chain, $reverse);
99 15528   50     33627 $blocksize ||= 512;
100              
101 15528         30609 foreach my $io (@$handles) {
102 15528         46926 $io->binmode(1);
103 15528         52459 while ($io->read(my $buf, $blocksize)) {
104 51079         1005097 $stdout->print($chain->process($buf));
105             }
106             }
107 15528         358093 $stdout->print($chain->final(''));
108 15528         540662 return;
109             }
110              
111             sub usage {
112 0     0   0 my ($ret, $verbose) = @_;
113 0 0       0 my $fh = $ret ? \*STDERR : \*STDOUT;
114 0         0 $fh->print(<<'EOM');
115             muter [-r | --reverse] -c CHAIN | --chain CHAIN [FILES...]
116             muter [--verbose] --help
117              
118             Modify the bytes in the concatentation of FILES (or standard input) by using the
119             specification in CHAIN.
120              
121             CHAIN is a colon-separated list of encoding transform. A transform can be
122             prefixed with - to reverse it (if possible). A transform can be followed by one
123             or more comma-separated parenthesized arguments as well. Instead of
124             parentheses, a single comma may be used.
125              
126             For example, '-hex:hash(sha256):base64' (or '-hex:hash,sha256:base64') decodes a
127             hex-encoded string, hashes it with SHA-256, and converts the result to base64.
128              
129             If --reverse is specified, reverse the order of transforms in order and in sense.
130              
131             The following transforms are available:
132             EOM
133 0         0 my $reg = App::Muter::Registry->instance;
134 0         0 foreach my $name ($reg->backends) {
135 0         0 $fh->print(" $name\n");
136 0         0 my $meta = $reg->info($name);
137 0 0 0     0 if ($meta->{args} && ref($meta->{args}) eq 'HASH') {
138 0         0 my @keys = sort keys %{$meta->{args}};
  0         0  
139 0 0       0 if ($verbose) {
140             $fh->printf(" %-10s: %s\n", $_, $meta->{args}->{$_})
141 0         0 for @keys;
142             }
143             else {
144 0         0 $fh->print(" ", join(', ', sort keys %{$meta->{args}}),
  0         0  
145             "\n");
146             }
147             }
148             }
149 0         0 return $ret;
150             }
151              
152             package App::Muter::Interface;
153             $App::Muter::Interface::VERSION = '0.003000';
154             sub process {
155 1229     1229   683990 my ($chain, $data) = @_;
156              
157 1229         3897 $chain = App::Muter::Chain->new($chain);
158 1229         3056 my $result = $chain->process($data);
159 1229         4636 $result .= $chain->final('');
160              
161 1229         11458 return $result;
162             }
163              
164             package App::Muter::Registry;
165             $App::Muter::Registry::VERSION = '0.003000';
166 4     4   2642 use File::Spec;
  4         9  
  4         3335  
167              
168             my $instance;
169              
170             sub instance {
171 16818     16818   39440 my $class = shift;
172 16818   33     46288 $class = ref($class) || $class;
173 16818         35075 my $self = {names => {}};
174 16818   100     57562 return $instance ||= bless $self, $class;
175             }
176              
177             sub register {
178 58     58   112 my ($self, $class) = @_;
179 58         252 my $info = $class->metadata;
180 58         223 $self->{names}{$info->{name}} = {%$info, class => $class};
181 58         242 return 1;
182             }
183              
184             sub info {
185 24274     24274   45608 my ($self, $name) = @_;
186 24274         40046 my $info = $self->{names}{$name};
187 24274 50       43942 die "No such transform '$name'" unless $info;
188 24274         45437 return $info;
189             }
190              
191             sub backends {
192 0     0   0 my ($self) = @_;
193 0         0 my @backends = sort keys %{$self->{names}};
  0         0  
194 0         0 return @backends;
195             }
196              
197             sub load_backends {
198 3     3   9 my ($self) = @_;
199 36 100       100 my @modules = map { /\A([A-Za-z0-9]+)\.pm\z/ ? ($1) : () } map {
200 30         53 my $dh;
201 30 100       601 opendir($dh, $_) ? readdir($dh) : ()
202 3         9 } map { File::Spec->catfile($_, qw/App Muter Backend/) } @INC;
  30         170  
203             eval "require App::Muter::Backend::$_;" ##no critic(ProhibitStringyEval)
204 3         192 for @modules;
205 3         12 return;
206             }
207              
208             package App::Muter::Backend::Chunked;
209             $App::Muter::Backend::Chunked::VERSION = '0.003000';
210             our @ISA = qw/App::Muter::Backend/;
211              
212             sub new {
213 11778     11778   28136 my ($class, $args, %opts) = @_;
214 11778         33686 my $self = $class->SUPER::new($args, %opts);
215 11778         20195 $self->{chunk} = '';
216 11778   33     22851 $self->{enchunksize} = $opts{enchunksize} || $opts{chunksize};
217 11778   33     28517 $self->{dechunksize} = $opts{dechunksize} || $opts{chunksize};
218 11778         22922 return $self;
219             }
220              
221             sub encode {
222 22903     22903   30547 my ($self, $data) = @_;
223 22903         40160 return $self->_with_chunk($data, $self->{enchunksize}, 'encode_chunk');
224             }
225              
226             sub decode {
227 24277     24277   33450 my ($self, $data) = @_;
228 24277         38684 return $self->_with_chunk($data, $self->{dechunksize}, 'decode_chunk');
229             }
230              
231             sub encode_final {
232 6161     6161   8535 my ($self, $data) = @_;
233 6161         13414 return $self->encode_chunk($self->{chunk} . $data);
234             }
235              
236             sub decode_final {
237 5250     5250   7554 my ($self, $data) = @_;
238 5250         10731 return $self->decode_chunk($self->{chunk} . $data);
239             }
240              
241             sub _with_chunk {
242 47180     47180   65807 my ($self, $data, $chunksize, $code) = @_;
243 47180         67396 my $chunk = $self->{chunk} . $data;
244 47180         51919 my $len = length($chunk);
245 47180         56644 my $rem = $len % $chunksize;
246 47180 100       64337 if ($rem) {
247 23384         38029 $self->{chunk} = substr($chunk, -$rem);
248 23384         32128 $chunk = substr($chunk, 0, -$rem);
249             }
250             else {
251 23796         28736 $self->{chunk} = '';
252             }
253 47180         80649 return $self->$code($chunk);
254             }
255              
256             package App::Muter::Backend::ChunkedDecode;
257             $App::Muter::Backend::ChunkedDecode::VERSION = '0.003000';
258             our @ISA = qw/App::Muter::Backend/;
259              
260             sub new {
261 8844     8844   25666 my ($class, $args, %opts) = @_;
262 8844         31785 my $self = $class->SUPER::new($args, %opts);
263 8844         18167 $self->{chunk} = '';
264 8844         14810 $self->{regexp} = $opts{regexp};
265 8844         17267 return $self;
266             }
267              
268             sub encode {
269 19080     19080   29420 my ($self, $data) = @_;
270 19080         33626 return $self->encode_chunk($data);
271             }
272              
273             sub decode {
274 19181     19181   27881 my ($self, $data) = @_;
275 19181         33093 $data = $self->{chunk} . $data;
276 19181 100       119232 if ($data =~ $self->{regexp}) {
277 5182   100     21124 $data = $1 // '';
278 5182         10805 $self->{chunk} = $2;
279             }
280             else {
281 13999         22196 $self->{chunk} = '';
282             }
283 19181         37176 return $self->decode_chunk($data);
284             }
285              
286             sub encode_final {
287 4025     4025   8232 my ($self, $data) = @_;
288 4025         11936 return $self->encode_chunk($self->{chunk} . $data);
289             }
290              
291             sub decode_final {
292 4311     4311   8056 my ($self, $data) = @_;
293 4311         11141 return $self->decode_chunk($self->{chunk} . $data);
294             }
295              
296             package App::Muter::Backend::Base64;
297             $App::Muter::Backend::Base64::VERSION = '0.003000';
298 4     4   996 use MIME::Base64 ();
  4         1508  
  4         1645  
299              
300             our @ISA = qw/App::Muter::Backend::Chunked/;
301              
302             sub new {
303 2763     2763   6414 my ($class, $args, %opts) = @_;
304 2763 100       5368 my $nl = (grep { $_ eq 'mime' } @$args) ? "\n" : '';
  1026         3072  
305 2763 100       8770 my $self = $class->SUPER::new(
306             $args, %opts,
307             enchunksize => $nl ? 57 : 3,
308             dechunksize => 4
309             );
310 2763         4481 $self->{nl} = $nl;
311 2763 100       4829 if (grep { $_ eq 'yui' } @$args) {
  1026         2562  
312 84     107   248 $self->{exfrm} = sub { (my $x = shift) =~ tr{+/=}{._-}; return $x };
  107         155  
  107         399  
313 84     85   180 $self->{dxfrm} = sub { (my $x = shift) =~ tr{._-}{+/=}; return $x };
  85         122  
  85         141  
314             }
315             else {
316 2679     6431   8112 $self->{exfrm} = sub { return shift };
  6431         21356  
317 2679     10643   6347 $self->{dxfrm} = sub { return shift };
  10643         16752  
318             }
319 2763         9478 return $self;
320             }
321              
322             sub encode_chunk {
323 6538     6538   9653 my ($self, $data) = @_;
324 6538         16841 return $self->{exfrm}->(MIME::Base64::encode($data, $self->{nl}));
325             }
326              
327             sub _filter {
328 9644     9644   12963 my ($self, $data) = @_;
329 9644         12837 $data =~ tr{A-Za-z0-9+/=}{}cd;
330 9644         18606 return $data;
331             }
332              
333             sub decode {
334 10728     10728   13883 my ($self, $data) = @_;
335 10728         15950 $data = $self->{dxfrm}->($data);
336 10728         18087 return $self->SUPER::decode($self->_filter($data));
337             }
338              
339             sub decode_chunk {
340 10508     10508   14968 my (undef, $data) = @_;
341 10508         34795 return MIME::Base64::decode($data);
342             }
343              
344             App::Muter::Registry->instance->register(__PACKAGE__);
345              
346             package App::Muter::Backend::URL64;
347             $App::Muter::Backend::URL64::VERSION = '0.003000';
348 4     4   27 use MIME::Base64 3.11 ();
  4         71  
  4         3740  
349             our @ISA = qw/App::Muter::Backend::Base64/;
350              
351             sub encode_chunk {
352 1250     1250   1685 my (undef, $data) = @_;
353 1250         2091 return MIME::Base64::encode_base64url($data);
354             }
355              
356             sub _filter {
357 1084     1084   1494 my (undef, $data) = @_;
358 1084         2081 return $data;
359             }
360              
361             sub decode_chunk {
362 1372     1372   1798 my (undef, $data) = @_;
363 1372         2087 return MIME::Base64::decode_base64url($data);
364             }
365              
366             App::Muter::Registry->instance->register(__PACKAGE__);
367              
368             package App::Muter::Backend::Hex;
369             $App::Muter::Backend::Hex::VERSION = '0.003000';
370             our @ISA = qw/App::Muter::Backend::Chunked/;
371              
372             sub new {
373 1095     1095   2086 my ($class, $args, %opts) = @_;
374 1095         2477 my $self = $class->SUPER::new(
375             $args, %opts,
376             enchunksize => 1,
377             dechunksize => 2
378             );
379 1095 100 100     2917 $self->{upper} = 1 if defined $args->[0] && $args->[0] eq 'upper';
380 1095         2559 return $self;
381             }
382              
383             sub metadata {
384 8     8   13 my $self = shift;
385 8         26 my $meta = $self->SUPER::metadata;
386             return {
387 8         37 %$meta,
388             args => {
389             upper => 'Use uppercase letters',
390             lower => 'Use lowercase letters',
391             }
392             };
393             }
394              
395             sub encode_chunk {
396 6476     6476   8597 my ($self, $data) = @_;
397 6476         12363 my $result = unpack("H*", $data);
398 6476 100       13144 return uc $result if $self->{upper};
399 5325         17448 return $result;
400             }
401              
402             sub decode_chunk {
403 2380     2380   3264 my (undef, $data) = @_;
404 2380         9219 return pack("H*", $data);
405             }
406              
407             App::Muter::Registry->instance->register(__PACKAGE__);
408              
409             package App::Muter::Backend::Base16;
410             $App::Muter::Backend::Base16::VERSION = '0.003000';
411             our @ISA = qw/App::Muter::Backend::Hex/;
412              
413             sub new {
414 475     475   918 my ($class, $args, %opts) = @_;
415 475         1189 my $self = $class->SUPER::new(['upper'], %opts);
416 475         1328 return $self;
417             }
418              
419             sub metadata {
420 4     4   8 my $self = shift;
421 4         16 my $meta = $self->SUPER::metadata;
422 4         13 delete $meta->{args};
423 4         10 return $meta;
424             }
425              
426             App::Muter::Registry->instance->register(__PACKAGE__);
427              
428             package App::Muter::Backend::Base32;
429             $App::Muter::Backend::Base32::VERSION = '0.003000';
430             our @ISA = qw/App::Muter::Backend::Chunked/;
431              
432             sub new {
433 6672     6672   12785 my ($class, @args) = @_;
434 6672         14124 my $self = $class->SUPER::new(@args, enchunksize => 5, dechunksize => 8);
435             $self->{ftr} =
436 6672     3264   19916 sub { my $val = shift; $val =~ tr/\x00-\x1f/A-Z2-7/; return $val };
  3264         5191  
  3264         5320  
  3264         13755  
437             $self->{rtr} =
438 6672     3258   15578 sub { my $val = shift; $val =~ tr/A-Z2-7/\x00-\x1f/; return $val };
  3258         4452  
  3258         5471  
  3258         10353  
439 6672         10312 $self->{func} = 'base32';
440             $self->{manual} =
441 6672         17838 grep { $_ eq 'manual' } @args ||
442 6672   33     14604 !eval { require MIME::Base32; MIME::Base32->VERSION(1.0) };
443 6672         13887 return $self->_initialize;
444             }
445              
446             sub _initialize {
447 8174     8174   12370 my ($self) = @_;
448 8174 50       14050 unless ($self->{manual}) {
449 8174         32172 $self->{eref} = MIME::Base32->can("encode_$self->{func}");
450 8174         22812 $self->{dref} = MIME::Base32->can("decode_$self->{func}");
451             }
452 8174         26133 return $self;
453             }
454              
455             sub encode_chunk {
456 10512     10512   15174 my ($self, $data) = @_;
457 10512 100       27770 return '' unless length($data);
458 4687 50       8820 return $self->{eref}->($data) if $self->{eref};
459 4687         5790 my $len = length($data);
460 4687         6085 my $rem = $len % 5;
461 4687         8026 my $lenmap = [0, 2, 4, 5, 7, 8];
462 4687         6798 my $lm = $lenmap->[$rem];
463 4687 100       16788 my @data = (unpack('C*', $data), ($rem ? ((0) x (5 - $rem)) : ()));
464 4687         7236 my $result = '';
465 4687         10617 my $truncate = int($len / 5) * 8 + $lm;
466 4687         12115 while (my @chunk = splice(@data, 0, 5)) {
467 5035         14164 my @converted = map { $_ & 0x1f } (
  40280         53149  
468             $chunk[0] >> 3,
469             ($chunk[0] << 2) | ($chunk[1] >> 6),
470             ($chunk[1] >> 1),
471             ($chunk[1] << 4) | ($chunk[2] >> 4),
472             ($chunk[2] << 1) | ($chunk[3] >> 7),
473             ($chunk[3] >> 2),
474             ($chunk[3] << 3) | ($chunk[4] >> 5),
475             $chunk[4]
476             );
477 5035         20582 $result .= pack('C*', @converted);
478             }
479 4687         8226 $result = substr($result, 0, $truncate);
480 4687 100       9525 $result .= $lm ? ('=' x (8 - $lm)) : '';
481 4687         9207 return $self->{ftr}->($result);
482             }
483              
484             sub decode_chunk {
485 14047     14047   19567 my ($self, $data) = @_;
486 14047 100       44204 return '' unless length($data);
487 4678 50       8597 return $self->{dref}->($data) if $self->{dref};
488 4678         8460 my $lenmap = [5, 4, undef, 3, 2, undef, 1];
489 4678 100       20473 my $trailing = $data =~ /(=+)$/ ? length $1 : 0;
490 4678         6886 my $truncate = $lenmap->[$trailing];
491 4678         5853 my $result = '';
492 4678         8622 my @data = unpack('C*', $self->{rtr}->($data));
493 4     4   1521 use bytes;
  4         48  
  4         16  
494              
495 4678         13524 while (my @chunk = splice(@data, 0, 8)) {
496 5035         14516 my @converted = (
497             ($chunk[0] << 3) | ($chunk[1] >> 2),
498             ($chunk[1] << 6) | ($chunk[2] << 1) | ($chunk[3] >> 4),
499             ($chunk[3] << 4) | ($chunk[4] >> 1),
500             ($chunk[4] << 7) | ($chunk[5] << 2) | ($chunk[6] >> 3),
501             ($chunk[6] << 5) | $chunk[7],
502             );
503 5035         7492 my $chunk = pack('C*', map { $_ & 0xff } @converted);
  25175         36768  
504 5035 100       18943 $result .= substr($chunk, 0, (@data ? 5 : $truncate));
505             }
506 4678         21247 return $result;
507             }
508              
509             sub metadata {
510 8     8   14 my $self = shift;
511 8         25 my $meta = $self->SUPER::metadata;
512             return {
513 8         30 %$meta,
514             args => {
515             'manual' => 'Disable use of MIME::Base32',
516             }
517             };
518             }
519              
520             App::Muter::Registry->instance->register(__PACKAGE__);
521              
522             package App::Muter::Backend::Base32Hex;
523             $App::Muter::Backend::Base32Hex::VERSION = '0.003000';
524             our @ISA = qw/App::Muter::Backend::Base32/;
525              
526             sub new {
527 1502     1502   2745 my ($class, @args) = @_;
528 1502         2784 my $self = $class->SUPER::new(@args);
529             $self->{ftr} =
530 1502     1423   5197 sub { my $val = shift; $val =~ tr/\x00-\x1f/0-9A-V/; return $val };
  1423         2263  
  1423         2178  
  1423         4675  
531             $self->{rtr} =
532 1502     1420   3840 sub { my $val = shift; $val =~ tr/0-9A-V/\x00-\x1f/; return $val };
  1420         1918  
  1420         2153  
  1420         3516  
533 1502         2263 $self->{func} = 'base32hex';
534 1502         2207 return $self->_initialize;
535             }
536              
537             App::Muter::Registry->instance->register(__PACKAGE__);
538              
539             package App::Muter::Backend::URI;
540             $App::Muter::Backend::URI::VERSION = '0.003000';
541             our @ISA = qw/App::Muter::Backend::ChunkedDecode/;
542              
543             sub new {
544 2054     2054   4201 my ($class, $args, %opts) = @_;
545 2054         8359 my $self = $class->SUPER::new($args, %opts, regexp => qr/^(.*)(%.?)$/s);
546 2054         2955 my $lower = grep { $_ eq 'lower' } @$args;
  758         1551  
547 2054         2825 $self->{chunk} = '';
548 2054 100       6167 $self->{format} = '%%%02' . ($lower ? 'x' : 'X');
549 2054         2970 $self->{form} = grep { $_ eq 'form' } @$args;
  758         1411  
550 2054         6045 return $self;
551             }
552              
553             sub metadata {
554 8     8   11 my $self = shift;
555 8         30 my $meta = $self->SUPER::metadata;
556             return {
557 8         40 %$meta,
558             args => {
559             'upper' => 'Use uppercase letters',
560             'lower' => 'Use lowercase letters',
561             }
562             };
563             }
564              
565             sub encode_chunk {
566 4666     4666   6131 my ($self, $data) = @_;
567 4666         11884 $data =~ s/([^A-Za-z0-9-._~])/sprintf $self->{format}, ord($1)/ge;
  5653         17851  
568 4666 50       9155 $data =~ s/%20/+/g if $self->{form};
569 4666         10581 return $data;
570             }
571              
572             sub decode_chunk {
573 4728     4728   6447 my ($self, $data) = @_;
574 4728         5824 $data =~ tr/+/ /;
575 4728         10917 $data =~ s/%([0-9a-fA-F]{2})/chr(hex($1))/ge;
  5578         13778  
576 4728         17035 return $data;
577             }
578              
579             App::Muter::Registry->instance->register(__PACKAGE__);
580              
581             package App::Muter::Backend::Form;
582             $App::Muter::Backend::Form::VERSION = '0.003000';
583             our @ISA = qw/App::Muter::Backend::URI/;
584              
585             sub encode_chunk {
586 1225     1225   1711 my ($self, $data) = @_;
587 1225         1854 $data = $self->SUPER::encode_chunk($data);
588 1225         1894 $data =~ s/%20/+/g;
589 1225         2739 return $data;
590             }
591              
592             sub decode_chunk {
593 1252     1252   1698 my ($self, $data) = @_;
594 1252         1696 $data =~ tr/+/ /;
595 1252         2007 return $self->SUPER::decode_chunk($data);
596             }
597              
598             App::Muter::Registry->instance->register(__PACKAGE__);
599              
600             package App::Muter::Backend::XML;
601             $App::Muter::Backend::XML::VERSION = '0.003000';
602             our @ISA = qw/App::Muter::Backend::ChunkedDecode/;
603              
604             sub new {
605 2416     2416   5057 my ($class, $args, %opts) = @_;
606 2416         10121 my $self = $class->SUPER::new($args, %opts, regexp => qr/^(.*)(&[^;]*)$/);
607 4     4   3468 no warnings 'qw'; ## no critic (ProhibitNoWarnings)
  4         8  
  4         6480  
608 2416         10134 my $maps = {
609             default => [qw/quot amp apos lt gt/],
610             html => [qw/quot amp #x27 lt gt/],
611             hex => [qw/#x22 #x26 #x27 #x3c #x3e/],
612             };
613 2416   100     6208 my $type = $args->[0] // 'default';
614 2416 50       4690 $type = 'default' unless exists $maps->{$type};
615 2416         2839 @{$self->{fmap}}{qw/" & ' < >/} = map { "&$_;" } @{$maps->{$type}};
  2416         10031  
  12080         19244  
  2416         4347  
616 2416         3913 @{$self->{rmap}}{@{$maps->{default}}} = qw/" & ' < >/;
  2416         7457  
  2416         3319  
617 2416         9768 return $self;
618             }
619              
620             sub metadata {
621 4     4   7 my $self = shift;
622 4         20 my $meta = $self->SUPER::metadata;
623             return {
624 4         23 %$meta,
625             args => {
626             default => 'Use XML entity names',
627             html => 'Use HTML-friendly entity names for XML entities',
628             hex => 'Use hexadecimal entity names for XML entities',
629             }
630             };
631             }
632              
633             # XML encodes Unicode characters. However, muter only works on byte sequences,
634             # so immediately encode these into UTF-8.
635             sub _decode_char {
636 569     569   1321 my ($self, $char) = @_;
637 569 50       1482 return chr($1) if $char =~ /^#([0-9]+)$/;
638 569 100       2036 return chr(hex($1)) if $char =~ /^#x([a-fA-F0-9]+)$/;
639 309 50       1176 return $self->{rmap}{$char} if exists $self->{rmap}{$char};
640 0         0 die "Unknown XML entity &$char;";
641             }
642              
643             sub encode_chunk {
644 5733     5733   7664 my ($self, $data) = @_;
645 5733         10634 $data =~ s/(["&'<>])/$self->{fmap}{$1}/ge;
  569         1927  
646 5733         13638 return $data;
647             }
648              
649             sub decode_chunk {
650 5974     5974   8252 my ($self, $data) = @_;
651 5974         21329 require Encode;
652 5974         30350 $data =~ s/&([^;]+);/Encode::encode('UTF-8', $self->_decode_char($1))/ge;
  569         4386  
653 5974         38934 return $data;
654             }
655              
656             App::Muter::Registry->instance->register(__PACKAGE__);
657              
658             package App::Muter::Backend::QuotedPrintable;
659             $App::Muter::Backend::QuotedPrintable::VERSION = '0.003000';
660             our @ISA = qw/App::Muter::Backend::ChunkedDecode/;
661              
662             sub new {
663 806     806   1719 my ($class, $args, %opts) = @_;
664 806         3569 my $self =
665             $class->SUPER::new($args, %opts, regexp => qr/\A(.*)(=[^\n]?)\z/);
666 806         1813 $self->{curlen} = 0;
667 806 100       1648 $self->{smtp} = 1 if grep { $_ eq 'smtp' } @$args;
  28         90  
668 806         2557 return $self;
669             }
670              
671             sub encode {
672 2139     2139   2878 my ($self, $data) = @_;
673 2139         3425 $data = $self->{chunk} . $data;
674 2139         3170 $self->{chunk} = '';
675 2139 100       3659 if (length($data) < 7) {
676 1655         2261 $self->{chunk} = $data;
677 1655         4384 return '';
678             }
679 484 100       1288 if ($data =~ /\A(.*)(\n.{0,6})\z/) {
680 34         80 $self->{chunk} = $2;
681 34         58 $data = $1;
682             }
683 484         983 return $self->encode_chunk($data);
684             }
685              
686             sub encode_final {
687 508     508   692 my ($self, $data) = @_;
688 508         902 $data = $self->{chunk} . $data;
689 508         840 $self->{chunk} = '';
690 508         960 return $self->encode_chunk($data);
691             }
692              
693             sub encode_chunk {
694 992     992   1409 my ($self, $data) = @_;
695 992         3385 $data =~ s/([^\x20-\x3c\x3e-\x7e])/sprintf '=%02X', ord($1)/ge;
  2323         6890  
696 992 100       2137 $data =~ s/(^|=0A)\./$1=2E/g if $self->{smtp};
697 992 100       1661 $data =~ s/(^|=0A)F(rom )/$1=46$2/g if $self->{smtp};
698 992         1299 my $result = '';
699 992         1131 my $maxlen = 75;
700 992         2014 while ($self->{curlen} + length($data) > $maxlen) {
701 12         24 my $chunk = substr($data, 0, $maxlen - $self->{curlen});
702 12 50       31 $chunk = $1 if $chunk =~ /^(.*)(=.?)$/;
703 12         24 $data = substr($data, length($chunk));
704 12         19 $result .= $chunk;
705 12 50       25 if ($data) {
706 12         16 $result .= "=\n";
707 12         28 $self->{curlen} = 0;
708             }
709             }
710 992         1427 $result .= $data;
711 992         1209 $self->{curlen} += length($data);
712 992         3424 return $result;
713             }
714              
715             sub decode_chunk {
716 2230     2230   2991 my ($self, $data) = @_;
717 2230         3025 $data =~ s/=\n//g;
718 2230         3128 $data =~ s/=([0-9A-F]{2})/chr(hex($1))/ge;
  1209         2914  
719 2230         6613 return $data;
720             }
721              
722             sub metadata {
723 4     4   6 my $self = shift;
724 4         20 my $meta = $self->SUPER::metadata;
725             return {
726 4         17 %$meta,
727             args => {
728             smtp => 'Encode "." and "From " at beginning of line',
729             }
730             };
731             }
732              
733             App::Muter::Registry->instance->register(__PACKAGE__);
734              
735             package App::Muter::Backend::Vis;
736             $App::Muter::Backend::Vis::VERSION = '0.003000';
737             our @ISA = qw/App::Muter::Backend::ChunkedDecode/;
738              
739             sub new {
740 3568     3568   12814 my ($class, $args, %opts) = @_;
741 3568         28613 my $self = $class->SUPER::new($args, %opts,
742             regexp => qr/\A(.*?[^^\\-])?(\\.{0,3})\z/);
743 3568         10746 $self->_setup_maps(map { $_ => 1 } @$args);
  2428         9885  
744 3568         13283 $self->{chunk} = '';
745 3568         29471 return $self;
746             }
747              
748             sub _setup_maps {
749 3568     3568   10391 my ($self, %flags) = @_;
750 3568         10982 $self->{flags} = \%flags;
751 3568         23195 my $standard = {_id_map(0x21 .. 0x7e), 0x5c => "\\\\"};
752 3568         61544 my $default = {_meta_map(0x00 .. 0x20, 0x7f .. 0xff)};
753 3568         89003 my $octal = {_octal_map(0x00 .. 0x20, 0x7f .. 0xff)};
754 3568         335627 my $cstyle = {
755             %$default,
756             0x00 => "\\000",
757             0x07 => "\\a",
758             0x08 => "\\b",
759             0x09 => "\\t",
760             0x0a => "\\n",
761             0x0b => "\\v",
762             0x0c => "\\f",
763             0x0d => "\\r",
764             0x20 => "\\s",
765             };
766             my $wanted_map =
767 3568 100       40785 $flags{cstyle} ? $cstyle : $flags{octal} ? $octal : $default;
    100          
768             my @chars = (
769             ($flags{sp} || $flags{space} || $flags{white} ? () : (0x20)),
770             ($flags{tab} || $flags{white} ? () : (0x09)),
771 3568 100 100     56218 ($flags{nl} || $flags{white} ? () : (0x0a)),
    100 100        
    100 100        
772             );
773 3568 100       13935 my %glob_chars = _octal_map($flags{glob} ? (0x23, 0x2a, 0x3f, 0x5b) : ());
774 3568         9986 my $extras = {_id_map(0x09, 0x0a, 0x20)};
775 3568         124474 my $map = {%$standard, %$wanted_map, %glob_chars, _id_map(@chars)};
776 3568         136121 $self->{map} = [map { $map->{$_} } sort { $a <=> $b } keys %$map];
  913408         1502562  
  6160893         6407841  
777             $self->{rmap} = {
778 3568         1255620 reverse(%$standard), reverse(%$wanted_map),
779             reverse(%$extras), reverse(%$octal),
780             reverse(%$cstyle), reverse(%glob_chars),
781             "\\0" => 0x00
782             };
783 3568         430112 return;
784             }
785              
786             sub _id_map { ## no critic(RequireArgUnpacking)
787 10704     10704   100934 return map { $_ => chr($_) } @_;
  354322         1032298  
788             }
789              
790             sub _octal_map { ## no critic(RequireArgUnpacking)
791 7136     7136   18389 return map { $_ => sprintf('\%03o', $_) } @_;
  578040         1177188  
792             }
793              
794             sub _meta_map { ## no critic(RequireArgUnpacking)
795 3568     3568   8898 return map { $_ => _encode($_) } @_;
  578016         804290  
796             }
797              
798             sub _encode {
799 578016     578016   724417 my ($byte) = @_;
800 4     4   31 use bytes;
  4         9  
  4         26  
801 578016         666142 my $ascii = $byte & 0x7f;
802 578016         716911 for ($byte) {
803 578016         1618183 when ([0x00 .. 0x1f, 0x7f]) { return '\^' . chr($ascii ^ 0x40) }
  117744         306262  
804 460272         1425340 when ([0x80 .. 0x9f, 0xff]) { return '\M^' . chr($ascii ^ 0x40) }
  117744         668064  
805 342528         1577205 when ([0xa1 .. 0xfe]) { return '\M-' . chr($ascii) }
  335392         1053270  
806 7136         18616 when (0x20) { return '\040' }
  3568         9465  
807 3568         6529 when (0xa0) { return '\240' }
  3568         8593  
808 0         0 default { die sprintf 'Found byte value %#02x', $byte; }
  0         0  
809             }
810 0         0 return;
811             }
812              
813             sub encode {
814 10913     10913   16837 my ($self, $data) = @_;
815 10913         19988 $data = $self->{chunk} . $data;
816 10913 100 100     43657 if (length $data && substr($data, -1) eq "\0") {
817 425         998 $data = substr($data, 0, -1);
818 425         920 $self->{chunk} = "\0";
819             }
820             else {
821 10488         17445 $self->{chunk} = '';
822             }
823 10913         23101 return $self->SUPER::encode($data);
824             }
825              
826             sub encode_chunk {
827 12706     12706   19215 my ($self, $data) = @_;
828 12706         35865 my $result = join('', map { $self->{map}[$_] } unpack('C*', $data));
  25227         52133  
829 12706 100       32706 if ($self->{flags}{cstyle}) {
830             # Do this twice to fix multiple consecutive NUL bytes.
831 3171         11737 $result =~ s/\\000($|[^0-7])/\\0$1/g for 1 .. 2;
832             }
833 12706         36913 return $result;
834             }
835              
836             sub _decode {
837 25849     25849   38525 my ($self, $val) = @_;
838 4     4   1612 use bytes;
  4         8  
  4         27  
839 25849 100       51196 return '' if !length $val;
840 16080 100 50     81287 return chr($self->{rmap}{$val} // die "val '$_'") if $val =~ /^\\/;
841 5251         13332 return pack('C*', map { $self->{rmap}{$_} } split //, $val);
  9754         52203  
842             }
843              
844             sub decode_chunk {
845 10560     10560   18279 my ($self, $data) = @_;
846             return join('',
847 10560         47675 map { $self->_decode($_) }
  25849         44509  
848             split /(\\(?:M[-^].|\^.|[0-7]{3}|\\|[0abtnvfrs]))/,
849             $data);
850             }
851              
852             sub metadata {
853 4     4   7 my $self = shift;
854 4         23 my $meta = $self->SUPER::metadata;
855             return {
856 4         34 %$meta,
857             args => {
858             sp => 'Encode space',
859             space => 'Encode space',
860             tab => 'Encode tab',
861             nl => 'Encode newline',
862             white => 'Encode space, tab, and newline',
863             cstyle => 'Encode using C-like escape sequences',
864             octal => 'Encode using octal escape sequences',
865             glob => 'Encode characters recognized by glob(3) and hash mark',
866             }
867             };
868             }
869              
870             App::Muter::Registry->instance->register(__PACKAGE__);
871              
872             package App::Muter::Backend::Ascii85;
873             $App::Muter::Backend::Ascii85::VERSION = '0.003000';
874             our @ISA = qw/App::Muter::Backend::Chunked/;
875              
876             sub new {
877 734     734   1375 my ($class, @args) = @_;
878 734         1305 my $self = $class->SUPER::new(@args, enchunksize => 4, dechunksize => 5);
879 734         1057 $self->{start} = '';
880 734         2091 return $self;
881             }
882              
883             sub encode {
884 1150     1150   1449 my ($self, $data) = @_;
885 1150 100       1769 return '' unless length $data;
886 1149 100       1853 my $prefix = defined $self->{start} ? '<~' : '';
887 1149         1386 $self->{start} = undef;
888 1149         1733 return $prefix . $self->SUPER::encode($data);
889             }
890              
891             sub encode_final {
892 367     367   512 my ($self, $data) = @_;
893             return $self->SUPER::encode_final($data) .
894 367 100       647 (defined $self->{start} ? '' : '~>');
895             }
896              
897             sub _encode_seq {
898 849     849   1215 my ($x, $flag) = @_;
899 849 100 100     1477 return (89) if !$x && !$flag;
900 823         894 my @res;
901 823         1337 for (0 .. 4) {
902 4115         4431 push @res, $x % 85;
903 4115         5251 $x = int($x / 85);
904             }
905 823         2459 return reverse @res;
906             }
907              
908             sub encode_chunk {
909 1516     1516   2080 my (undef, $data) = @_;
910 1516         1846 my $rem = length($data) % 4;
911 1516 100       2052 my $pad = $rem ? (4 - $rem) : 0;
912 1516         2034 $data .= "\0" x $pad;
913 1516         2860 my @chunks = unpack("N*", $data);
914 1516 100       2195 my @last = $pad ? (pop @chunks) : ();
915 1516         2367 my $res = pack('C*', map { _encode_seq($_) } @chunks);
  589         784  
916 1516         2194 $res .= pack('C*', map { _encode_seq($_, 1) } @last);
  260         392  
917 1516         2103 $res =~ tr/\x00-\x54\x59/!-uz/;
918 1516 100       2684 $res = substr($res, 0, -$pad) if $pad;
919 1516         5448 return $res;
920             }
921              
922             sub decode {
923 1350     1350   1789 my ($self, $data) = @_;
924              
925 1350 100       2616 return '' unless length $data;
926              
927 995 100       1721 if (defined $self->{start}) {
928 489         744 $self->{start} .= $data;
929 489 100       1326 return '' unless length $self->{start} > 2;
930              
931 288 50       1207 ($data = $self->{start}) =~ s/^<~// or die 'Invalid Ascii85 prefix';
932 288         466 $self->{start} = undef;
933             }
934 794         1656 return $self->decode_chunk($self->{chunk} . $data);
935             }
936              
937             sub _decode_seq {
938 849     849   1641 my ($s) = @_;
939 849 100       1396 return 0 if $s eq 'z';
940 823 50       1310 die 'Invalid Ascii85 encoding' if $s gt 's8W-!';
941 823     4115   3528 my $val = List::Util::reduce { $a * 85 + ($b - 33) } (0, unpack('C*', $s));
  4115         5154  
942 823         3006 return $val;
943             }
944              
945             sub decode_chunk {
946 1502     1502   2097 my ($self, $data) = @_;
947 1502         1585 my @chunks;
948 1502         4800 push @chunks, _decode_seq($1) while $data =~ s/^(z|[^~]{5})//s;
949 1502         2144 $self->{chunk} = $data;
950 1502         4998 return pack('N*', @chunks);
951             }
952              
953             sub decode_final {
954 367     367   512 my ($self, $data) = @_;
955 367         582 $data = $self->{chunk} . $data;
956 367 100 100     897 return '' if defined $self->{start} && !length $data;
957 354         603 my $res = $self->decode_chunk($data);
958 354         513 $data = $self->{chunk};
959 354 50       1307 $data =~ s/~>$// or die "Missing Ascii85 trailer";
960 354         593 my $rem = length($data) % 5;
961 354 100       554 my $pad = $rem ? (5 - $rem) : 0;
962 354         841 $res .= $self->decode_chunk($data . 'u' x $pad);
963 354 100       791 $res = substr($res, 0, -$pad) if $pad;
964 354         1355 return $res;
965             }
966              
967             App::Muter::Registry->instance->register(__PACKAGE__);
968              
969             package App::Muter::Backend::Hash;
970             $App::Muter::Backend::Hash::VERSION = '0.003000';
971 4     4   3948 use Digest::MD5;
  4         8  
  4         172  
972 4     4   1029 use Digest::SHA;
  4         6485  
  4         2164  
973              
974             our @ISA = qw/App::Muter::Backend/;
975              
976             my $hashes = {};
977              
978             sub new {
979 56     56   144 my ($class, $args, @args) = @_;
980 56         109 my ($hash) = @$args;
981 56         195 my $self = $class->SUPER::new($args, @args);
982 56         197 $self->{hash} = $hashes->{$hash}->();
983 56         933 return $self;
984             }
985              
986             sub encode {
987 3949     3949   5206 my ($self, $data) = @_;
988 3949         10459 $self->{hash}->add($data);
989 3949         7001 return '';
990             }
991              
992             sub encode_final {
993 56     56   110 my ($self, $data) = @_;
994 56         190 $self->{hash}->add($data);
995 56         372 return $self->{hash}->digest;
996             }
997              
998             sub metadata {
999 4     4   7 my ($self, $data) = @_;
1000 4         16 my $meta = $self->SUPER::metadata;
1001 4         19 $meta->{args} = {map { $_ => "Use the $_ hash algorithm" } keys %$hashes};
  40         102  
1002 4         12 return $meta;
1003             }
1004              
1005             sub register_hash {
1006 40     40   61 my ($name, $code) = @_;
1007 40 50       61 return $hashes->{$name} unless $code;
1008 40         87 return $hashes->{$name} = $code;
1009             }
1010              
1011             register_hash('md5', sub { Digest::MD5->new });
1012             register_hash('sha1', sub { Digest::SHA->new });
1013             register_hash('sha224', sub { Digest::SHA->new(224) });
1014             register_hash('sha256', sub { Digest::SHA->new(256) });
1015             register_hash('sha384', sub { Digest::SHA->new(384) });
1016             register_hash('sha512', sub { Digest::SHA->new(512) });
1017             register_hash('sha3-224', sub { require Digest::SHA3; Digest::SHA3->new(224) });
1018             register_hash('sha3-256', sub { require Digest::SHA3; Digest::SHA3->new(256) });
1019             register_hash('sha3-384', sub { require Digest::SHA3; Digest::SHA3->new(384) });
1020             register_hash('sha3-512', sub { require Digest::SHA3; Digest::SHA3->new(512) });
1021             App::Muter::Registry->instance->register(__PACKAGE__);
1022              
1023             __END__
1024              
1025             =pod
1026              
1027             =encoding UTF-8
1028              
1029             =head1 NAME
1030              
1031             App::Muter - tool to convert between various formats and encodings
1032              
1033             =head1 VERSION
1034              
1035             version 0.003000
1036              
1037             =head1 DESCRIPTION
1038              
1039             App::Muter provides the C<muter> command, which converts data between various
1040             formats.
1041              
1042             For more information, see L<muter>.
1043              
1044             =head1 AUTHOR
1045              
1046             brian m. carlson <sandals@crustytoothpaste.net>
1047              
1048             =head1 COPYRIGHT AND LICENSE
1049              
1050             This software is Copyright (c) 2016–2017 by brian m. carlson.
1051              
1052             This is free software, licensed under:
1053              
1054             The MIT (X11) License
1055              
1056             =cut