File Coverage

blib/lib/Image/ExifTool/Validate.pm
Criterion Covered Total %
statement 102 193 52.8
branch 54 168 32.1
condition 59 181 32.6
subroutine 9 10 90.0
pod 0 6 0.0
total 224 558 40.1


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Validate.pm
3             #
4             # Description: Additional metadata validation
5             #
6             # Created: 2017/01/18 - P. Harvey
7             #
8             # Notes: My apologies for the convoluted logic contained herein, but it
9             # is done this way to retro-fit the Validate feature into the
10             # existing ExifTool code while reducing the possibility of
11             # introducing bugs or slowing down processing when this feature
12             # is not used.
13             #------------------------------------------------------------------------------
14              
15             package Image::ExifTool::Validate;
16              
17 1     1   5 use strict;
  1         3  
  1         41  
18 1     1   4 use vars qw($VERSION %exifSpec);
  1         2  
  1         60  
19              
20             $VERSION = '1.26';
21              
22 1     1   4 use Image::ExifTool qw(:Utils);
  1         2  
  1         133  
23 1     1   4 use Image::ExifTool::Exif;
  1         2  
  1         3593  
24              
25             # EXIF table tag ID's which are part of the EXIF 2.32 specification
26             # (with ExifVersion numbers for tags where I can determine the version)
27             # (also used by BuildTagLookup to add underlines in HTML version of EXIF Tag Table)
28             %exifSpec = (
29             0x1 => 210,
30             0x100 => 1, 0x8298 => 1, 0x9207 => 1, 0xa217 => 1,
31             0x101 => 1, 0x829a => 1, 0x9208 => 1, 0xa300 => 1,
32             0x102 => 1, 0x829d => 1, 0x9209 => 1, 0xa301 => 1,
33             0x103 => 1, 0x8769 => 1, 0x920a => 1, 0xa302 => 1,
34             0x106 => 1, 0x8822 => 1, 0x9214 => 220, 0xa401 => 220,
35             0x10e => 1, 0x8824 => 1, 0x927c => 1, 0xa402 => 220,
36             0x10f => 1, 0x8825 => 200, 0x9286 => 1, 0xa403 => 220,
37             0x110 => 1, 0x8827 => 1, 0x9290 => 1, 0xa404 => 220,
38             0x111 => 1, 0x8828 => 1, 0x9291 => 1, 0xa405 => 220,
39             0x112 => 1, 0x8830 => 230, 0x9292 => 1, 0xa406 => 220,
40             0x115 => 1, 0x8831 => 230, 0x9400 => 231, 0xa407 => 220,
41             0x116 => 1, 0x8832 => 230, 0x9401 => 231, 0xa408 => 220,
42             0x117 => 1, 0x8833 => 230, 0x9402 => 231, 0xa409 => 220,
43             0x11a => 1, 0x8834 => 230, 0x9403 => 231, 0xa40a => 220,
44             0x11b => 1, 0x8835 => 230, 0x9404 => 231, 0xa40b => 220,
45             0x11c => 1, 0x9000 => 1, 0x9405 => 231, 0xa40c => 220,
46             0x128 => 1, 0x9003 => 1, 0xa000 => 1, 0xa460 => 232,
47             0x12d => 1, 0x9004 => 1, 0xa001 => 1, 0xa461 => 232,
48             0x131 => 1, 0x9010 => 231, 0xa002 => 1, 0xa462 => 232,
49             0x132 => 1, 0x9011 => 231, 0xa003 => 1, 0xa420 => 220,
50             0x13b => 1, 0x9012 => 231, 0xa004 => 1, 0xa430 => 230,
51             0x13e => 1, 0x9101 => 1, 0xa005 => 210, 0xa431 => 230,
52             0x13f => 1, 0x9102 => 1, 0xa20b => 1, 0xa432 => 230,
53             0x201 => 1, 0x9201 => 1, 0xa20c => 1, 0xa433 => 230,
54             0x202 => 1, 0x9202 => 1, 0xa20e => 1, 0xa434 => 230,
55             0x211 => 1, 0x9203 => 1, 0xa20f => 1, 0xa435 => 230,
56             0x212 => 1, 0x9204 => 1, 0xa210 => 1, 0xa500 => 221,
57             0x213 => 1, 0x9205 => 1, 0xa214 => 1,
58             0x214 => 1, 0x9206 => 1, 0xa215 => 1,
59              
60             # new Exif 3.0 tags
61             0xa436 => 300,
62             0xa437 => 300,
63             0xa438 => 300,
64             0xa439 => 300,
65             0xa43a => 300,
66             0xa43b => 300,
67             0xa43c => 300,
68             );
69              
70             # GPSVersionID numbers when each tag was introduced
71             my %gpsVer = (
72             0x01 => 2000, 0x09 => 2000, 0x11 => 2000, 0x19 => 2000,
73             0x02 => 2000, 0x0a => 2000, 0x12 => 2000, 0x1a => 2000,
74             0x03 => 2000, 0x0b => 2000, 0x13 => 2000, 0x1b => 2200,
75             0x04 => 2000, 0x0c => 2000, 0x14 => 2000, 0x1c => 2200,
76             0x05 => 2000, 0x0d => 2000, 0x15 => 2000, 0x1d => 2200,
77             0x06 => 2000, 0x0e => 2000, 0x16 => 2000, 0x1e => 2200,
78             0x07 => 2000, 0x0f => 2000, 0x17 => 2000, 0x1f => 2300,
79             0x08 => 2000, 0x10 => 2000, 0x18 => 2000,
80             );
81              
82             # lookup to check version numbers
83             my %verCheck = (
84             ExifIFD => { ExifVersion => \%exifSpec },
85             InteropIFD => { ExifVersion => \%exifSpec },
86             GPS => { GPSVersionID => \%gpsVer },
87             );
88              
89             # tags standard in various RAW file formats or IFD's
90             my %otherSpec = (
91             CR2 => { 0xc5d8 => 1, 0xc5d9 => 1, 0xc5e0 => 1, 0xc640 => 1, 0xc6dc => 1, 0xc6dd => 1 },
92             NEF => { 0x9216 => 1, 0x9217 => 1 },
93             DNG => { 0x882a => 1, 0x9211 => 1, 0x9216 => 1 },
94             ARW => { 0x7000 => 1, 0x7001 => 1, 0x7010 => 1, 0x7011 => 1, 0x7020 => 1, 0x7031 => 1,
95             0x7032 => 1, 0x7034 => 1, 0x7035 => 1, 0x7036 => 1, 0x7037 => 1, 0x7038 => 1,
96             0x7310 => 1, 0x7313 => 1, 0x7316 => 1, 0x74c7 => 1, 0x74c8 => 1, 0xa500 => 1 },
97             RW2 => { All => 1 }, # ignore all unknown tags in RW2
98             RWL => { All => 1 },
99             RAF => { All => 1 }, # (temporary)
100             DCR => { All => 1 },
101             KDC => { All => 1 },
102             JXR => { All => 1 },
103             SRW => { 0xa010 => 1, 0xa011 => 1, 0xa101 => 1, 0xa102 => 1 },
104             NRW => { 0x9216 => 1, 0x9217 => 1 },
105             X3F => { 0xa500 => 1 },
106             CameraIFD => { All => 1 }, # (exists in JPG and DNG of Leica Q3 images)
107             );
108              
109             # standard format for tags (not necessary for exifSpec or GPS tags where Writable is defined)
110             my %stdFormat = (
111             ExifIFD => {
112             0xa002 => 'int(16|32)u',
113             0xa003 => 'int(16|32)u',
114             },
115             InteropIFD => {
116             0x01 => 'string',
117             0x02 => 'undef',
118             0x1000 => 'string',
119             0x1001 => 'int(16|32)u',
120             0x1002 => 'int(16|32)u',
121             },
122             IFD => {
123             # TIFF, EXIF, XMP, IPTC, ICC_Profile and PrintIM standard tags:
124             0xfe => 'int32u', 0x11f => 'rational64u', 0x14a => 'int32u', 0x205 => 'int16u',
125             0xff => 'int16u', 0x120 => 'int32u', 0x14c => 'int16u', 0x206 => 'int16u',
126             0x100 => 'int(16|32)u', 0x121 => 'int32u', 0x14d => 'string', 0x207 => 'int32u',
127             0x101 => 'int(16|32)u', 0x122 => 'int16u', 0x14e => 'int16u', 0x208 => 'int32u',
128             0x107 => 'int16u', 0x123 => 'int16u', 0x150 => 'int(8|16)u', 0x209 => 'int32u',
129             0x108 => 'int16u', 0x124 => 'int32u', 0x151 => 'string', 0x211 => 'rational64u',
130             0x109 => 'int16u', 0x125 => 'int32u', 0x152 => 'int16u', 0x212 => 'int16u',
131             0x10a => 'int16u', 0x129 => 'int16u', 0x153 => 'int16u', 0x213 => 'int16u',
132             0x10d => 'string', 0x13c => 'string', 0x154 => '.*', 0x214 => 'rational64u',
133             0x111 => 'int(16|32)u', 0x13d => 'int16u', 0x155 => '.*', 0x2bc => 'int8u',
134             0x116 => 'int(16|32)u', 0x140 => 'int16u', 0x156 => 'int16u', 0x828d => 'int16u',
135             0x117 => 'int(16|32)u', 0x141 => 'int16u', 0x15b => 'undef', 0x828e => 'int8u',
136             0x118 => 'int16u', 0x142 => 'int(16|32)u', 0x200 => 'int16u', 0x83bb => 'int32u',
137             0x119 => 'int16u', 0x143 => 'int(16|32)u', 0x201 => 'int32u', 0x8649 => 'int8u',
138             0x11d => 'string', 0x144 => 'int32u', 0x202 => 'int32u', 0x8773 => 'undef',
139             0x11e => 'rational64u', 0x145 => 'int(16|32)u', 0x203 => 'int16u', 0xc4a5 => 'undef',
140             # Windows Explorer tags:
141             0x9c9b => 'int8u', 0x9c9d => 'int8u', 0x9c9f => 'int8u',
142             0x9c9c => 'int8u', 0x9c9e => 'int8u',
143             # GeoTiff tags:
144             0x830e => 'double', 0x8482 => 'double', 0x87af => 'int16u', 0x87b1 => 'string',
145             0x8480 => 'double', 0x85d8 => 'double', 0x87b0 => 'double',
146             # DNG tags: (use '' for non-DNG tags in the range 0xc612-0xcd48)
147             0xc615 => '(string|int8u)', 0xc6f4 => '(string|int8u)', 0xcd49 => 'float',
148             0xc61a => '(int16u|int32u|rational64u)', 0xc6f6 => '(string|int8u)', 0xcd4a => 'int32u',
149             0xc61d => 'int(16|32)u', 0xc6f8 => '(string|int8u)', 0xcd4b => 'int32u',
150             0xc61f => '(int16u|int32u|rational64u)', 0xc6fe => '(string|int8u)',
151             0xc620 => '(int16u|int32u|rational64u)', 0xc716 => '(string|int8u)',
152             0xc628 => '(int16u|rational64u)', 0xc717 => '(string|int8u)',
153             0xc634 => 'int8u', 0xc718 => '(string|int8u)',
154             0xc640 => '', 0xc71e => 'int(16|32)u',
155             0xc660 => '', 0xc71f => 'int(16|32)u',
156             0xc68b => '(string|int8u)', 0xc791 => 'int(16|32)u',
157             0xc68d => 'int(16|32)u', 0xc792 => 'int(16|32)u',
158             0xc68e => 'int(16|32)u', 0xc793 => '(int16u|int32u|rational64u)',
159             0xc6d2 => '', 0xcd43 => 'int(16|32)u',
160             0xc6d3 => '', 0xcd48 => '(string|int8u)',
161              
162             # Exif 3.0 spec
163             0x10e => 'string|utf8', 0xa430 => 'string|utf8', 0xa439 => 'string|utf8',
164             0x10f => 'string|utf8', 0xa433 => 'string|utf8', 0xa43a => 'string|utf8',
165             0x110 => 'string|utf8', 0xa434 => 'string|utf8', 0xa43b => 'string|utf8',
166             0x131 => 'string|utf8', 0xa436 => 'string|utf8', 0xa43c => 'string|utf8',
167             0x13b => 'string|utf8', 0xa437 => 'string|utf8', 0xa43a => 'string|utf8',
168             0x8298 => 'string|utf8', 0xa438 => 'string|utf8',
169             },
170             );
171              
172             # generate lookup for any IFD
173             my %stdFormatAnyIFD = map { %{$stdFormat{$_}} } keys %stdFormat;
174              
175             # tag values to validate based on file type (from EXIF specification)
176             # - validation code may access $val and %val, and returns 1 on success,
177             # or error message otherwise ('' for a generic message)
178             # - entry is undef if tag must not exist (same as 'not defined $val' in code)
179             my %validValue = (
180             JPEG => {
181             IFD0 => {
182             0x100 => undef, # ImageWidth
183             0x101 => undef, # ImageLength
184             0x102 => undef, # BitsPerSample
185             0x103 => undef, # Compression
186             0x106 => undef, # PhotometricInterpretation
187             0x111 => undef, # StripOffsets
188             0x115 => undef, # SamplesPerPixel
189             0x116 => undef, # RowsPerStrip
190             0x117 => undef, # StripByteCounts
191             # (optional as of 3.0) 0x11a => 'defined $val', # XResolution
192             # (optional as of 3.0) 0x11b => 'defined $val', # YResolution
193             0x11c => undef, # PlanarConfiguration
194             # (optional as of 3.0) 0x128 => '$val =~ /^[123]$/', # ResolutionUnit
195             0x201 => undef, # JPEGInterchangeFormat
196             0x202 => undef, # JPEGInterchangeFormatLength
197             0x212 => undef, # YCbCrSubSampling
198             0x213 => '$val =~ /^[12]$/', # YCbCrPositioning
199             },
200             IFD1 => {
201             0x100 => undef, # ImageWidth
202             0x101 => undef, # ImageLength
203             0x102 => undef, # BitsPerSample
204             0x103 => '$val == 6', # Compression
205             0x106 => undef, # PhotometricInterpretation
206             0x111 => undef, # StripOffsets
207             0x115 => undef, # SamplesPerPixel
208             0x116 => undef, # RowsPerStrip
209             0x117 => undef, # StripByteCounts
210             0x11a => 'defined $val', # XResolution
211             0x11b => 'defined $val', # YResolution
212             0x11c => undef, # PlanarConfiguration
213             0x128 => '$val =~ /^[123]$/', # ResolutionUnit
214             0x201 => 'defined $val', # JPEGInterchangeFormat
215             0x202 => 'defined $val', # JPEGInterchangeFormatLength
216             0x212 => undef, # YCbCrSubSampling
217             },
218             ExifIFD => {
219             0x9000 => 'defined $val and $val =~ /^\d{4}$/', # ExifVersion
220             0x9101 => 'defined $val', # ComponentsConfiguration
221             # (optional as of 3.0) 0xa000 => 'defined $val', # FlashpixVersion
222             0xa001 => '$val == 1 or $val == 0xffff', # ColorSpace
223             0xa002 => 'defined $val', # PixelXDimension
224             0xa003 => 'defined $val', # PixelYDimension
225             },
226             GPS => {
227             0x00 => 'defined $val and $val =~ /^\d \d \d \d$/', # GPSVersionID
228             0x1b => 'not defined $val or $val =~ /^(GPS|CELLID|WLAN|MANUAL)$/', # GPSProcessingMethod
229             },
230             InteropIFD => { }, # (needed for ExifVersion check)
231             },
232             TIFF => {
233             IFD0 => {
234             0x100 => 'defined $val', # ImageWidth
235             0x101 => 'defined $val', # ImageLength
236             # (default is 1) 0x102 => 'defined $val', # BitsPerSample
237             0x103 => q{
238             not defined $val or $val =~ /^(1|5|6|32773)$/ or
239             ($val == 2 and (not defined $val{0x102} or $val{0x102} == 1));
240             }, # Compression
241             0x106 => '$val =~ /^[0123]$/', # PhotometricInterpretation
242             0x111 => 'defined $val', # StripOffsets
243             # SamplesPerPixel
244             0x115 => q{
245             my $pi = $val{0x106} || 0;
246             my $xtra = ($val{0x152} ? scalar(split ' ', $val{0x152}) : 0);
247             if ($pi == 2 or $pi == 6) {
248             return $val == 3 + $xtra;
249             } elsif ($pi == 5) {
250             return $val == 4 + $xtra;
251             } else {
252             return 1;
253             }
254             },
255             0x116 => 'defined $val', # RowsPerStrip
256             0x117 => 'defined $val', # StripByteCounts
257             0x11a => 'defined $val', # XResolution
258             0x11b => 'defined $val', # YResolution
259             0x128 => 'not defined $val or $val =~ /^[123]$/', # ResolutionUnit
260             # ColorMap (must be palette image with correct number of colors)
261             0x140 => q{
262             return '' if defined $val{0x106} and $val{0x106} == 3 xor defined $val;
263             return 1 if not defined $val or length($val) == 6 * 2 ** ($val{0x102} || 0);
264             return 'Invalid count for';
265             },
266             0x201 => undef, # JPEGInterchangeFormat
267             0x202 => undef, # JPEGInterchangeFormatLength
268             },
269             ExifIFD => {
270             0x9000 => 'defined $val', # ExifVersion
271             0x9101 => undef, # ComponentsConfiguration
272             0x9102 => undef, # CompressedBitsPerPixel
273             0xa000 => 'defined $val', # FlashpixVersion
274             0xa001 => '$val == 1 or $val == 0xffff', # ColorSpace
275             0xa002 => undef, # PixelXDimension
276             0xa003 => undef, # PixelYDimension
277             },
278             InteropIFD => {
279             0x0001 => undef, # InteropIndex
280             },
281             GPS => {
282             0x00 => 'defined $val and $val =~ /^\d \d \d \d$/', # GPSVersionID
283             0x1b => '$val =~ /^(GPS|CELLID|WLAN|MANUAL)$/', # GPSProcessingMethod
284             },
285             },
286             );
287              
288             # validity ranges for constrained date/time fields
289             my @validDateField = (
290             [ 'Month', 1, 12 ],
291             [ 'Day', 1, 31 ],
292             [ 'Hour', 0, 23 ],
293             [ 'Minutes', 0, 59 ],
294             [ 'Seconds', 0, 59 ],
295             [ 'TZhr', 0, 14 ],
296             [ 'TZmin', 0, 59 ],
297             );
298              
299             # "Validate" tag information
300             my %validateInfo = (
301             Groups => { 0 => 'ExifTool', 1 => 'ExifTool', 2 => 'ExifTool' },
302             Notes => q{
303             generated only if specifically requested. Requesting this tag automatically
304             enables the API L option, imposing
305             additional validation checks when extracting metadata. Returns the number
306             of errors, warnings and minor warnings encountered. Note that the Validate
307             feature focuses mainly on validation of EXIF/TIFF metadata
308             },
309             PrintConv => {
310             '0 0 0' => 'OK',
311             OTHER => sub {
312             my @val = split ' ', shift;
313             my @rtn;
314             push @rtn, sprintf('%d Error%s', $val[0], $val[0] == 1 ? '' : 's') if $val[0];
315             push @rtn, sprintf('%d Warning%s', $val[1], $val[1] == 1 ? '' : 's') if $val[1];
316             if ($val[2]) {
317             my $str = ($val[1] == $val[2] ? ($val[1] == 1 ? '' : 'all ') : "$val[2] ");
318             $rtn[-1] .= " (${str}minor)";
319             }
320             return join(' and ', @rtn);
321             },
322             },
323             Hidden => 0,
324             );
325              
326             # add "Validate" tag to Extra table
327             AddTagToTable(\%Image::ExifTool::Extra, Validate => \%validateInfo, 1);
328              
329             #------------------------------------------------------------------------------
330             # Validate the raw value of a tag
331             # Inputs: 0) ExifTool ref, 1) tag key, 2) raw tag value
332             # Returns: nothing, but issues a minor Warning if a problem was detected
333             sub ValidateRaw($$$)
334             {
335 213     213 0 488 my ($self, $tag, $val) = @_;
336 213         405 my $tagInfo = $$self{TAG_INFO}{$tag};
337 213         356 my $wrn;
338              
339             # evaluate Validate code if specified
340 213 100       488 if ($$tagInfo{Validate}) {
341 3         12 local $SIG{'__WARN__'} = \&Image::ExifTool::SetWarning;
342 3         5 undef $Image::ExifTool::evalWarning;
343             #### eval Validate ($self, $val, $tagInfo)
344 3         178 my $wrn = eval $$tagInfo{Validate};
345 3   33     14 my $err = $Image::ExifTool::evalWarning || $@;
346 3 50 33     17 if ($wrn or $err) {
347 0         0 my $name = $$tagInfo{Table}{GROUPS}{0} . ':' . Image::ExifTool::GetTagName($tag);
348 0 0       0 $self->Warn("Validate $name: $err", 1) if $err;
349 0 0       0 $self->Warn("$wrn for $name", 1) if $wrn;
350             }
351             }
352             # check for unknown values in PrintConv lookup for all standard EXIF tags
353 213 100 66     1044 if (ref $$tagInfo{PrintConv} eq 'HASH' and ($$tagInfo{Table}{SHORT_NAME} eq 'GPS::Main' or
      100        
354             ($$tagInfo{Table} eq \%Image::ExifTool::Exif::Main and $exifSpec{$$tagInfo{TagID}})))
355             {
356 18         87 my $prt = $self->GetValue($tag, 'PrintConv');
357 18 50 33     106 $wrn = 'Unknown value for' if $prt and $prt =~ /^Unknown \(/;
358             }
359 213 50       483 $wrn = 'Undefined value for' if $val eq 'undef';
360 213 50       595 if ($wrn) {
361 0         0 my $name = $$self{DIR_NAME} . ':' . Image::ExifTool::GetTagName($tag);
362 0         0 $self->Warn("$wrn $name", 1);
363             }
364             }
365              
366             #------------------------------------------------------------------------------
367             # Validate raw EXIF date/time value
368             # Inputs: 0) date/time value
369             # Returns: error string
370             sub ValidateExifDate($)
371             {
372 3     3 0 5 my $val = shift;
373 3 50 0     15 if ($val =~ /^\d{4}:(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/) {
    0          
374 3         17 my @a = ($1,$2,$3,$4,$5);
375 3         5 my ($i, @bad);
376 3         8 for ($i=0; $i<@a; ++$i) {
377 15 50 33     64 next if $a[$i] eq ' ' or ($a[$i] >= $validDateField[$i][1] and $a[$i] <= $validDateField[$i][2]);
      33        
378 0         0 push @bad, $validDateField[$i][0];
379             }
380 3 50       9 return join('+', @bad) . ' out of range' if @bad;
381             # the EXIF specification allows blank fields or an entire blank value
382             } elsif ($val ne ' : : : : ' and $val ne ' ') {
383 0         0 return 'Invalid date/time format';
384             }
385 3         21 return undef; # OK!
386             }
387              
388             #------------------------------------------------------------------------------
389             # Validate EXIF-reformatted XMP date/time value
390             # Inputs: 0) date/time value
391             # Returns: error string
392             sub ValidateXMPDate($)
393             {
394 0     0 0 0 my $val = shift;
395 0 0 0     0 if ($val =~ /^\d{4}$/ or
      0        
      0        
      0        
      0        
396             $val =~ /^\d{4}:(\d{2})$/ or
397             $val =~ /^\d{4}:(\d{2}):(\d{2})$/ or
398             $val =~ /^\d{4}:(\d{2}):(\d{2}) (\d{2}):(\d{2})()(Z|[-+](\d{2}):(\d{2}))?$/ or
399             $val =~ /^\d{4}:(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})(Z|[-+](\d{2}):(\d{2}))?$/ or
400             $val =~ /^\d{4}:(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})\.?\d*(Z|[-+](\d{2}):(\d{2}))?$/)
401             {
402 0         0 my @a = ($1,$2,$3,$4,$5,$7,$8);
403 0         0 my ($i, @bad);
404 0         0 for ($i=0; $i<@a; ++$i) {
405 0 0       0 last unless defined $a[$i];
406 0 0 0     0 next if $a[$i] eq '' or ($a[$i] >= $validDateField[$i][1] and $a[$i] <= $validDateField[$i][2]);
      0        
407 0         0 push @bad, $validDateField[$i][0];
408             }
409 0 0       0 return join('+', @bad) . ' out of range' if @bad;
410             } else {
411 0         0 return 'Invalid date/time format';
412             }
413 0         0 return undef; # OK!
414             }
415              
416             #------------------------------------------------------------------------------
417             # Validate EXIF tag
418             # Inputs: 0) ExifTool ref, 1) tag table ref, 2) tag ID, 3) tagInfo ref,
419             # 4) previous tag ID, 5) IFD name, 6) number of values, 7) value format string
420             # Returns: Nothing, but sets Warning tags if any problems are found
421             sub ValidateExif($$$$$$$$)
422             {
423 63     63 0 271 my ($et, $tagTablePtr, $tag, $tagInfo, $lastTag, $ifd, $count, $formatStr) = @_;
424              
425 63 50       163 $et->Warn("Entries in $ifd are out of order") if $tag <= $lastTag;
426              
427             # (get tagInfo for unknown tags if Unknown option not used)
428 63 50 66     186 if (not defined $tagInfo and $$tagTablePtr{$tag} and ref $$tagTablePtr{$tag} eq 'HASH') {
      33        
429 0         0 $tagInfo = $$tagTablePtr{$tag};
430             }
431 63 100 33     205 if (defined $tagInfo) {
    50          
432 61   66     147 my $ti = $tagInfo || $$tagTablePtr{$tag};
433 61 100       137 $ti = $$ti[-1] if ref $ti eq 'ARRAY';
434 61   66     227 my $stdFmt = $stdFormat{$ifd} || $stdFormat{IFD};
435 61 100 66     761 if (defined $$stdFmt{All} or ($tagTablePtr eq \%Image::ExifTool::Exif::Main and
    50 33        
    50 33        
      33        
      33        
436             ($exifSpec{$tag} or $$stdFmt{$tag} or
437             ($tag >= 0xc612 and $tag <= 0xcd48 and not defined $$stdFmt{$tag}))) or # (DNG tags)
438             $$tagTablePtr{SHORT_NAME} eq 'GPS::Main')
439             {
440 59   66     250 my $wgp = $$ti{WriteGroup} || $$tagTablePtr{WRITE_GROUP};
441 59 0 66     429 if ($wgp and $wgp ne $ifd and $wgp ne 'All' and not $$ti{OffsetPair} and
      66        
      100        
      25        
      66        
      0        
      33        
      0        
442             ($ifd =~ /^(Sub|Profile)?IFD\d*$/ xor $wgp =~ /^(Sub)?IFD\d*$/) and
443             ($$ti{Writable} or $$ti{WriteGroup}) and $ifd !~ /^SRF\d+$/)
444             {
445 0         0 $et->Warn(sprintf('Wrong IFD for 0x%.4x %s (should be %s not %s)', $tag, $$ti{Name}, $wgp, $ifd));
446             }
447 59   100     302 my $fmt = $$stdFmt{$tag} || $$ti{Writable};
448 59 0 66     1155 if ($fmt and $formatStr !~ /^$fmt$/ and (not $tagInfo or
      0        
      33        
449             not $$tagInfo{IsOffset} or $Image::ExifTool::Exif::intFormat{$formatStr}))
450             {
451 0         0 $et->Warn(sprintf('Non-standard format (%s) for %s 0x%.4x %s', $formatStr, $ifd, $tag, $$ti{Name}))
452             }
453             } elsif ($stdFormatAnyIFD{$tag}) {
454 0 0 0     0 if ($$ti{Writable} || $$ti{WriteGroup}) {
455 0   0     0 my $wgp = $$ti{WriteGroup} || $$tagTablePtr{WRITE_GROUP};
456 0 0 0     0 if ($wgp and $wgp ne $ifd) {
457 0         0 $et->Warn(sprintf('Wrong IFD for 0x%.4x %s (should be %s not %s)', $tag, $$ti{Name}, $wgp, $ifd));
458             }
459             }
460             } elsif (not $otherSpec{$$et{FileType}} or
461             (not $otherSpec{$$et{FileType}}{$tag} and not $otherSpec{$$et{FileType}}{All}))
462             {
463 0 0 0     0 if ($tagTablePtr eq \%Image::ExifTool::Exif::Main or $$ti{Unknown}) {
464 0 0       0 $et->Warn(sprintf('Non-standard %s tag 0x%.4x %s', $ifd, $tag, $$ti{Name}), 1) unless $otherSpec{$ifd};
465             }
466             }
467             # change expected count from read Format to Writable size
468 61         202 my $tiCount = $$ti{Count};
469 61 100       231 if ($tiCount) {
470 4 50 66     23 if ($$ti{Format} and $$ti{Writable} and
      33        
      33        
471             $Image::ExifTool::Exif::formatNumber{$$ti{Format}} and
472             $Image::ExifTool::Exif::formatNumber{$$ti{Writable}})
473             {
474 1         3 my $s1 = $Image::ExifTool::Exif::formatSize[$Image::ExifTool::Exif::formatNumber{$$ti{Format}}];
475 1         3 my $s2 = $Image::ExifTool::Exif::formatSize[$Image::ExifTool::Exif::formatNumber{$$ti{Writable}}];
476 1         5 $tiCount = int($tiCount * $s1 / $s2);
477             }
478 4 50 66     21 if ($tiCount > 0 and $count != $tiCount) {
479 0         0 $et->Warn(sprintf('Non-standard count (%d) for %s 0x%.4x %s', $count, $ifd, $tag, $$ti{Name}));
480             }
481             }
482             } elsif (not $otherSpec{$$et{FileType}} or
483             (not $otherSpec{$$et{FileType}}{$tag} and not $otherSpec{$$et{FileType}}{All}))
484             {
485 0 0       0 $et->Warn(sprintf('Unknown %s tag 0x%.4x', $ifd, $tag), 1) unless $otherSpec{$ifd};
486             }
487             }
488              
489             #------------------------------------------------------------------------------
490             # Validate image data offsets/sizes
491             # Inputs: 0) ExifTool ref, 1) offset info hash ref (arrays of tagInfo/value pairs, keyed by tagID)
492             # 2) directory name, 3) optional flag for minor warning
493             sub ValidateOffsetInfo($$$;$)
494             {
495 5     5 0 12 local $_;
496 5         19 my ($et, $offsetInfo, $dirName, $minor) = @_;
497              
498 5 50       31 my $fileSize = $$et{VALUE}{FileSize} or return;
499              
500             # (don't test RWZ files and some other file types)
501 5 50       21 return if $$et{DontValidateImageData};
502             # (Minolta A200 uses wrong byte order for these)
503 5 0 33     27 return if $$et{TIFF_TYPE} eq 'MRW' and $dirName eq 'IFD0' and $$et{Model} =~ /^DiMAGE A200/;
      33        
504             # (don't test 3FR, RWL or RW2 files)
505 5 50       33 return if $$et{TIFF_TYPE} =~ /^(3FR|RWL|RW2)$/;
506              
507 5         37 Image::ExifTool::Exif::ValidateImageData($et, $offsetInfo, $dirName);
508              
509             # loop through all offsets
510 5         19 while (%$offsetInfo) {
511 16         71 my ($id1) = sort keys %$offsetInfo;
512 16         43 my $offsets = $$offsetInfo{$id1};
513 16         31 delete $$offsetInfo{$id1};
514 16 100       60 next unless ref $offsets eq 'ARRAY';
515 6         56 my $id2 = $$offsets[0]{OffsetPair};
516 6 100 66     45 unless (defined $id2 and $$offsetInfo{$id2}) {
517 2 50 33     20 unless ($$offsets[0]{NotRealPair} or (defined $id2 and $id2 == -1)) {
      33        
518 2 100       25 my $corr = $$offsets[0]{IsOffset} ? 'size' : 'offset';
519 2 50       8 $et->Warn("$dirName:$$offsets[0]{Name} is missing the corresponding $corr tag") unless $minor;
520             }
521 2         14 next;
522             }
523 4         14 my $sizes = $$offsetInfo{$id2};
524 4         9 delete $$offsetInfo{$id2};
525 4 50       20 ($sizes, $offsets) = ($offsets, $sizes) if $$sizes[0]{IsOffset};
526 4         17 my @offsets = split ' ', $$offsets[1];
527 4         17 my @sizes = split ' ', $$sizes[1];
528 4 50       15 if (@sizes != @offsets) {
529             $et->Warn(sprintf('Wrong number of values in %s 0x%.4x %s',
530 0         0 $dirName, $$offsets[0]{TagID}, $$offsets[0]{Name}), $minor);
531 0         0 next;
532             }
533 4         15 while (@offsets) {
534 4         13 my $start = pop @offsets;
535 4         18 my $end = $start + pop @sizes;
536 4 50       17 $et->Warn("$dirName:$$offsets[0]{Name} is zero", $minor) if $start == 0;
537 4 50       15 $et->Warn("$dirName:$$sizes[0]{Name} is zero", $minor) if $start == $end;
538 4 50       38 next unless $end > $fileSize;
539 0 0       0 if ($start >= $fileSize) {
540 0 0       0 if ($start == 0xffffffff) {
541 0         0 $et->Warn("$dirName:$$offsets[0]{Name} is invalid (0xffffffff)", $minor);
542             } else {
543 0         0 $et->Warn("$dirName:$$offsets[0]{Name} is past end of file", $minor);
544             }
545             } else {
546 0         0 $et->Warn("$dirName:$$offsets[0]{Name}+$$sizes[0]{Name} runs past end of file", $minor);
547             }
548 0         0 last;
549             }
550             }
551             }
552              
553             #------------------------------------------------------------------------------
554             # Finish Validating tags
555             # Inputs: 0) ExifTool ref, 1) True to generate Validate tag
556             sub FinishValidate($$)
557             {
558 1     1 0 4 local $_;
559 1         5 my ($et, $mkTag) = @_;
560              
561 1   50     7 my $fileType = $$et{FILE_TYPE} || '';
562 1 50       9 $fileType = $$et{TIFF_TYPE} if $fileType eq 'TIFF';
563              
564 1 50       6 if ($validValue{$fileType}) {
565 0         0 my ($grp, $tag, %val);
566 0         0 local $SIG{'__WARN__'} = \&Image::ExifTool::SetWarning;
567 0         0 foreach $grp (sort keys %{$validValue{$fileType}}) {
  0         0  
568 0 0       0 next unless $$et{FOUND_DIR}{$grp};
569 0         0 my ($key, %val, %info, $minor, $verTag, $ver, $vstr);
570 0         0 my $verCheck = $verCheck{$grp};
571 0 0       0 if ($verCheck) {
572 0         0 ($verTag) = keys %$verCheck;
573 0         0 ($ver = $$et{VALUE}{$verTag}) =~ tr/0-9//dc; # (remove non-digits)
574 0 0       0 undef $ver unless $ver =~ /^\d{4}$/; # (already warned if invalid version)
575             }
576             # get all tags in this group
577 0         0 foreach $key (sort keys %{$$et{VALUE}}) {
  0         0  
578 0 0       0 next unless $et->GetGroup($key, 1) eq $grp;
579 0 0       0 next if $$et{TAG_EXTRA}{$key}{G3}; # ignore sub-documents
580             # fill in %val lookup with values based on tag ID
581 0         0 my $tag = $$et{TAG_INFO}{$key}{TagID};
582 0         0 $val{$tag} = $$et{VALUE}{$key};
583             # save TagInfo ref for later
584 0         0 $info{$tag} = $$et{TAG_INFO}{$key};
585 0 0       0 next unless defined $ver;
586 0         0 my $chk = $$verCheck{$verTag};
587 0 0 0     0 next if not defined $$chk{$tag} or $$chk{$tag} == 1 or $ver >= $$chk{$tag};
      0        
588 0 0       0 if ($verTag eq 'GPSVersionID') {
589 0         0 ($vstr = $$chk{$tag}) =~ s/^(\d)(\d)(\d)/$1.$2.$3./;
590             } else {
591 0         0 $vstr = sprintf('%.4d', $$chk{$tag});
592             }
593             $et->Warn(sprintf('%s tag 0x%.4x %s requires %s %s or higher',
594 0         0 $grp, $tag, $$et{TAG_INFO}{$key}{Name}, $verTag, $vstr));
595             }
596             # make quick lookup for values based on tag ID
597 0         0 my $validValue = $validValue{$fileType}{$grp};
598 0         0 foreach $tag (sort { $a <=> $b } keys %$validValue) {
  0         0  
599 0         0 my $val = $val{$tag};
600 0         0 my ($pre, $post);
601 0 0       0 if (defined $$validValue{$tag}) {
602             #### eval ($val, %val)
603 0         0 my $result = eval $$validValue{$tag};
604 0 0       0 if (not defined $result) {
    0          
605 0         0 $pre = 'Internal error validating';
606             } elsif ($result eq '') {
607 0 0       0 $pre = defined $val ? 'Invalid value for' : "Missing required $fileType";
608             } else {
609 0 0       0 next if $result eq '1';
610 0         0 $pre = $result;
611             }
612             } else {
613 0 0       0 next unless defined $val;
614 0         0 $post = "is not allowed in $fileType";
615 0         0 $minor = 1;
616             }
617 0         0 my $name;
618 0 0       0 if ($info{$tag}) {
619 0         0 $name = $info{$tag}{Name};
620             } else {
621 0 0       0 my $table = 'Image::ExifTool::'.($grp eq 'GPS' ? 'GPS' : 'Exif').'::Main';
622 0         0 my $tagInfo = GetTagTable($table)->{$tag};
623 0 0       0 $tagInfo = $$tagInfo[0] if ref $tagInfo eq 'ARRAY';
624 0 0       0 $name = $tagInfo ? $$tagInfo{Name} : '';
625             }
626 0 0 0     0 next if $$et{WrongFormat} and $$et{WrongFormat}{"$grp:$name"};
627 0 0       0 $pre ? ($pre .= ' ') : ($pre = '');
628 0 0       0 $post ? ($post = ' '.$post) : ($post = '');
629 0         0 $et->Warn(sprintf('%s%s tag 0x%.4x %s%s', $pre, $grp, $tag, $name, $post), $minor);
630             }
631             }
632             }
633             # validate file extension
634 1 50       8 if ($$et{FILENAME} ne '') {
635 1 50       15 my $fileExt = ($$et{FILENAME} =~ /^.*\.([^.]+)$/s) ? uc($1) : '';
636 1         8 my $extFileType = Image::ExifTool::GetFileType($fileExt);
637 1 50 33     9 if ($extFileType and $extFileType ne $fileType) {
638 1         7 my $normExt = $$et{VALUE}{FileTypeExtension};
639 1 50 33     9 if ($normExt and $normExt ne $fileExt) {
640 0         0 my $lkup = $Image::ExifTool::fileTypeLookup{$fileExt};
641 0 0 0     0 if (ref $lkup or $lkup ne $normExt) {
642 0         0 $et->Warn("File has wrong extension (should be $normExt, not $fileExt)");
643             }
644             }
645             }
646             }
647             # issue warning if FastScan option used
648 1 50       5 $et->Warn('Validation incomplete because FastScan option used') if $et->Options('FastScan');
649              
650             # generate Validate tag if necessary
651 1 50       11 if ($mkTag) {
652 1         4 my (@num, $key);
653             push @num, $$et{VALUE}{Error} ? ($$et{DUPL_TAG}{Error} || 0) + 1 : 0,
654 1 50 0     11 $$et{VALUE}{Warning} ? ($$et{DUPL_TAG}{Warning} || 0) + 1 : 0, 0;
    50 0        
655 1         4 for ($key = 'Warning'; ; ) {
656 1 50 33     7 ++$num[2] if $$et{VALUE}{$key} and $$et{VALUE}{$key} =~ /^\[minor\]/i;
657 1 50       9 $key = $et->NextTagKey($key) or last;
658             }
659 1         10 $et->FoundTag(Validate => "@num");
660             }
661             }
662              
663             1; # end
664              
665             __END__