File Coverage

blib/lib/Image/ExifTool/Protobuf.pm
Criterion Covered Total %
statement 113 141 80.1
branch 77 116 66.3
condition 33 53 62.2
subroutine 8 8 100.0
pod 0 5 0.0
total 231 323 71.5


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Protobuf.pm
3             #
4             # Description: Decode protocol buffer data
5             #
6             # Revisions: 2024-12-04 - P. Harvey Created
7             #
8             # Notes: Tag definitions for Protobuf tags support 'signed', 'unsigned',
9             # and 'int64s' formats for VARINT (type 0) values, 'int64u',
10             # 'int64s', 'rational64u', 'rational64s' and 'double' for I64
11             # (type 1), 'undef', 'string' and 'rational' for LEN (type 2),
12             # and 'int32u', 'int32s', 'rational32u', 'rational32s',
13             # 'fixed32u', 'fixed32s' and 'float' for I32 (type 5) values.
14             #
15             # References: 1) https://protobuf.dev/programming-guides/encoding/
16             #------------------------------------------------------------------------------
17              
18             package Image::ExifTool::Protobuf;
19              
20 12     12   87 use strict;
  12         35  
  12         555  
21 12     12   95 use vars qw($VERSION);
  12         30  
  12         672  
22 12     12   68 use Image::ExifTool qw(:DataAccess :Utils);
  12         30  
  12         28432  
