#------------------------------------------------------------------------------
# File: Protobuf.pm
#
# Description: Decode protocol buffer data
#
# Revisions: 2024-12-04 - P. Harvey Created
#
# Notes: Tag definitions for Protobuf tags support additional 'signed',
# 'unsigned' and 'int64s' formats for varInt (type 0) values,
# and 'rational' for byte (type 2) values
#
# References: 1) https://protobuf.dev/programming-guides/encoding/
#------------------------------------------------------------------------------
package Image::ExifTool::Protobuf;
use strict;
use vars qw($VERSION);
use Image::ExifTool qw(:DataAccess :Utils);
$VERSION = '1.04';
sub ProcessProtobuf($$$;$);
#------------------------------------------------------------------------------
# Read bytes from dirInfo object
# Inputs: 0) dirInfo ref (with DataPt and Pos set), 1) number of bytes
# Returns: binary data or undef on error
sub GetBytes($$)
{
my ($dirInfo, $n) = @_;
my $dataPt = $$dirInfo{DataPt};
my $pos = $$dirInfo{Pos};
return undef if $pos + $n > length $$dataPt;
$$dirInfo{Pos} += $n;
return substr($$dataPt, $pos, $n);
}
#------------------------------------------------------------------------------
# Read variable-length integer
# Inputs: 0) dirInfo ref
# Returns: integer value
sub VarInt($)
{
my $dirInfo = shift;
my $val = 0;
my $shift = 0;
for (;;) {
my $buff = GetBytes($dirInfo, 1);
defined $buff or return undef;
$val += (ord($buff) & 0x7f) << $shift;
last unless ord($buff) & 0x80;
$shift += 7;
}
return $val;
}
#------------------------------------------------------------------------------
# Read protobuf record
# Inputs: 0) dirInfo ref
# Returns: 0) record payload (plus tag id and format type in list context)
# Notes: Updates dirInfo Pos to start of next record
sub ReadRecord($)
{
my $dirInfo = shift;
my $val = VarInt($dirInfo);
return undef unless defined $val;
my $id = $val >> 3;
my $type = $val & 0x07;
my $buff;
if ($type == 0) { # varInt
$buff = VarInt($dirInfo);
} elsif ($type == 1) { # 64-bit number
$buff = GetBytes($dirInfo, 8);
} elsif ($type == 2) { # string, bytes or protobuf
my $len = VarInt($dirInfo);
if ($len) {
$buff = GetBytes($dirInfo, $len);
} else {
$buff = '';
}
} elsif ($type == 3) { # (deprecated start group)
$buff = '';
} elsif ($type == 4) { # (deprecated end group)
$buff = '';
} elsif ($type == 5) { # 32-bit number
$buff = GetBytes($dirInfo, 4);
}
return wantarray ? ($buff, $id, $type) : $buff;
}
#------------------------------------------------------------------------------
# Check to see if this could be a protobuf object
# Inputs: 0) data reference
# Retursn: true if this looks like a protobuf
sub IsProtobuf($)
{
my $pt = shift;
my $dirInfo = { DataPt => $pt, Pos => 0 };
for (;;) {
return 0 unless defined ReadRecord($dirInfo);
return 1 if $$dirInfo{Pos} == length $$pt;
}
}
#------------------------------------------------------------------------------
# Process protobuf data (eg. DJI djmd timed data from Action4 videos) (ref 1)
# Inputs: 0) ExifTool ref, 1) dirInfo ref with DataPt, DataPos, DirName and Base,
# 2) tag table ptr, 3) prefix of parent protobuf ID's
# Returns: true on success
sub ProcessProtobuf($$$;$)
{
my ($et, $dirInfo, $tagTbl, $prefix) = @_;
my $dataPt = $$dirInfo{DataPt};
my $dirName = $$dirInfo{DirName};
my $dirStart = $$dirInfo{DirStart} || 0;
my $dirLen = $$dirInfo{DirLen} || (length($$dataPt) - $dirStart);
my $dirEnd = $dirStart + $dirLen;
my $dataPos = ($$dirInfo{Base} || 0) + ($$dirInfo{DataPos} || 0);
my $unknown = $et->Options('Unknown') || $et->Options('Verbose');
$$dirInfo{Pos} = $$dirInfo{DirStart} || 0; # initialize buffer Pos
$et->VerboseDir('Protobuf', undef, $dirLen);
unless ($prefix) {
$prefix = '';
$$et{ProtoPrefix}{$dirName} = '' unless defined $$et{ProtoPrefix}{$dirName};
SetByteOrder('II');
}
# loop through protobuf records
for (;;) {
my $pos = $$dirInfo{Pos};
last if $pos >= $dirEnd;
my ($buff, $id, $type) = ReadRecord($dirInfo);
defined $buff or $et->Warn('Protobuf format error'), last;
if ($type == 2 and $buff =~ /\.proto$/) {
# save protocol name separately for directory type
$$et{ProtoPrefix}{$dirName} = substr($buff, 0, -6) . '_';
$et->HandleTag($tagTbl, Protocol => $buff);
}
my $tag = "$$et{ProtoPrefix}{$dirName}$prefix$id";
my $tagInfo = $$tagTbl{$tag};
if ($tagInfo) {
next if $type != 2 and $$tagInfo{Unknown} and not $unknown;
} else {
next unless $type == 2 or $unknown;
$tagInfo = AddTagToTable($tagTbl, $tag, { Unknown => 1 });
}
# set IsProtobuf flag (only for Unknown tags) if necessary
if ($type == 2 and $$tagInfo{Unknown}) {
if ($$tagInfo{IsProtobuf}) {
$$tagInfo{IsProtobuf} = 0 unless IsProtobuf(\$buff);
} elsif (not defined $$tagInfo{IsProtobuf} and $buff =~ /[^\x20-\x7e]/ and
IsProtobuf(\$buff))
{
$$tagInfo{IsProtobuf} = 1;
}
next unless $$tagInfo{IsProtobuf} or $unknown;
}
# format binary payload into a useful value
my $val;
if ($$tagInfo{Format}) {
if ($type == 0) {
$val = $buff;
if ($$tagInfo{Format} eq 'signed') {
$val = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
} elsif ($$tagInfo{Format} eq 'int64s' and $val > 0xffffffff) {
# hack for DJI drones which store 64-bit signed integers improperly
# (just toss upper 32 bits which should be all 1's anyway)
$val = ($val & 0xffffffff) - 4294967296;
}
} elsif ($type == 2 and $$tagInfo{Format} eq 'rational') {
my $dir = { DataPt => \$buff, Pos => 0 };
my $num = VarInt($dir);
my $den = VarInt($dir);
$val = (defined $num and $den) ? $num/$den : 'err';
} else {
$val = ReadValue(\$buff, 0, $$tagInfo{Format}, undef, length($buff));
}
} elsif ($type == 0) { # varInt
$val = $buff;
my $hex = sprintf('%x', $val);
if (length($hex) == 16 and $hex =~ /^ffffffff/) {
my $s64 = hex(substr($hex, 8)) - 4294967296;
$val .= " (0x$hex, int64s $s64)";
} else {
my $signed = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
$val .= " (0x$hex, signed $signed)";
}
} elsif ($type == 1) { # 64-bit number
$val = '0x' . unpack('H*', $buff) . ' (double ' . GetDouble(\$buff,0) . ')';
} elsif ($type == 2) { # string, bytes or protobuf
if ($$tagInfo{SubDirectory}) {
# (fall through to process known SubDirectory)
} elsif ($$tagInfo{IsProtobuf}) {
# process Unknown protobuf directories
$et->VPrint(1, "$$et{INDENT}Protobuf $tag (" . length($buff) . " bytes) -->\n");
my $addr = $dataPos + $$dirInfo{Pos} - length($buff);
$et->VerboseDump(\$buff, Addr => $addr, Prefix => $$et{INDENT});
my %subdir = ( DataPt => \$buff, DataPos => $addr, DirName => $dirName );
$$et{INDENT} .= '| ';
ProcessProtobuf($et, \%subdir, $tagTbl, "$prefix$id-");
$$et{INDENT} = substr($$et{INDENT}, 0, -2);
next;
} else {
# check for rational value (2 varInt values)
my $rat;
my %dir = ( DataPt => \$buff, Pos => 0 );
my $num = VarInt(\%dir);
if (defined $num) {
my $denom = VarInt(\%dir);
$rat = " (rational $num/$denom)" if $denom and $dir{Pos} == length($buff);
}
if ($buff !~ /[^\x20-\x7e]/) {
$val = $buff; # assume this is an ASCII string
} elsif (length($buff) % 4) {
$val = '0x' . unpack('H*', $buff);
} else {
my $n = length($buff) / 4;
# (do this instead of '(H8)*' because older Perl version didn't support this)
$val = '0x' . join(' ', unpack("(H8)$n", $buff)); # (group in 4-byte blocks)
}
$val .= $rat if $rat;
}
} elsif ($type == 5) { # 32-bit number
$val = '0x' . unpack('H*', $buff) . ' (int32u ' . Get32u(\$buff, 0);
$val .= ', int32s ' . Get32s(\$buff, 0) if ord(substr($buff,3,1)) & 0x80;
$val .= ', float ' . GetFloat(\$buff, 0) . ')';
} else {
$val = $buff;
}
# get length of data in the record
my $start = $type == 0 ? $pos + 1 : $$dirInfo{Pos} - length $buff;
$et->HandleTag($tagTbl, $tag, $val,
DataPt => $dataPt,
DataPos=> $dataPos,
Start => $start,
Size => $$dirInfo{Pos} - $start,
Extra => ", type=$type",
Format => $$tagInfo{Format},
);
}
# warn if we didn't finish exactly at the end of the buffer
$et->Warn('Truncated protobuf data') unless $prefix or $$dirInfo{Pos} == $dirEnd;
return 1;
}
__END__
=head1 NAME
Image::ExifTool::Protobuf - Decode protocol buffer information
=head1 SYNOPSIS
This module is loaded automatically by Image::ExifTool when required.
=head1 DESCRIPTION
This module contains definitions required by Image::ExifTool to decode
information in protocol buffer (protobuf) format.
=head1 AUTHOR
Copyright 2003-2025, Phil Harvey (philharvey66 at gmail.com)
This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=head1 REFERENCES
=over 4
=item L
=back
=head1 SEE ALSO
L
=cut