File Coverage

blib/lib/Messaging/Message.pm
Criterion Covered Total %
statement 229 263 87.0
branch 97 116 83.6
condition 46 86 53.4
subroutine 45 49 91.8
pod 18 18 100.0
total 435 532 81.7


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Messaging/Message.pm #
4             # #
5             # Description: abstraction of a message #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Messaging::Message;
14 5     5   508258 use strict;
  5         13  
  5         176  
15 5     5   31 use warnings;
  5         13  
  5         431  
16             our $VERSION = "1.5";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.25 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 5     5   3279 use Encode qw(encode decode FB_CROAK LEAVE_SRC);
  5         40151  
  5         484  
24 5     5   5982 use JSON qw();
  5         92607  
  5         179  
25 5     5   5664 use MIME::Base64 qw(encode_base64 decode_base64);
  5         4305  
  5         499  
26 5     5   4864 use No::Worries::Die qw(dief);
  5         149307  
  5         39  
27 5     5   681 use No::Worries::Export qw(export_control);
  5         12  
  5         34  
28 5     5   559 use Params::Validate qw(validate validate_pos :types);
  5         45  
  5         28235  
29              
30             #
31             # global variables
32             #
33              
34             our(
35             %_LoadedModule, # hash of successfully loaded modules
36             %_CompressionModule, # known compression modules
37             $_CompressionAlgos, # known compression algorithms
38             $_JSON, # JSON object
39             );
40              
41             %_CompressionModule = (
42             "lz4" => "LZ4",
43             "snappy" => "Snappy",
44             "zlib" => "Zlib",
45             );
46             $_CompressionAlgos = join("|", sort(keys(%_CompressionModule)));
47             $_JSON = JSON->new();
48              
49             #+++############################################################################
50             # #
51             # helper functions #
52             # #
53             #---############################################################################
54              
55             #
56             # make sure a module is loaded
57             #
58              
59             sub _require ($) {
60 15     15   27 my($module) = @_;
61              
62 15 100       58 return if $_LoadedModule{$module};
63 2         156 eval("require $module"); ## no critic 'ProhibitStringyEval'
64 2 50       11 if ($@) {
65 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
66 0         0 dief("failed to load %s: %s", $module, $@);
67             } else {
68 2         9 $_LoadedModule{$module} = 1;
69             }
70             }
71              
72             #
73             # evaluate some code with fatal warnings
74             #
75              
76             sub _eval ($&;$) {
77 180     180   7649 my($what, $code, $arg) = @_;
78              
79 180         209 eval {
80 180     0   881 local $SIG{__WARN__} = sub { die($_[0]) };
  0         0  
81 180         371 $code->($arg);
82             };
83 180 100       10380 return unless $@;
84 3         38 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
85 3         13 dief("%s failed: %s", $what, $@);
86             }
87              
88             #
89             # helpers for body encoding and compression
90             #
91              
92             sub _maybe_base64_encode ($) {
93 15     15   18 my($object) = @_;
94              
95 15 50       65 return unless $object->{"body"} =~ /[^\t\n\r\x20-\x7e]/;
96             # only if it contains more than printable ASCII characters (plus \t \n \r)
97             _eval("Base64 encoding", sub {
98 15     15   110 $object->{"body"} = encode_base64($object->{"body"}, "");
99 15         60 });
100 15         60 $object->{"encoding"}{"base64"}++;
101             }
102              
103             sub _maybe_utf8_encode ($) {
104 3     3   5 my($object) = @_;
105 3         4 my($tmp);
106              
107             _eval("UTF-8 encoding", sub {
108 3     3   28 $tmp = encode("UTF-8", $object->{"body"}, FB_CROAK|LEAVE_SRC);
109 3         19 });
110 3 100       18 return if $tmp eq $object->{"body"};
111 1         3 $object->{"body"} = $tmp;
112 1         4 $object->{"encoding"}{"utf8"}++;
113             }
114              
115             sub _do_compress ($$) {
116 6     6   11 my($object, $algo) = @_;
117 6         10 my($compress, $tmp);
118              
119 6         7 $compress = \&{"Compress::$_CompressionModule{$algo}::compress"};
  6         30  
120             _eval("$_CompressionModule{$algo} compression", sub {
121 6     6   21 $tmp = $compress->(\$object->{"body"});
122 6         35 });
123 6         22 $object->{"body"} = $tmp;
124 6         26 $object->{"encoding"}{$algo}++;
125             }
126              
127             #+++############################################################################
128             # #
129             # object oriented interface #
130             # #
131             #---############################################################################
132              
133             #
134             # normal constructor
135             #
136              
137             my %new_options = (
138             "header" => {
139             type => HASHREF,
140             callbacks => {
141             "hash of strings" =>
142             sub { grep(!defined($_)||ref($_), values(%{$_[0]})) == 0 },
143             },
144             optional => 1,
145             },
146             "body" => {
147             type => SCALAR,
148             optional => 1,
149             },
150             "body_ref" => {
151             type => SCALARREF,
152             optional => 1,
153             },
154             "text" => {
155             type => BOOLEAN,
156             optional => 1,
157             },
158             );
159              
160             sub new : method {
161 222     222 1 99468 my($class, %option, $body, $self);
162              
163 222         289 $class = shift(@_);
164 222 100       3299 %option = validate(@_, \%new_options) if @_;
165 219 100 100     1283 dief("new(): options body and body_ref are mutually exclusive")
166             if exists($option{"body"}) and exists($option{"body_ref"});
167             # default message
168 218         276 $body = "";
169 218         1293 $self = { "header" => {}, "body_ref" => \$body, "text" => 0 };
170             # handle options
171 218 100       541 $self->{"header"} = $option{"header"} if exists($option{"header"});
172 218 100       577 $self->{"body_ref"} = $option{"body_ref"} if exists($option{"body_ref"});
173 218 100       410 $self->{"body_ref"} = \$option{"body"} if exists($option{"body"});
174 218 100       576 $self->{"text"} = $option{"text"} ? 1 : 0 if exists($option{"text"});
    100          
175             # so far so good!
176 218         391 bless($self, $class);
177 218         933 return($self);
178             }
179              
180             #
181             # normal accessors
182             #
183              
184             sub header : method {
185 58     58 1 9933 my($self);
186              
187 58         99 $self = shift(@_);
188 58 50       396 return($self->{"header"}) if @_ == 0;
189 0         0 validate_pos(@_, $new_options{"header"});
190 0         0 $self->{"header"} = $_[0];
191 0         0 return($self);
192             }
193              
194             sub body_ref : method {
195 2     2 1 260 my($self);
196              
197 2         3 $self = shift(@_);
198 2 100       8 return($self->{"body_ref"}) if @_ == 0;
199 1 50 33     10 validate_pos(@_, $new_options{"body_ref"})
      33        
200             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
201 1         2 $self->{"body_ref"} = $_[0];
202 1         2 return($self);
203             }
204              
205             sub text : method {
206 74     74 1 1414 my($self);
207              
208 74         119 $self = shift(@_);
209 74 100       311 return($self->{"text"}) if @_ == 0;
210 1 50 33     10 validate_pos(@_, $new_options{"text"})
      33        
211             unless @_ == 1 and (not defined($_[0]) or ref($_[0]) eq "");
212 1 50       4 $self->{"text"} = $_[0] ? 1 : 0;
213 1         2 return($self);
214             }
215              
216             #
217             # extra accessors
218             #
219              
220             sub header_field : method {
221 82     82 1 7094 my($self);
222              
223 82         127 $self = shift(@_);
224 82 100 66     739 if (@_ >= 1 and defined($_[0]) and ref($_[0]) eq "") {
      100        
225 80 100       403 return($self->{"header"}{$_[0]}) if @_ == 1;
226 2 100 33     37 if (@_ == 2 and defined($_[1]) and ref($_[1]) eq "") {
      66        
227 1         3 $self->{"header"}{$_[0]} = $_[1];
228 1         2 return($self);
229             }
230             }
231             # so far so bad :-(
232 3         665 validate_pos(@_, { type => SCALAR }, { type => SCALAR, optional => 1 });
233             }
234              
235             sub body : method {
236 50     50 1 215 my($self, $body);
237              
238 50         69 $self = shift(@_);
239 50 100       125 return(${ $self->{"body_ref"} }) if @_ == 0;
  49         193  
240 1 50 33     10 validate_pos(@_, $new_options{"body"})
      33        
241             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
242 1         2 $body = $_[0]; # copy
243 1         2 $self->{"body_ref"} = \$body;
244 1         2 return($self);
245             }
246              
247             #
248             # extra methods
249             #
250              
251             sub copy : method {
252 7     7 1 6313 my($self, %header, $body, $copy);
253              
254 7         11 $self = shift(@_);
255 7 50       13 validate_pos(@_) if @_;
256 7         8 %header = %{ $self->{"header"} }; # copy
  7         18  
257 7         8 $body = ${ $self->{"body_ref"} }; # copy
  7         11  
258 7         22 $copy = {
259             "header" => \%header,
260             "body_ref" => \$body,
261             "text" => $self->{"text"},
262             };
263 7         15 bless($copy, ref($self));
264 7         39 return($copy);
265             }
266              
267             sub size : method {
268 0     0 1 0 my($self, $size, $key, $value);
269              
270 0         0 $self = shift(@_);
271 0 0       0 validate_pos(@_) if @_;
272 0         0 $size = 1 + length(${ $self->{"body_ref"} });
  0         0  
273 0         0 while (($key, $value) = each(%{ $self->{"header"} })) {
  0         0  
274 0         0 $size += 2 + length($key) + length($value);
275             }
276 0         0 return($size);
277             }
278              
279             #+++############################################################################
280             # #
281             # (de)jsonification #
282             # #
283             #---############################################################################
284              
285             #
286             # jsonify (= transform into a JSON object)
287             #
288              
289             my %jsonify_options = (
290             "compression" => {
291             type => SCALAR,
292             regex => qr/^($_CompressionAlgos)?!?$/o,
293             optional => 1,
294             },
295             );
296              
297             sub _jsonify_text ($$$$$) {
298 10     10   18 my($self, $object, $algo, $force, $len) = @_;
299              
300 10 50 66     98 if ($algo and $force) {
    100 66        
301             # always compress
302 0         0 _maybe_utf8_encode($object);
303 0         0 _do_compress($object, $algo);
304 0         0 _maybe_base64_encode($object);
305             } elsif ($algo and $len > 255) {
306             # maybe compress
307 3         9 _maybe_utf8_encode($object);
308 3         7 _do_compress($object, $algo);
309 3         9 _maybe_base64_encode($object);
310 3 50       14 if (length($object->{"body"}) >= $len) {
311             # not worth it
312 0         0 $object->{"body"} = ${ $self->{"body_ref"} };
  0         0  
313 0         0 delete($object->{"encoding"});
314             }
315             } else {
316             # do not compress
317             }
318             }
319              
320             sub _jsonify_binary ($$$$$) {
321 12     12   24 my($self, $object, $algo, $force, $len) = @_;
322              
323 12 50 66     60 if ($algo and $force) {
    100 66        
324             # always compress
325 0         0 _do_compress($object, $algo);
326 0         0 _maybe_base64_encode($object);
327             } elsif ($algo and $len > 255) {
328             # maybe compress
329 3 100       63 $len *= 4/3 if $object->{"body"} =~ /[^\t\n\r\x20-\x7e]/;
330 3         9 _do_compress($object, $algo);
331 3         9 _maybe_base64_encode($object);
332 3 50       13 if (length($object->{"body"}) >= $len) {
333             # not worth it
334 0         0 $object->{"body"} = ${ $self->{"body_ref"} };
  0         0  
335 0         0 delete($object->{"encoding"});
336 0         0 _maybe_base64_encode($object);
337             }
338             } else {
339             # do not compress
340 9         18 _maybe_base64_encode($object);
341             }
342             }
343              
344             sub jsonify : method {
345 38     38 1 82 my($self, %option, %object, $algo, $force, $len);
346              
347 38         45 $self = shift(@_);
348 38 100       161 %option = validate(@_, \%jsonify_options) if @_;
349 38 100 66     230 if ($option{"compression"} and $option{"compression"} =~ /^(\w+)(!?)$/) {
350 6         21 ($algo, $force) = ($1, $2);
351             }
352             # check compression availability
353 38 100       88 _require("Compress::$_CompressionModule{$algo}") if $algo;
354             # build the JSON object
355 38 100       114 $object{"text"} = JSON::true if $self->{"text"};
356 38 100       75 $object{"header"} = $self->{"header"} if keys(%{ $self->{"header"} });
  38         124  
357 38         40 $len = length(${ $self->{"body_ref"} });
  38         60  
358 38 100       112 return(\%object) unless $len;
359 22         20 $object{"body"} = ${ $self->{"body_ref"} };
  22         45  
360             # handle non-empty body
361 22 100       46 if ($self->{"text"}) {
362             # text body
363 10         23 _jsonify_text($self, \%object, $algo, $force, $len);
364             } else {
365             # binary body
366 12         54 _jsonify_binary($self, \%object, $algo, $force, $len);
367             }
368             # set the encoding string
369 22 100       49 $object{"encoding"} = join("+", sort(keys(%{ $object{"encoding"} })))
  15         71  
370             if $object{"encoding"};
371             # so far so good!
372 22         68 return(\%object);
373             }
374              
375             #
376             # dejsonify (= alternate constructor using the JSON object)
377             #
378              
379             my %dejsonify_options = (
380             "header" => $new_options{"header"},
381             "body" => {
382             type => SCALAR,
383             optional => 1,
384             },
385             "text" => {
386             type => OBJECT,
387             callbacks => {
388             "JSON::is_bool" => sub { JSON::is_bool($_[0]) },
389             },
390             optional => 1,
391             },
392             "encoding" => {
393             type => SCALAR,
394             optional => 1,
395             },
396             );
397              
398             sub dejsonify : method {
399 74     74 1 109 my($class, $object, $encoding, $self, $tmp, $len, $uncompress);
400              
401 74         100 $class = shift(@_);
402 74 100 33     3663 validate_pos(@_, { type => HASHREF })
      66        
403             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "HASH";
404 73         1659 validate(@_, \%dejsonify_options);
405 70         349 $object = $_[0];
406 70   100     256 $encoding = $object->{"encoding"} || "";
407 70 100 100     444 dief("invalid encoding: %s", $encoding)
408             unless $encoding eq ""
409             or "${encoding}+" =~ /^((base64|utf8|$_CompressionAlgos)\+)+$/o;
410 69 100       337 _require("Compress::$_CompressionModule{$1}")
411             if $encoding =~ /($_CompressionAlgos)/o;
412             # construct the message
413 69         230 $self = $class->new();
414 69 100       176 $self->{"text"} = 1 if $object->{"text"};
415 40         192 $self->{"header"} = $object->{"header"}
416 69 100 100     283 if $object->{"header"} and keys(%{ $object->{"header"} });
417 69 100       197 if (exists($object->{"body"})) {
418 57         90 $tmp = $object->{"body"};
419 57 100       151 if ($encoding =~ /base64/) {
420             # body has been Base64 encoded, compute length to detect unexpected
421             # characters (this is because MIME::Base64 silently ignores them)
422 26         57 $len = length($tmp);
423 26 100       73 dief("invalid Base64 data: %s", $object->{"body"}) if $len % 4;
424 25         55 $len = $len * 3 / 4;
425 25         66 $len -= substr($tmp, -2) =~ tr/=/=/;
426             _eval("Base64 decoding", sub {
427 25     25   472 $tmp = decode_base64($tmp);
428 25         101 });
429 25 100       101 dief("invalid Base64 data: %s", $object->{"body"})
430             unless $len == length($tmp);
431             }
432 54 100       217 if ($encoding =~ /($_CompressionAlgos)/o) {
433             # body has been compressed
434 9         20 $uncompress = \&{"Compress::$_CompressionModule{$1}::uncompress"};
  9         72  
435             _eval("$_CompressionModule{$1} decompression", sub {
436 9     9   44 $tmp = $uncompress->(\$tmp);
437 9         59 });
438 9 50       44 dief("invalid $_CompressionModule{$1} compressed data!")
439             unless defined($tmp);
440             }
441 54 100       120 if ($encoding =~ /utf8/) {
442             # body has been UTF-8 encoded
443             _eval("UTF-8 decoding", sub {
444 4     4   29 $tmp = decode("UTF-8", $tmp, FB_CROAK);
445 4         23 });
446             }
447 54         113 $self->{"body_ref"} = \$tmp;
448             }
449             # so far so good!
450 66         325 return($self);
451             }
452              
453             #+++############################################################################
454             # #
455             # (de)stringification #
456             # #
457             #---############################################################################
458              
459             #
460             # stringify (= transform into a text string)
461             #
462              
463             sub stringify : method {
464 7     7 1 9125 my($self, $tmp);
465              
466 7         14 $self = shift(@_);
467 7         15 $tmp = $self->jsonify(@_);
468             _eval("JSON encoding", sub {
469 7     7   63 $tmp = $_JSON->encode($tmp);
470 7         27 });
471 7         23 return($tmp);
472             }
473              
474             sub stringify_ref : method {
475 18     18 1 20 my($self, $tmp);
476              
477 18         20 $self = shift(@_);
478 18         35 $tmp = $self->jsonify(@_);
479             _eval("JSON encoding", sub {
480 18     18   168 $tmp = $_JSON->encode($tmp);
481 18         69 });
482 18         49 return(\$tmp);
483             }
484              
485             #
486             # destringify (= alternate constructor using the stringified representation)
487             #
488              
489             sub destringify : method {
490 36     36 1 59 my($class, $tmp);
491              
492 36         45 $class = shift(@_);
493 36 50 33     209 validate_pos(@_, { type => SCALAR })
      33        
494             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
495             _eval("JSON decoding", sub {
496 36     36   39 $tmp = $_JSON->decode(${ $_[0] });
  36         610  
497 36         138 }, \$_[0]);
498 34         124 return($class->dejsonify($tmp));
499             }
500              
501             sub destringify_ref : method {
502 33     33 1 43 my($class, $tmp);
503              
504 33         75 $class = shift(@_);
505 33 50 33     403 validate_pos(@_, { type => SCALARREF })
      33        
506             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
507             _eval("JSON decoding", sub {
508 33     33   45 $tmp = $_JSON->decode(${ $_[0] });
  33         1791  
509 33         224 }, $_[0]);
510 33         163 return($class->dejsonify($tmp));
511             }
512              
513             #+++############################################################################
514             # #
515             # (de)serialization #
516             # #
517             #---############################################################################
518              
519             #
520             # serialize (= transform into a binary string)
521             #
522              
523             sub serialize : method {
524 18     18 1 6490 my($self, $tmp);
525              
526 18         25 $self = shift(@_);
527 18         39 $tmp = $self->stringify_ref(@_);
528             _eval("UTF-8 encoding", sub {
529 18     18   18 $tmp = encode("UTF-8", ${ $tmp }, FB_CROAK);
  18         75  
530 18         57 });
531 18         67 return($tmp);
532             }
533              
534             sub serialize_ref : method {
535 0     0 1 0 my($self, $tmp);
536              
537 0         0 $self = shift(@_);
538 0         0 $tmp = $self->stringify_ref(@_);
539             _eval("UTF-8 encoding", sub {
540 0     0   0 $tmp = encode("UTF-8", ${ $tmp }, FB_CROAK);
  0         0  
541 0         0 });
542 0         0 return(\$tmp);
543             }
544              
545             #
546             # deserialize (= alternate constructor using the serialized representation)
547             #
548              
549             sub deserialize : method {
550 28     28 1 6822 my($class, $tmp);
551              
552 28         37 $class = shift(@_);
553 28 50 33     185 validate_pos(@_, { type => SCALAR })
      33        
554             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
555 28 100       114 return($class->destringify($_[0])) unless $_[0] =~ /[^[:ascii:]]/;
556             _eval("UTF-8 decoding", sub {
557 4     4   4 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  4         25  
558 4         18 }, \$_[0]);
559 3         57 return($class->destringify($tmp));
560             }
561              
562             sub deserialize_ref : method {
563 35     35 1 144302 my($class, $tmp);
564              
565 35         79 $class = shift(@_);
566 35 50 33     527 validate_pos(@_, { type => SCALARREF })
      33        
567             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
568 35 100       53 return($class->destringify_ref($_[0])) unless ${ $_[0] } =~ /[^[:ascii:]]/;
  35         997  
569             _eval("UTF-8 decoding", sub {
570 2     2   4 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  2         17  
571 2         17 }, $_[0]);
572 2         15 return($class->destringify($tmp));
573             }
574              
575             #
576             # export control
577             #
578              
579             sub import : method {
580 6     6   40 my($pkg, %exported);
581              
582 6         14 $pkg = shift(@_);
583 6         15 %exported = ("_require" => 1);
584 6         38 export_control(scalar(caller()), $pkg, \%exported, @_);
585             }
586              
587             1;
588              
589             __DATA__