File Coverage

lib/Codec/CBOR.pm
Criterion Covered Total %
statement 23 25 92.0
branch 2 4 50.0
condition 4 6 66.6
subroutine 8 8 100.0
pod 0 2 0.0
total 37 45 82.2


line stmt bran cond sub pod time code
1 2     2   402937 use v5.40;
  2         7  
2 2     2   13 use feature 'class';
  2         3  
  2         400  
3 2     2   14 no warnings 'experimental::class';
  2         12  
  2         409  
4             #
5             package # Simple boolean wrapper
6             Codec::CBOR::Boolean {
7 2 0   2   13 use overload 'bool' => sub { ${ $_[0] } }, '""' => sub { ${ $_[0] } ? 'true' : 'false' }, fallback => 1;
  2     4   5  
  2         30  
  4         2845  
  4         15  
  0         0  
  0         0  
8 8 100   8   14 sub new ( $class, $val ) { my $v = $val ? 1 : 0; bless \$v, $class }
  8         15  
  8         17  
  8         10  
  8         19  
  8         41  
9             }
10             #
11             class Codec::CBOR v1.0.1 {
12             field %class_handlers;
13             field %tag_handlers = (
14             42 => sub ($data) { # Default Tag 42 handler (generic)
15             my $cid_raw = $data;
16             return { cid_raw => substr( $cid_raw, 1 ) } if length($cid_raw) > 0 && substr( $cid_raw, 0, 1 ) eq "\x00";
17             return { cid_raw => $cid_raw };
18             }
19             );
20             #
21             method add_tag_handler ( $tag, $cb ) { $tag_handlers{$tag} = $cb }
22             method add_class_handler ( $class, $cb ) { $class_handlers{$class} = $cb }
23 2   66 2 0 9 sub true_obj { state $r //= Codec::CBOR::Boolean->new(1); $r; }
  2         9  
24 2   66 2 0 50 sub false_obj { state $r //= Codec::CBOR::Boolean->new(0); $r; }
  2         43  
25             method encode ($data) { $self->_encode_item($data) }
26              
27             method decode ($input) {
28             if ( !ref $input ) {
29             open my $fh, '<:raw', \$input;
30             return $self->_decode_item($fh);
31             }
32             $self->_decode_item($input);
33             }
34              
35             method decode_sequence ($input) {
36             my $fh;
37             if ( !ref $input ) {
38             open $fh, '<:raw', \$input;
39             }
40             else {
41             $fh = $input;
42             }
43             my @items;
44             my $safety = 0;
45             my $last_pos = tell($fh);
46             while ( !eof($fh) ) {
47             my $item;
48             try { $item = $self->_decode_item($fh); }
49             catch ($e) {
50             last;
51             }
52             my $curr_pos = tell($fh);
53             last if $curr_pos <= $last_pos && !eof($fh); # Failed to advance but not at EOF? Stop to avoid infinite loop.
54             $last_pos = $curr_pos;
55             push @items, $item if defined $item || !eof($fh);
56             last if ++$safety > 10000;
57             }
58             wantarray ? @items : \@items;
59             }
60              
61             method _encode_item ( $item //= () ) {
62             return pack 'C', 0xf6 unless defined $item; # null
63             my $ref = ref($item);
64             if ( !$ref ) {
65             return pack( 'C', 0xfb ) . pack( 'd>', $item ) if $item =~ /^-?\d+\.\d+$/; # Float check (simple heuristic for now)
66             return $self->_encode_int($item) if $item =~ /^-?\d+$/ && length($item) < 20; # Integer or UTF-8 String
67             return $self->_encode_utf8($item);
68             }
69             return $self->_encode_bytes($$item) if $ref eq 'SCALAR';
70             return $self->_encode_array($item) if $ref eq 'ARRAY';
71             return $self->_encode_hash($item) if $ref eq 'HASH';
72             return pack( "C", $$item ? 0xf5 : 0xf4 ) if builtin::blessed($item) && $item->isa('Codec::CBOR::Boolean');
73              
74             # Handle registered classes (like CID)
75             for my $class ( keys %class_handlers ) {
76             return $class_handlers{$class}->( $self, $item ) if builtin::blessed($item) && $item->isa($class);
77             }
78              
79             # Fallback for generic CID-like objects if not registered
80             if ( builtin::blessed($item) && $item->can('raw') ) {
81             my $raw = $item->raw;
82             return pack( 'C', 0xd8 ) . pack( 'C', 42 ) . $self->_encode_bytes( "\x00" . $raw );
83             }
84             die 'Codec::CBOR: Cannot encode ' . $ref;
85             }
86              
87             method _encode_int ($val) {
88             return $self->_encode_header( 0, $val ) if $val >= 0;
89             $self->_encode_header( 1, -1 - $val );
90             }
91              
92             method _encode_header ( $major, $val ) {
93             return pack( 'C', ( $major << 5 ) | $val ) if $val < 24;
94             return pack( 'CC', ( $major << 5 ) | 24, $val ) if $val < 256;
95             return pack( 'Cn', ( $major << 5 ) | 25, $val ) if $val < 65536;
96             return pack( 'CN', ( $major << 5 ) | 26, $val ) if $val < 4294967296;
97             pack( 'CQ>', ( $major << 5 ) | 27, $val );
98             }
99              
100             method _encode_utf8 ($str) {
101             my $encoded = $str;
102             utf8::encode($encoded) if utf8::is_utf8($encoded);
103             $self->_encode_header( 3, length($encoded) ) . $encoded;
104             }
105             method _encode_bytes ($bytes) { $self->_encode_header( 2, length($bytes) ) . $bytes }
106              
107             method _encode_array ($arr) {
108             my $out = $self->_encode_header( 4, scalar @$arr );
109             $out .= $self->_encode_item($_) for @$arr;
110             $out;
111             }
112              
113             method _encode_hash ($hash) { # DAG-CBOR deterministic sort: length first, then lexical
114             my @keys = sort { length($a) <=> length($b) || $a cmp $b } keys %$hash;
115             my $out = $self->_encode_header( 5, scalar @keys );
116             for my $k (@keys) {
117             $out .= $self->_encode_utf8($k);
118             $out .= $self->_encode_item( $hash->{$k} );
119             }
120             $out;
121             }
122              
123             method _decode_item ($fh) {
124             return undef unless defined $fh;
125             return undef if eof($fh);
126             read( $fh, my $byte, 1 ) or return undef;
127             my $b = ord($byte);
128             my $major = $b >> 5;
129             my $info = $b & 0x1f;
130             if ( $major == 0 ) { return $self->_decode_value( $info, $fh ); }
131             if ( $major == 1 ) { return -1 - $self->_decode_value( $info, $fh ); }
132              
133             if ( $major == 2 ) { # Byte string
134             my $len = $self->_decode_value( $info, $fh );
135             read( $fh, my $buf, $len );
136             return $buf;
137             }
138             if ( $major == 3 ) { # UTF-8 string
139             my $len = $self->_decode_value( $info, $fh );
140             read( $fh, my $buf, $len );
141             my $decoded = $buf;
142             return $decoded if utf8::decode($decoded);
143              
144             # Fallback for invalid UTF-8: return raw bytes
145             return $buf;
146             }
147             if ( $major == 4 ) { # Array
148             my $len = $self->_decode_value( $info, $fh );
149             my @arr;
150             push @arr, $self->_decode_item($fh) for 1 .. $len;
151             return \@arr;
152             }
153             if ( $major == 5 ) { # Map
154             my $len = $self->_decode_value( $info, $fh );
155             my %hash;
156             for ( 1 .. $len ) {
157             my $k = $self->_decode_item($fh);
158             my $v = $self->_decode_item($fh);
159             $hash{$k} = $v if defined $k;
160             }
161             return \%hash;
162             }
163             if ( $major == 6 ) { # Tag
164             my $tag = $self->_decode_value( $info, $fh );
165             my $val = $self->_decode_item($fh);
166             return $tag_handlers{$tag}->($val) if exists $tag_handlers{$tag};
167             return $val;
168             }
169             if ( $major == 7 ) { # Simple / Float
170             return Codec::CBOR::Boolean->new(0) if $info == 20;
171             return Codec::CBOR::Boolean->new(1) if $info == 21;
172             return undef if $info == 22;
173             if ( $info == 25 ) { read( $fh, my $b, 2 ); return unpack( 'f>', $b ); }
174             if ( $info == 26 ) { read( $fh, my $b, 4 ); return unpack( 'f>', $b ); }
175             if ( $info == 27 ) { read( $fh, my $b, 8 ); return unpack( 'd>', $b ); }
176             return $self->_decode_value( $info, $fh );
177             }
178             die 'Codec::CBOR: Unsupported major type ' . $major;
179             }
180              
181             method _decode_value ( $info, $fh ) {
182             return $info if $info < 24;
183             if ( $info == 24 ) { read( $fh, my $b, 1 ); return unpack( 'C', $b ); }
184             if ( $info == 25 ) { read( $fh, my $b, 2 ); return unpack( 'n', $b ); }
185             if ( $info == 26 ) { read( $fh, my $b, 4 ); return unpack( 'N', $b ); }
186             if ( $info == 27 ) { read( $fh, my $b, 8 ); return unpack( 'Q>', $b ); }
187             die 'Codec::CBOR: Indefinite length or invalid info ' . $info;
188             }
189             };
190             #
191             1;