#------------------------------------------------------------------------------
# File: Protobuf.pm
#
# Description: Decode protocol buffer data
#
# Revisions: 2024-12-04 - P. Harvey Created
#
# Notes: Tag definitions for Protobuf tags support additional 'signed'
# and 'unsigned' formats for varInt (type 0) 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.00';
sub ProcessProtobuf($$$;$);
#------------------------------------------------------------------------------
# Read bytes from dirInfo object
# Inputs: 0) dirInfo ref, 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, 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 $unknown = $et->Options('Unknown') || $et->Options('Verbose');
$$dirInfo{Pos} = $$dirInfo{DirStart} || 0; # initialize buffer Pos
unless ($prefix) {
$prefix = '';
$$et{ProtocolName}{$dirName} = '*' unless defined $$et{ProtocolName}{$dirName};
SetByteOrder('II');
}
# loop through protobuf records
for (;;) {
my $pos = $$dirInfo{Pos};
last if $pos >= length $$dataPt;
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{ProtocolName}{$dirName} = substr($buff, 0, -6);
$et->HandleTag($tagTbl, Protocol => $buff);
}
my $tag = "$$et{ProtocolName}{$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;
$val = ($val & 1) ? -($val >> 1)-1 : ($val >> 1) if $$tagInfo{Format} eq 'signed';
} else {
$val = ReadValue(\$buff, 0, $$tagInfo{Format}, undef, length($buff));
}
} elsif ($type == 0) {
$val = $buff;
my $signed = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
$val .= sprintf(" (0x%x, signed $signed)", $val);
} elsif ($type == 1) {
$val = '0x' . unpack('H*', $buff) . ' (double ' . GetDouble(\$buff,0) . ')';
} elsif ($type == 2) {
if ($$tagInfo{IsProtobuf}) {
$et->VPrint(1, "+ Protobuf $tag (" . length($buff) . " bytes)\n");
my $addr = $$dirInfo{Base} + $$dirInfo{Pos} - length($buff);
$et->VerboseDump(\$buff, Addr => $addr);
my %subdir = ( DataPt => \$buff, Base => $addr, DirName => $dirName );
ProcessProtobuf($et, \%subdir, $tagTbl, "$prefix$id-");
next;
} elsif ($buff !~ /[^\x20-\x7e]/) {
$val = $buff; # assume this is an ASCII string
} elsif (length($buff) % 4) {
$val = '0x' . unpack('H*', $buff);
} else {
$val = '0x' . join(' ', unpack('(H8)*', $buff)); # (group in 4-byte blocks)
}
} elsif ($type == 5) {
$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=> $$dirInfo{Base},
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} == length $$dataPt;
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-2024, 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