23              
24             $VERSION = '1.07';
25              
26             sub ProcessProtobuf($$$;$);
27              
28             # largest unsigned integer on this system (2^32 or 2^64 - 1)
29             my $intMax = ~0;
30              
31             # smallest unsigned integer that we interpret as int64s (0xffffffff00000000)
32             my $int64sMin = 18446744069414584320;
33              
34             #------------------------------------------------------------------------------
35             # Read bytes from dirInfo object
36             # Inputs: 0) dirInfo ref (with DataPt and Pos set), 1) number of bytes
37             # Returns: binary data or undef on error
38             sub GetBytes($$)
39             {
40 57578     57578 0 76153 my ($dirInfo, $n) = @_;
41 57578         74419 my $dataPt = $$dirInfo{DataPt};
42 57578         64305 my $pos = $$dirInfo{Pos};
43 57578 100       86652 return undef if $pos + $n > length $$dataPt;
44 57497         66834 $$dirInfo{Pos} += $n;
45 57497         101326 return substr($$dataPt, $pos, $n);
46             }
47              
48             #------------------------------------------------------------------------------
49             # Read variable-length integer
50             # Inputs: 0) dirInfo ref
51             # Returns: integer value
52             # - sets $$dirInfo{Bit0} according to bit 0 of returned value
53             # (necessary for cases where a signed integer exceeds $intMax)
54             sub VarInt($)
55             {
56 39576     39576 0 43949 my $dirInfo = shift;
57 39576         50557 my $buff = GetBytes($dirInfo, 1);
58 39576 100       58355 return undef unless defined $buff;
59 39545         49182 my $val = ord($buff) & 0x7f;
60 39545         49339 $$dirInfo{Bit0} = $val & 0x01;
61 39545         41758 my $mult = 128;
62 39545         41356 my $i = 0;
63 39545         43696 for (;;) {
64 41222 100       60837 last unless ord($buff) & 0x80;
65 8419         11176 $buff = GetBytes($dirInfo, 1);
66 8419 50       12310 return undef unless defined $buff;
67 8419         11293 $val += (ord($buff) & 0x7f) * $mult;
68 8419 100       12446 last unless ord($buff) & 0x80;
69 1678 100       2615 return undef if ++$i > 32; # set a reasonable limit
70 1677         1915 $mult *= 128; # (Note: don't use integer bit shift to avoid integer overflow)
71             }
72 39544         54506 return $val;
73             }
74              
75             #------------------------------------------------------------------------------
76             # Read protobuf record
77             # Inputs: 0) dirInfo ref
78             # Returns: 0) record payload (plus tag id and format type in list context) or undef on error
79             # Notes: Updates $$dirInfo{Pos} to start of next record, and sets $$dirInfo{Bit0}
80             # according to the least significant bit of type 0 (varInt) records
81             sub ReadRecord($)
82             {
83 22842     22842 0 25808 my $dirInfo = shift;
84 22842         29569 my $val = VarInt($dirInfo);
85 22842 100       34027 return undef unless defined $val;
86 22836         26970 my $id = $val >> 3;
87 22836         25691 my $type = $val & 0x07;
88 22836         23969 my $buff;
89              
90 22836 100       42474 if ($type == 0) { # varInt
    100          
    100          
    100          
    100          
    100          
91 12535         15784 $buff = VarInt($dirInfo);
92             } elsif ($type == 1) { # 64-bit number
93 380         510 $buff = GetBytes($dirInfo, 8);
94             } elsif ($type == 2) { # string, bytes or protobuf
95 4153         6145 my $len = VarInt($dirInfo);
96 4153 100       6446 if ($len) {
97 3656         5115 $buff = GetBytes($dirInfo, $len);
98             } else {
99 497         699 $buff = '';
100             }
101             } elsif ($type == 3) { # (deprecated start group)
102 20         46 $buff = '';
103             } elsif ($type == 4) { # (deprecated end group)
104 74         113 $buff = '';
105             } elsif ($type == 5) { # 32-bit number
106 5547         8149 $buff = GetBytes($dirInfo, 4);
107             }
108 22836 100       49482 return wantarray ? ($buff, $id, $type) : $buff;
109             }
110              
111             #------------------------------------------------------------------------------
112             # Check to see if this could be a protobuf object
113             # Inputs: 0) data reference
114             # Retursn: true if this looks like a protobuf
115             sub IsProtobuf($)
116             {
117 1745     1745 0 2214 my $pt = shift;
118 1745         4742 my $dirInfo = { DataPt => $pt, Pos => 0 };
119 1745         2301 for (;;) {
120 11713 100       15572 return 0 unless defined ReadRecord($dirInfo);
121 11506 100       23395 return 1 if $$dirInfo{Pos} == length $$pt;
122             }
123             }
124              
125             #------------------------------------------------------------------------------
126             # Process protobuf data (eg. DJI djmd timed data from Action4 videos) (ref 1)
127             # Inputs: 0) ExifTool ref, 1) dirInfo ref with DataPt, DataPos, DirName and Base,
128             # 2) tag table ptr, 3) prefix of parent protobuf ID's
129             # Returns: true on success
130             sub ProcessProtobuf($$$;$)
131             {
132 1539     1539 0 2646 my ($et, $dirInfo, $tagTbl, $prefix) = @_;
133 1539         2028 my $dataPt = $$dirInfo{DataPt};
134 1539         2256 my $dirName = $$dirInfo{DirName};
135 1539   50     4421 my $dirStart = $$dirInfo{DirStart} || 0;
136 1539   33     4315 my $dirLen = $$dirInfo{DirLen} || (length($$dataPt) - $dirStart);
137 1539         1939 my $dirEnd = $dirStart + $dirLen;
138 1539   50     4543 my $dataPos = ($$dirInfo{Base} || 0) + ($$dirInfo{DataPos} || 0);
      100        
139 1539   33     3814 my $unknown = $et->Options('Unknown') || $et->Options('Verbose');
140              
141 1539   50     3975 $$dirInfo{Pos} = $$dirInfo{DirStart} || 0; # initialize buffer Pos
142 1539         3904 $et->VerboseDir('Protobuf', undef, $dirLen);
143 1539 100       2861 unless ($prefix) {
144 1         2 $prefix = '';
145 1 50       8 $$et{ProtoPrefix}{$dirName} = '' unless defined $$et{ProtoPrefix}{$dirName};
146 1         4 SetByteOrder('II');
147             }
148             # prefix for unknown tags
149 1539 50       3977 my $unkPre = $$tagTbl{TAG_PREFIX} ? $$tagTbl{TAG_PREFIX} . '_' : 'Protobuf ';
150              
151             # loop through protobuf records
152 1539         1990 for (;;) {
153 12668         16740 my $pos = $$dirInfo{Pos};
154 12668 100       18874 last if $pos >= $dirEnd;
155 11129         15034 my ($buff, $id, $type) = ReadRecord($dirInfo);
156 11129 50       17452 defined $buff or $et->Warn('Protobuf format error'), last;
157 11129 50 66     21866 if ($type == 2 and $buff =~ /\.proto$/) {
158             # save protocol name separately for directory type
159 0         0 $$et{ProtoPrefix}{$dirName} = substr($buff, 0, -6) . '_';
160 0         0 $et->HandleTag($tagTbl, Protocol => $buff);
161             }
162 11129         20166 my $tag = "$$et{ProtoPrefix}{$dirName}$prefix$id";
163 11129         16847 my $tagInfo = $$tagTbl{$tag};
164 11129 100       15126 if ($tagInfo) {
165 1630 100 100     3293 next if $type != 2 and $$tagInfo{Unknown} and not $unknown;
      66        
166             } else {
167 9499 100 66     25159 next unless $type == 2 or $unknown;
168 492         1641 $tagInfo = AddTagToTable($tagTbl, $tag, { Unknown => 1 });
169             }
170             # set IsProtobuf flag (only for Unknown tags) if necessary
171 2071 100 100     6863 if ($type == 2 and $$tagInfo{Unknown}) {
172 2027 100 100     6416 if ($$tagInfo{IsProtobuf}) {
    100 100        
173 1175 100       2176 $$tagInfo{IsProtobuf} = 0 unless IsProtobuf(\$buff);
174             } elsif (not defined $$tagInfo{IsProtobuf} and $buff =~ /[^\x20-\x7e]/ and
175             IsProtobuf(\$buff))
176             {
177 372         648 $$tagInfo{IsProtobuf} = 1;
178             }
179 2027 100 66     5090 next unless $$tagInfo{IsProtobuf} or $unknown;
180             }
181             # format binary payload into a useful value
182 1582         1928 my $val;
183 1582 100       5132 if ($$tagInfo{Format}) {
    50          
    50          
    50          
    0          
184 21 100 66     90 if ($type == 0) {
    50          
185 3         7 $val = $buff;
186 3 50 33     25 if ($$tagInfo{Format} eq 'signed') {
    50          
187 0 0       0 if ($val > $intMax) {
188             # use double math (15 decimal digits precision)
189 0 0       0 $val = $$dirInfo{Bit0} ? -int($val / 2) - 1 : $val / 2;
190             } else {
191             # use integer math
192 0 0       0 $val = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
193             }
194             } elsif ($$tagInfo{Format} eq 'int64s' and $val >= $int64sMin) {
195             # hack for DJI drones which store 64-bit signed integers improperly
196             # (just toss upper 32 bits which should be all 1's anyway)
197             # Note: do the two subtractions because $int64sMin + 4294967296
198             # is too large for a 64-bit integer
199 0         0 $val = $val - $int64sMin - 4294967296;
200             }
201             } elsif ($type == 2 and $$tagInfo{Format} eq 'rational') {
202 0         0 my $dir = { DataPt => \$buff, Pos => 0 };
203 0         0 my $num = VarInt($dir);
204 0         0 my $den = VarInt($dir);
205 0 0 0     0 $val = (defined $num and $den) ? $num/$den : 'err';
206             } else {
207 18         91 $val = ReadValue(\$buff, 0, $$tagInfo{Format}, undef, length($buff));
208             }
209             } elsif ($type == 0) { # varInt
210 0         0 $val = $buff;
211 0         0 my $hex = sprintf('%x', $val);
212 0 0       0 if ($val >= $int64sMin) {
213 0         0 my $s64 = $val - $int64sMin - 4294967296;
214 0         0 $val .= " (0x$hex, int64s $s64)";
215             } else {
216 0         0 my $signed;
217 0 0       0 if ($val > $intMax) {
218 0 0       0 $signed = $$dirInfo{Bit0} ? -int($val / 2) - 1 : $val / 2;
219             } else {
220 0 0       0 $signed = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
221             }
222 0         0 $val .= " (0x$hex, signed $signed)";
223             }
224             } elsif ($type == 1) { # 64-bit number
225 0         0 $val = '0x' . unpack('H*', $buff) . ' (double ' . GetDouble(\$buff,0) . ')';
226             } elsif ($type == 2) { # string, bytes or protobuf
227 1561 50       3341 if ($$tagInfo{SubDirectory}) {
    100          
228             # (fall through to process known SubDirectory)
229             } elsif ($$tagInfo{IsProtobuf}) {
230             # process Unknown protobuf directories
231 1538         7706 $et->VPrint(1, "$$et{INDENT}${unkPre}$tag (" . length($buff) . " bytes) -->\n");
232 1538         2662 my $addr = $dataPos + $$dirInfo{Pos} - length($buff);
233 1538         4191 $et->VerboseDump(\$buff, Addr => $addr, Prefix => $$et{INDENT});
234 1538         4445 my %subdir = ( DataPt => \$buff, DataPos => $addr, DirName => $dirName );
235 1538         2630 $$et{INDENT} .= '| ';
236 1538         4521 ProcessProtobuf($et, \%subdir, $tagTbl, "$prefix$id-");
237 1538         3422 $$et{INDENT} = substr($$et{INDENT}, 0, -2);
238 1538         3586 next;
239             } else {
240             # check for rational value (2 varInt values)
241 23         39 my $rat;
242 23         95 my %dir = ( DataPt => \$buff, Pos => 0 );
243 23         87 my $num = VarInt(\%dir);
244 23 50       72 if (defined $num) {
245 23         48 my $denom = VarInt(\%dir);
246 23 50 33     161 $rat = " (rational $num/$denom)" if $denom and $dir{Pos} == length($buff);
247             }
248 23 50       397 if ($buff !~ /[^\r\n\t\x20-\x7e]/) {
    0          
249 23         55 $val = $buff; # assume this is an ASCII string
250             } elsif (length($buff) % 4) {
251 0         0 $val = '0x' . unpack('H*', $buff);
252             } else {
253 0         0 my $n = length($buff) / 4;
254             # (do this instead of '(H8)*' because older Perl version didn't support this)
255 0         0 $val = '0x' . join(' ', unpack("(H8)$n", $buff)); # (group in 4-byte blocks)
256             }
257 23 50       77 $val .= $rat if $rat;
258             }
259             } elsif ($type == 5) { # 32-bit number
260 0         0 $val = '0x' . unpack('H*', $buff) . ' (int32u ' . Get32u(\$buff, 0);
261 0 0       0 $val .= ', int32s ' . Get32s(\$buff, 0) if ord(substr($buff,3,1)) & 0x80;
262 0         0 $val .= ', float ' . GetFloat(\$buff, 0) . ')';
263             } else {
264 0         0 $val = $buff;
265             }
266             # get length of data in the record
267 44 100       137 my $start = $type == 0 ? $pos + 1 : $$dirInfo{Pos} - length $buff;
268             $et->HandleTag($tagTbl, $tag, $val,
269             DataPt => $dataPt,
270             DataPos=> $dataPos,
271             Start => $start,
272             Size => $$dirInfo{Pos} - $start,
273             Extra => ", type=$type",
274             Format => $$tagInfo{Format},
275 44         325 );
276             }
277             # warn if we didn't finish exactly at the end of the buffer
278 1539 50 66     3007 $et->Warn('Truncated protobuf data') unless $prefix or $$dirInfo{Pos} == $dirEnd;
279 1539         2667 return 1;
280             }
281              
282             __END__