File Coverage

/root/.cpan/build/App-zipdetails-4.005-0/bin/zipdetails
Criterion Covered Total %
statement 1477 3335 44.2
branch 248 1064 23.3
condition 106 496 21.3
subroutine 233 384 60.6
pod n/a
total 2064 5279 39.1


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             # zipdetails
4             #
5             # Display info on the contents of a Zip file
6             #
7              
8 6     6   37236 use 5.010; # for unpack "Q<"
  6         30  
9              
10 6         610120 my $NESTING_DEBUG = 0 ;
11              
12             BEGIN {
13             # Check for a 32-bit Perl
14 6 50   6   17 if (!eval { pack "Q", 1 }) {
  6         240  
15 0         0 warn "zipdetails requires 64 bit integers, ",
16             "this Perl has 32 bit integers.\n";
17 0         0 exit(1);
18             }
19             }
20              
21 6 50   6   232 BEGIN { pop @INC if $INC[-1] eq '.' }
22 6     6   39 use strict;
  6         12  
  6         154  
23 6     6   27 use warnings ;
  6         17  
  6         313  
24 6     6   25 no warnings 'portable'; # for unpacking > 2^32
  6         11  
  6         322  
25 6     6   40 use feature qw(state say);
  6         9  
  6         1180  
26              
27 6     6   3185 use IO::File;
  6         67777  
  6         951  
28 6     6   3685 use Encode;
  6         118075  
  6         625  
29 6     6   4381 use Getopt::Long;
  6         119907  
  6         49  
30 6     6   1421 use List::Util qw(min max);
  6         12  
  6         1515  
31              
32 6         22 my $VERSION = '4.005' ;
33              
34             sub fatal_tryWalk;
35             sub fatal_truncated ;
36             sub info ;
37             sub warning ;
38             sub error ;
39             sub debug ;
40             sub fatal ;
41             sub topLevelFatal ;
42             sub internalFatal;
43             sub need ;
44             sub decimalHex;
45              
46 6     6   47 use constant MAX64 => 0xFFFFFFFFFFFFFFFF ;
  6         14  
  6         485  
47 6     6   39 use constant MAX32 => 0xFFFFFFFF ;
  6         13  
  6         337  
48 6     6   38 use constant MAX16 => 0xFFFF ;
  6         12  
  6         306  
49              
50             # Compression types
51 6     6   32 use constant ZIP_CM_STORE => 0 ;
  6         13  
  6         329  
52 6     6   62 use constant ZIP_CM_IMPLODE => 6 ;
  6         11  
  6         323  
53 6     6   41 use constant ZIP_CM_DEFLATE => 8 ;
  6         9  
  6         258  
54 6     6   29 use constant ZIP_CM_BZIP2 => 12 ;
  6         11  
  6         276  
55 6     6   33 use constant ZIP_CM_LZMA => 14 ;
  6         12  
  6         325  
56 6     6   34 use constant ZIP_CM_REFERENCE => 92 ;
  6         11  
  6         277  
57 6     6   27 use constant ZIP_CM_PPMD => 98 ;
  6         12  
  6         285  
58 6     6   33 use constant ZIP_CM_AES => 99 ; # Not really c compression method
  6         11  
  6         305  
59              
60             # General Purpose Flag
61 6     6   28 use constant ZIP_GP_FLAG_ENCRYPTED_MASK => (1 << 0) ;
  6         11  
  6         280  
62 6     6   29 use constant ZIP_GP_FLAG_STREAMING_MASK => (1 << 3) ;
  6         10  
  6         327  
63 6     6   27 use constant ZIP_GP_FLAG_PATCHED_MASK => (1 << 5) ;
  6         11  
  6         344  
64 6     6   29 use constant ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK => (1 << 6) ;
  6         11  
  6         299  
65 6     6   32 use constant ZIP_GP_FLAG_LZMA_EOS_PRESENT => (1 << 1) ;
  6         10  
  6         438  
66 6     6   78 use constant ZIP_GP_FLAG_LANGUAGE_ENCODING => (1 << 11) ;
  6         11  
  6         487  
67 6     6   32 use constant ZIP_GP_FLAG_PKWARE_ENHANCED_COMP => (1 << 12) ;
  6         10  
  6         355  
68 6     6   33 use constant ZIP_GP_FLAG_ENCRYPTED_CD => (1 << 13) ;
  6         69  
  6         431  
69              
70             # All the encryption flags
71 6     6   33 use constant ZIP_GP_FLAG_ALL_ENCRYPT => (ZIP_GP_FLAG_ENCRYPTED_MASK | ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK | ZIP_GP_FLAG_ENCRYPTED_CD );
  6         34  
  6         704  
72              
73             # Internal File Attributes
74 6     6   38 use constant ZIP_IFA_TEXT_MASK => 1;
  6         9  
  6         356  
75              
76             # Signatures for each of the headers
77 6     6   34 use constant ZIP_LOCAL_HDR_SIG => 0x04034b50;
  6         11  
  6         284  
78 6     6   32 use constant ZIP_DATA_HDR_SIG => 0x08074b50;
  6         20  
  6         323  
79 6     6   32 use constant ZIP_CENTRAL_HDR_SIG => 0x02014b50;
  6         11  
  6         326  
80 6     6   40 use constant ZIP_END_CENTRAL_HDR_SIG => 0x06054b50;
  6         7  
  6         380  
81 6     6   32 use constant ZIP64_END_CENTRAL_REC_HDR_SIG => 0x06064b50;
  6         8  
  6         287  
82 6     6   29 use constant ZIP64_END_CENTRAL_LOC_HDR_SIG => 0x07064b50;
  6         8  
  6         266  
83 6     6   38 use constant ZIP_DIGITAL_SIGNATURE_SIG => 0x05054b50;
  6         10  
  6         268  
84 6     6   30 use constant ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG => 0x08064b50;
  6         10  
  6         264  
85 6     6   28 use constant ZIP_SINGLE_SEGMENT_MARKER => 0x30304b50; # APPNOTE 6.3.10, sec 8.5.4
  6         10  
  6         289  
86              
87             # Windows file attributes -- see https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
88 6     6   33 use constant WIN_FILE_ATTRIBUTE_READONLY => 0x0001;
  6         10  
  6         280  
89 6     6   31 use constant WIN_FILE_ATTRIBUTE_HIDDEN => 0x0002;
  6         26  
  6         359  
90 6     6   34 use constant WIN_FILE_ATTRIBUTE_SYSTEM => 0x0004;
  6         14  
  6         306  
91 6     6   30 use constant WIN_FILE_ATTRIBUTE_LABEL => 0x0008; # Unused?
  6         8  
  6         340  
92 6     6   30 use constant WIN_FILE_ATTRIBUTE_DIRECTORY => 0x0010;
  6         66  
  6         384  
93 6     6   46 use constant WIN_FILE_ATTRIBUTE_ARCHIVE => 0x0020;
  6         21  
  6         329  
94 6     6   36 use constant WIN_FILE_ATTRIBUTE_DEVICE_OR_SYMBOLIC_LINK => 0x0040; # Not clear if this is used for a symbolic link. Needs a real use case to verify.
  6         13  
  6         324  
95 6     6   74 use constant WIN_FILE_ATTRIBUTE_NORMAL_OR_EXECUTABLE => 0x0080; # Not clear if this is used for a executable. Needs a real use case to verify.
  6         9  
  6         330  
96 6     6   49 use constant WIN_FILE_ATTRIBUTE_TEMPORARY => 0x0100;
  6         11  
  6         357  
97 6     6   32 use constant WIN_FILE_ATTRIBUTE_SPARSE_FILE => 0x0200;
  6         20  
  6         265  
98 6     6   30 use constant WIN_FILE_ATTRIBUTE_REPARSE_POINT => 0x0400;
  6         9  
  6         260  
99 6     6   26 use constant WIN_FILE_ATTRIBUTE_COMPRESSED => 0x0800;
  6         7  
  6         327  
100 6     6   30 use constant WIN_FILE_ATTRIBUTE_OFFLINE => 0x1000;
  6         20  
  6         253  
101 6     6   37 use constant WIN_FILE_ATTRIBUTE_NOT_CONTENT_INDEXED => 0x2000;
  6         11  
  6         284  
102 6     6   30 use constant WIN_FILE_ATTRIBUTE_ENCRYPTED => 0x4000;
  6         9  
  6         286  
103              
104             # Windows symlink covers multiple bits
105 6     6   29 use constant WIN_FILE_ATTRIBUTE_SYMBOLIC_LINK_MASK => WIN_FILE_ATTRIBUTE_REPARSE_POINT;
  6         9  
  6         275  
106              
107             # The Symbolic Link Mask may become this if I can find a real use case.
108             # use constant WIN_FILE_ATTRIBUTE_SYMBOLIC_LINK_MASK => WIN_FILE_ATTRIBUTE_DEVICE_OR_SYMBOLIC_LINK | WIN_FILE_ATTRIBUTE_REPARSE_POINT;
109              
110             # Extra sizes
111 6     6   30 use constant ZIP_EXTRA_HEADER_SIZE => 2 ;
  6         9  
  6         383  
112 6     6   79 use constant ZIP_EXTRA_MAX_SIZE => 0xFFFF ;
  6         70  
  6         388  
113 6     6   108 use constant ZIP_EXTRA_SUBFIELD_ID_SIZE => 2 ;
  6         12  
  6         334  
114 6     6   36 use constant ZIP_EXTRA_SUBFIELD_LEN_SIZE => 2 ;
  6         9  
  6         453  
115 6         785 use constant ZIP_EXTRA_SUBFIELD_HEADER_SIZE => ZIP_EXTRA_SUBFIELD_ID_SIZE +
116 6     6   536 ZIP_EXTRA_SUBFIELD_LEN_SIZE;
  6         15  
117 6         343 use constant ZIP_EXTRA_SUBFIELD_MAX_SIZE => ZIP_EXTRA_MAX_SIZE -
118 6     6   40 ZIP_EXTRA_SUBFIELD_HEADER_SIZE;
  6         10  
119              
120 6     6   38 use constant ZIP_EOCD_MIN_SIZE => 22 ;
  6         13  
  6         430  
121              
122              
123 6     6   33 use constant ZIP_LD_FILENAME_OFFSET => 30;
  6         54  
  6         339  
124 6     6   32 use constant ZIP_CD_FILENAME_OFFSET => 46;
  6         9  
  6         14475  
125              
126 6         176 my %ZIP_CompressionMethods =
127             (
128             0 => 'Stored',
129             1 => 'Shrunk',
130             2 => 'Reduced compression factor 1',
131             3 => 'Reduced compression factor 2',
132             4 => 'Reduced compression factor 3',
133             5 => 'Reduced compression factor 4',
134             6 => 'Imploded',
135             7 => 'Reserved for Tokenizing compression algorithm',
136             8 => 'Deflated',
137             9 => 'Deflate64',
138             10 => 'PKWARE Data Compression Library Imploding',
139             11 => 'Reserved by PKWARE',
140             12 => 'BZIP2',
141             13 => 'Reserved by PKWARE',
142             14 => 'LZMA',
143             15 => 'Reserved by PKWARE',
144             16 => 'IBM z/OS CMPSC Compression',
145             17 => 'Reserved by PKWARE',
146             18 => 'IBM/TERSE or Xceed BWT', # APPNOTE has IBM/TERSE. Xceed reuses it unofficially
147             19 => 'IBM LZ77 z Architecture (PFS)',
148             20 => 'Ipaq8', # see https://encode.su/threads/1048-info-zip-lpaq8
149             92 => 'Reference', # Winzip Only from version 25
150             93 => 'Zstandard',
151             94 => 'MP3',
152             95 => 'XZ',
153             96 => 'WinZip JPEG Compression',
154             97 => 'WavPack compressed data',
155             98 => 'PPMd version I, Rev 1',
156             99 => 'AES Encryption', # Apple also use this code for LZFSE compression in IPA files
157             );
158              
159 6         93 my %OS_Lookup = (
160             0 => "MS-DOS",
161             1 => "Amiga",
162             2 => "OpenVMS",
163             3 => "Unix",
164             4 => "VM/CMS",
165             5 => "Atari ST",
166             6 => "HPFS (OS/2, NT 3.x)",
167             7 => "Macintosh",
168             8 => "Z-System",
169             9 => "CP/M",
170             10 => "Windows NTFS or TOPS-20",
171             11 => "MVS or NTFS",
172             12 => "VSE or SMS/QDOS",
173             13 => "Acorn RISC OS",
174             14 => "VFAT",
175             15 => "alternate MVS",
176             16 => "BeOS",
177             17 => "Tandem",
178             18 => "OS/400",
179             19 => "OS/X (Darwin)",
180             30 => "AtheOS/Syllable",
181             );
182              
183             {
184 0         0 package Signatures ;
185              
186 6         125 my %Lookup = (
187             # Map unpacked signature to
188             # decoder
189             # name
190             # central flag
191              
192             # Core Signatures
193             ::ZIP_LOCAL_HDR_SIG, [ \&::LocalHeader, "Local File Header", 0 ],
194             ::ZIP_DATA_HDR_SIG, [ \&::DataDescriptor, "Data Descriptor", 0 ],
195             ::ZIP_CENTRAL_HDR_SIG, [ \&::CentralHeader, "Central Directory Header", 1 ],
196             ::ZIP_END_CENTRAL_HDR_SIG, [ \&::EndCentralHeader, "End Central Directory Record", 1 ],
197             ::ZIP_SINGLE_SEGMENT_MARKER, [ \&::SingleSegmentMarker, "Split Archive Single Segment Marker", 0],
198              
199             # Zip64
200             ::ZIP64_END_CENTRAL_REC_HDR_SIG, [ \&::Zip64EndCentralHeader, "Zip64 End of Central Directory Record", 1 ],
201             ::ZIP64_END_CENTRAL_LOC_HDR_SIG, [ \&::Zip64EndCentralLocator, "Zip64 End of Central Directory Locator", 1 ],
202              
203             # Digital signature (pkzip)
204             ::ZIP_DIGITAL_SIGNATURE_SIG, [ \&::DigitalSignature, "Digital Signature", 1 ],
205              
206             # Archive Encryption Headers (pkzip) - never seen this one
207             ::ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG, [ \&::ArchiveExtraDataRecord, "Archive Extra Record", 1 ],
208             );
209              
210             sub decoder
211             {
212 30     30   50 my $signature = shift ;
213              
214             return undef
215 30 50       94 unless exists $Lookup{$signature};
216              
217 30         76 return $Lookup{$signature}[0];
218             }
219              
220             sub name
221             {
222 122     122   223 my $signature = shift ;
223              
224             return 'UNKNOWN'
225 122 50       366 unless exists $Lookup{$signature};
226              
227 122         500 return $Lookup{$signature}[1];
228             }
229              
230             sub titleName
231             {
232 0     0   0 my $signature = shift ;
233              
234 0         0 uc name($signature);
235             }
236              
237             sub hexValue
238             {
239 0     0   0 my $signature = shift ;
240 0         0 sprintf "0x%X", $signature ;
241             }
242              
243             sub hexValue32
244             {
245 0     0   0 my $signature = shift ;
246 0         0 sprintf "0x%08X", $signature ;
247             }
248              
249             sub hexValue16
250             {
251 0     0   0 my $signature = shift ;
252 0         0 sprintf "0x%04X", $signature ;
253             }
254              
255             sub nameAndHex
256             {
257 0     0   0 my $signature = shift ;
258              
259 0         0 return "'" . name($signature) . "' (" . hexValue32($signature) . ")"
260             }
261              
262             sub isCentralHeader
263             {
264 0     0   0 my $signature = shift ;
265              
266             return undef
267 0 0       0 unless exists $Lookup{$signature};
268              
269 0         0 return $Lookup{$signature}[2];
270             }
271             #sub isValidSignature
272             #{
273             # my $signature = shift ;
274             # return exists $Lookup{$signature}}
275             #}
276              
277             sub getSigsForScan
278             {
279             my %sigs =
280             # map { $_ => 1 }
281             # map { substr($_->[0], 2, 2) => $_->[1] } # don't want the initial "PK"
282 26     26   147 map { substr(pack("V", $_), 2, 2) => $_ }
  234         1094  
283             keys %Lookup ;
284              
285 26         263 return %sigs;
286             }
287              
288             }
289              
290 6         14 my %Extras = (
  6         715  
291              
292             # Local Central
293             # ID Name Handler min size max size min size max size
294             0x0001, ['ZIP64', \&decode_Zip64, 0, 28, 0, 28],
295             0x0007, ['AV Info', undef], # TODO
296             0x0008, ['Extended Language Encoding', undef], # TODO
297             0x0009, ['OS/2 extended attributes', undef], # TODO
298             0x000a, ['NTFS FileTimes', \&decode_NTFS_Filetimes, 32, 32, 32, 32],
299             0x000c, ['OpenVMS', \&decode_OpenVMS, 4, undef, 4, undef],
300             0x000d, ['Unix', undef],
301             0x000e, ['Stream & Fork Descriptors', undef], # TODO
302             0x000f, ['Patch Descriptor', undef],
303             0x0014, ['PKCS#7 Store for X.509 Certificates', undef],
304             0x0015, ['X.509 Certificate ID and Signature for individual file', undef],
305             0x0016, ['X.509 Certificate ID for Central Directory', undef],
306             0x0017, ['Strong Encryption Header', \&decode_strong_encryption, 12, undef, 12, undef],
307             0x0018, ['Record Management Controls', undef],
308             0x0019, ['PKCS#7 Encryption Recipient Certificate List', undef],
309             0x0020, ['Reserved for Timestamp record', undef],
310             0x0021, ['Policy Decryption Key Record', undef],
311             0x0022, ['Smartcrypt Key Provider Record', undef],
312             0x0023, ['Smartcrypt Policy Key Data Record', undef],
313              
314             # The Header ID mappings defined by Info-ZIP and third parties are:
315              
316             0x0065, ['IBM S/390 attributes - uncompressed', \&decode_MVS, 4, undef, 4, undef],
317             0x0066, ['IBM S/390 attributes - compressed', undef],
318             0x07c8, ['Info-ZIP Macintosh (old, J. Lee)', undef],
319             0x10c5, ['Minizip CMS Signature', \&decode_Minizip_Signature, undef, undef, undef, undef], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md
320             0x1986, ['Pixar USD', undef], # TODO
321             0x1a51, ['Minizip Hash', \&decode_Minizip_Hash, 4, undef, 4, undef], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md
322             0x2605, ['ZipIt Macintosh (first version)', undef],
323             0x2705, ['ZipIt Macintosh v 1.3.5 and newer (w/o full filename)', undef],
324             0x2805, ['ZipIt Macintosh v 1.3.5 and newer', undef],
325             0x334d, ["Info-ZIP Macintosh (new, D. Haase's 'Mac3' field)", undef], # TODO
326             0x4154, ['Tandem NSK [TA]', undef], # TODO
327             0x4341, ['Acorn/SparkFS [AC]', undef], # TODO
328             0x4453, ['Windows NT security descriptor [SD]', \&decode_NT_security, 11, undef, 4, 4], # TODO
329             0x4690, ['POSZIP 4690', undef],
330             0x4704, ['VM/CMS', undef],
331             0x470f, ['MVS', undef],
332             0x4850, ['Info-ZIP PHold [PH]', undef],
333             0x4854, ['Theos [TH]', undef],
334             0x4b46, ['FWKCS MD5 [FK]', undef],
335             0x4c41, ['OS/2 access control list [AL]', undef],
336             0x4d49, ['Info-ZIP OpenVMS (obsolete) [IM]', undef],
337             0x4d63, ['Macintosh SmartZIP [cM]', undef], # TODO
338             0x4f4c, ['Xceed original location [LO]', undef],
339             0x5356, ['AOS/VS (binary ACL) [VS]', undef],
340             0x5455, ['Extended Timestamp [UT]', \&decode_UT, 1, 13, 1, 13],
341             0x554e, ['Xceed unicode extra field [UN]', \&decode_Xceed_unicode, 6, undef, 8, undef],
342             0x564B, ['Key-Value Pairs [KV]', \&decode_Key_Value_Pair, 13, undef, 13, undef],# TODO -- https://github.com/sozip/keyvaluepairs-spec/blob/master/zip_keyvalue_extra_field_specification.md
343             0x5855, ['Unix Extra type 1 [UX]', \&decode_UX, 12, 12, 8, 8],
344             0x5a4c, ['ZipArchive Unicode Filename [LZ]', undef], # https://www.artpol-software.com/ZipArchive
345             0x5a4d, ['ZipArchive Offsets Array [MZ]', undef], # https://www.artpol-software.com/ZipArchive
346             0x6375, ['Unicode Comment [uc]', \&decode_uc, 5, undef, 5, undef],
347             0x6542, ['BeOS/Haiku [Be]', undef], # TODO
348             0x6854, ['Theos [Th]', undef],
349             0x6C78, ['Extended Local File Header [xl]', undef], # TODO
350             0x7075, ['Unicode Path [up]', \&decode_up, 5, undef, 5, undef],
351             0x756e, ['ASi Unix [un]', \&decode_ASi_Unix], # TODO
352             0x7441, ['AtheOS [At]', undef],
353             0x7855, ['Unix Extra type 2 [Ux]', \&decode_Ux, 4,4, 0, 0 ],
354             0x7875, ['Unix Extra type 3 [ux]', \&decode_ux, 3, undef, 3, undef],
355             0x9901, ['AES Encryption', \&decode_AES, 7, 7, 7, 7],
356             0x9903, ['Reference', \&decode_Reference, 20, 20, 20, 20], # Added in WinZip ver 25
357             0xa11e, ['Data Stream Alignment', \&decode_DataStreamAlignment, 2, undef, 2, undef ],
358             0xA220, ['Open Packaging Growth Hint', \&decode_GrowthHint, 4, undef, 4, undef ],
359             0xCAFE, ['Java Executable', \&decode_Java_exe, 0, 0, 0, 0],
360             0xCDCD, ['Minizip Central Directory', \&decode_Minizip_CD, 8, 8, 8, 8], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md
361             0xd935, ['Android APK Alignment', undef], # TODO
362             0xE57a, ['ALZip Codepage', undef], # TODO
363             0xfb4a, ['SMS/QDOS', undef], # TODO
364             );
365              
366             # Dummy entry only used in test harness, so only enable when ZIPDETAILS_TESTHARNESS is set
367             $Extras{0xFFFF} =
368             ['DUMMY', \&decode_DUMMY, undef, undef, undef, undef]
369 6 50       74 if $ENV{ZIPDETAILS_TESTHARNESS} ;
370              
371             sub extraFieldIdentifier
372             {
373 0     0   0 my $id = shift ;
374              
375 0   0     0 my $name = $Extras{$id}[0] // "Unknown";
376              
377 0         0 return "Extra Field '$name' (ID " . hexValue16($id) .")";
378             }
379              
380             # Zip64EndCentralHeader version 2
381 6         108 my %HashIDLookup = (
382             0x0000 => 'none',
383             0x0001 => 'CRC32',
384             0x8003 => 'MD5',
385             0x8004 => 'SHA1',
386             0x8007 => 'RIPEMD160',
387             0x800C => 'SHA256',
388             0x800D => 'SHA384',
389             0x800E => 'SHA512',
390             );
391              
392              
393             # Zip64EndCentralHeader version 2, Strong Encryption Header & DecryptionHeader
394 6         106 my %AlgIdLookup = (
395             0x6601 => "DES",
396             0x6602 => "RC2 (version needed to extract < 5.2)",
397             0x6603 => "3DES 168",
398             0x6609 => "3DES 112",
399             0x660E => "AES 128",
400             0x660F => "AES 192",
401             0x6610 => "AES 256",
402             0x6702 => "RC2 (version needed to extract >= 5.2)",
403             0x6720 => "Blowfish",
404             0x6721 => "Twofish",
405             0x6801 => "RC4",
406             0xFFFF => "Unknown algorithm",
407             );
408              
409             # Zip64EndCentralHeader version 2, Strong Encryption Header & DecryptionHeader
410 6         31 my %FlagsLookup = (
411             0x0001 => "Password required to decrypt",
412             0x0002 => "Certificates only",
413             0x0003 => "Password or certificate required to decrypt",
414              
415             # Values > 0x0003 reserved for certificate processing
416             );
417              
418             # Strong Encryption Header & DecryptionHeader
419 6         19 my %HashAlgLookup = (
420             0x8004 => 'SHA1',
421             );
422              
423 6         12 my $FH;
424              
425 6         13 my $ZIP64 = 0 ;
426 6         13 my $NIBBLES = 8;
427              
428 6         17 my $LocalHeaderCount = 0;
429 6         10 my $CentralHeaderCount = 0;
430 6         13 my $InfoCount = 0;
431 6         14 my $WarningCount = 0;
432 6         12 my $ErrorCount = 0;
433 6         13 my $lastWasMessage = 0;
434              
435 6         11 my $fatalDisabled = 0;
436              
437 6         11 my $OFFSET = 0 ;
438              
439             # Prefix data
440 6         10 my $POSSIBLE_PREFIX_DELTA = 0;
441 6         13 my $PREFIX_DELTA = 0;
442              
443 6         11 my $TRAILING = 0 ;
444 6         13 my $PAYLOADLIMIT = 256;
445 6         15 my $ZERO = 0 ;
446 6         11 my $APK = 0 ;
447 6         11 my $START_APK = 0;
448 6         9 my $APK_LEN = 0;
449              
450 6         49 my $CentralDirectory = CentralDirectory->new();
451 6         39 my $LocalDirectory = LocalDirectory->new();
452 6         41 my $HeaderOffsetIndex = HeaderOffsetIndex->new();
453 6         12 my $EOCD_Present = 0;
454              
455             sub prOff
456             {
457 432     432   683 my $offset = shift;
458 432         673 my $s = offset($OFFSET);
459 432         625 $OFFSET += $offset;
460 432         730 return $s;
461             }
462              
463             sub offset
464             {
465 1296     1296   1749 my $v = shift ;
466              
467 1296         3772 sprintf("%0${NIBBLES}X", $v);
468             }
469              
470             # Format variables
471 6         18 my ($OFF, $ENDS_AT, $LENGTH, $CONTENT, $TEXT, $VALUE) ;
472              
473 6         17 my $FMT1 = 'STDOUT1';
474 6         14 my $FMT2 = 'STDOUT2';
475              
476             sub setupFormat
477             {
478 6     6   12 my $wantVerbose = shift ;
479 6         13 my $nibbles = shift;
480              
481 6         31 my $width = '@' . ('>' x ($nibbles -1));
482 6         20 my $space = " " x length($width);
483              
484             # See https://github.com/Perl/perl5/issues/14255 for issue with "^*" in perl < 5.22
485             # my $rightColumn = "^*" ;
486 6         12 my $rightColumn = "^" . ("<" x 132);
487              
488             # Fill mode can split on space or newline chars
489             # Splitting on hyphen works differently from Perl 5.20 onwards
490 6         26 $: = " \n";
491              
492 6         13 my $fmt ;
493              
494 6 100       20 if ($wantVerbose) {
495              
496 3         448 eval "format $FMT1 =
497             $width $width $width ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< $rightColumn
498             \$OFF, \$ENDS_AT, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE
499             $space $space $space ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< $rightColumn~~
500             \$CONTENT, \$TEXT, \$VALUE
501             .
502             ";
503              
504 3         424 eval "format $FMT2 =
505             $width $width $width ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< $rightColumn
506             \$OFF, \$ENDS_AT, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE
507             $space $space $space ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< $rightColumn~~
508             \$CONTENT, \$TEXT, \$VALUE
509             .
510             ";
511              
512             }
513             else {
514 3         369 eval "format $FMT1 =
515             $width ^<<<<<<<<<<<<<<<<<<<< $rightColumn
516             \$OFF, \$TEXT, \$VALUE
517             $space ^<<<<<<<<<<<<<<<<<<<< $rightColumn~~
518             \$TEXT, \$VALUE
519             .
520             ";
521              
522 3         265 eval "format $FMT2 =
523             $width ^<<<<<<<<<<<<<<<<<< $rightColumn
524             \$OFF, \$TEXT, \$VALUE
525             $space ^<<<<<<<<<<<<<<<<<< $rightColumn~~
526             \$TEXT, \$VALUE
527             .
528             "
529             }
530              
531 6     6   53 no strict 'refs';
  6         14  
  6         6914  
532 6         204 open($FMT1, ">&", \*STDOUT); select $FMT1; $| = 1 ;
  6         29  
  6         26  
533 6         78 open($FMT2, ">&", \*STDOUT); select $FMT2; $| = 1 ;
  6         20  
  6         16  
534              
535 6         15 select 'STDOUT';
536 6         25 $| = 1;
537              
538             }
539              
540             sub mySpr
541             {
542 468     468   740 my $format = shift ;
543              
544 468 50       976 return "" if ! defined $format;
545 468 50       1316 return $format unless @_ ;
546 0         0 return sprintf $format, @_ ;
547             }
548              
549             sub xDump
550             {
551 12     12   40 my $input = shift;
552              
553 12         29 $input =~ tr/\0-\37\177-\377/./;
554 12         43 return $input;
555             }
556              
557             sub hexDump
558             {
559 432     432   2334 return uc join ' ', unpack('(H2)*', $_[0]);
560             }
561              
562             sub hexDump16
563             {
564             return uc
565             join "\r",
566 0     0   0 map { join ' ', unpack('(H2)*', $_ ) }
  0         0  
567             unpack('(a16)*', $_[0]) ;
568             }
569              
570             sub charDump2
571             {
572 0     0   0 sprintf "%v02X", $_[0];
573             }
574              
575             sub charDump
576             {
577 0     0   0 sprintf "%vX", $_[0];
578             }
579              
580             sub hexValue
581             {
582 0     0   0 return sprintf("0x%X", $_[0]);
583             }
584              
585             sub hexValue32
586             {
587 24     24   128 return sprintf("0x%08X", $_[0]);
588             }
589              
590             sub hexValue16
591             {
592 0     0   0 return sprintf("0x%04X", $_[0]);
593             }
594              
595             sub outHexdump
596             {
597 0     0   0 my $size = shift;
598 0         0 my $text = shift;
599 0         0 my $limit = shift ;
600              
601 0 0       0 return 0
602             if $size == 0;
603              
604             # TODO - add a limit to data output
605             # if ($limit)
606             # {
607             # outSomeData($size, $text);
608             # }
609             # else
610             {
611 0         0 myRead(my $payload, $size);
  0         0  
612 0         0 out($payload, $text, hexDump16($payload));
613             }
614              
615 0         0 return $size;
616             }
617              
618             sub decimalHex
619             {
620 420   50 420   2482 sprintf("%0*X (%u)", $_[1] // 0, $_[0], $_[0])
621             }
622              
623             sub decimalHex0x
624             {
625 0   0 0   0 sprintf("0x%0*X (%u)", $_[1] // 0, $_[0], $_[0])
626             }
627              
628             sub decimalHex0xUndef
629             {
630 0 0   0   0 return 'Unknown'
631             if ! defined $_[0];
632              
633 0         0 return decimalHex0x @_;
634             }
635              
636             sub out
637             {
638 432     432   634 my $data = shift;
639 432         569 my $text = shift;
640 432         656 my $format = shift;
641              
642 432         630 my $size = length($data) ;
643              
644 432 50       1280 $ENDS_AT = offset($OFFSET + ($size ? $size - 1 : 0)) ;
645 432         901 $OFF = prOff($size);
646 432         694 $LENGTH = offset($size) ;
647 432         878 $CONTENT = hexDump($data);
648 432         715 $TEXT = $text;
649 432         852 $VALUE = mySpr $format, @_;
650              
651 6     6   51 no warnings;
  6         12  
  6         2807  
652              
653 432         14459 write $FMT1 ;
654              
655 432         2246 $lastWasMessage = 0;
656             }
657              
658             sub out0
659             {
660 0     0   0 my $size = shift;
661 0         0 my $text = shift;
662 0         0 my $format = shift;
663              
664 0 0       0 $ENDS_AT = offset($OFFSET + ($size ? $size - 1 : 0)) ;
665 0         0 $OFF = prOff($size);
666 0         0 $LENGTH = offset($size) ;
667 0         0 $CONTENT = '...';
668 0         0 $TEXT = $text;
669 0         0 $VALUE = mySpr $format, @_;
670              
671 0         0 write $FMT1;
672              
673 0         0 skip($FH, $size);
674              
675 0         0 $lastWasMessage = 0;
676             }
677              
678             sub out1
679             {
680 36     36   65 my $text = shift;
681 36         51 my $format = shift;
682              
683 36         53 $ENDS_AT = '' ;
684 36         72 $OFF = '';
685 36         53 $LENGTH = '' ;
686 36         56 $CONTENT = '';
687 36         53 $TEXT = $text;
688 36         73 $VALUE = mySpr $format, @_;
689              
690 36         1016 write $FMT1;
691              
692 36         164 $lastWasMessage = 0;
693             }
694              
695             sub out2
696             {
697 0     0   0 my $data = shift ;
698 0         0 my $text = shift ;
699 0         0 my $format = shift;
700              
701 0         0 my $size = length($data) ;
702 0 0       0 $ENDS_AT = offset($OFFSET + ($size ? $size - 1 : 0)) ;
703 0         0 $OFF = prOff($size);
704 0         0 $LENGTH = offset($size);
705 0         0 $CONTENT = hexDump($data);
706 0         0 $TEXT = $text;
707 0         0 $VALUE = mySpr $format, @_;
708              
709 6     6   50 no warnings;
  6         12  
  6         82057  
710 0         0 write $FMT2;
711              
712 0         0 $lastWasMessage = 0;
713             }
714              
715              
716             sub Value
717             {
718 300     300   473 my $letter = shift;
719              
720 300 100       889 if ($letter eq 'C')
    100          
    50          
    0          
721 72         167 { return decimalHex($_[0], 2) }
722             elsif ($letter eq 'v')
723 114         268 { return decimalHex($_[0], 4) }
724             elsif ($letter eq 'V')
725 114         255 { return decimalHex($_[0], 8) }
726             elsif ($letter eq 'Q<')
727 0         0 { return decimalHex($_[0], 16) }
728             else
729 0         0 { internalFatal undef, "here letter $letter"}
730             }
731              
732             sub outer
733             {
734 300     300   419 my $name = shift ;
735 300         446 my $unpack = shift ;
736 300         430 my $size = shift ;
737 300         406 my $cb1 = shift ;
738 300         428 my $cb2 = shift ;
739              
740              
741 300         1294 myRead(my $buff, $size);
742 300         801 my (@value) = unpack $unpack, $buff;
743 300         631 my $hex = Value($unpack, @value);
744              
745 300 100       683 if (defined $cb1) {
746 96         135 my $v ;
747 96 50       212 if (ref $cb1 eq 'CODE') {
748 96         228 $v = $cb1->(@value) ;
749             }
750             else {
751 0         0 $v = $cb1 ;
752             }
753              
754 96 100       344 $v = "'" . $v unless $v =~ /^'/;
755 96 100       297 $v .= "'" unless $v =~ /'$/;
756 96         170 $hex .= " $v" ;
757             }
758              
759 300         709 out $buff, $name, $hex ;
760              
761 300 50       780 $cb2->(@value)
762             if defined $cb2 ;
763              
764 300         882 return $value[0];
765             }
766              
767             sub out_C
768             {
769 72     72   131 my $name = shift ;
770 72         97 my $cb1 = shift ;
771 72         98 my $cb2 = shift ;
772              
773 72         212 outer($name, 'C', 1, $cb1, $cb2);
774             }
775              
776             sub out_v
777             {
778 114     114   193 my $name = shift ;
779 114         182 my $cb1 = shift ;
780 114         154 my $cb2 = shift ;
781              
782 114         228 outer($name, 'v', 2, $cb1, $cb2);
783             }
784              
785             sub out_V
786             {
787 114     114   199 my $name = shift ;
788 114         167 my $cb1 = shift ;
789 114         160 my $cb2 = shift ;
790              
791 114         267 outer($name, 'V', 4, $cb1, $cb2);
792             }
793              
794             sub out_Q
795             {
796 0     0   0 my $name = shift ;
797 0         0 my $cb1 = shift ;
798 0         0 my $cb2 = shift ;
799              
800 0         0 outer($name, 'Q<', 8, $cb1, $cb2);
801             }
802              
803             sub outSomeData
804             {
805 12     12   25 my $size = shift;
806 12         20 my $message = shift;
807 12         21 my $redact = shift ;
808              
809             # return if $size == 0;
810              
811 12 50       32 if ($size > 0) {
812 12 50       30 if ($size > $PAYLOADLIMIT) {
813 0         0 my $before = $FH->tell();
814 0         0 out0 $size, $message;
815             } else {
816 12         71 myRead(my $buffer, $size );
817 12 50       41 $buffer = "X" x $size
818             if $redact;
819 12         49 out $buffer, $message, xDump $buffer ;
820             }
821             }
822             }
823              
824             sub outSomeDataParagraph
825             {
826 0     0   0 my $size = shift;
827 0         0 my $message = shift;
828 0         0 my $redact = shift ;
829              
830 0 0       0 return if $size == 0;
831              
832 0         0 print "\n";
833 0         0 outSomeData($size, $message, $redact);
834              
835             }
836              
837             sub unpackValue_C
838             {
839 0     0   0 Value_v(unpack "C", $_[0]);
840             }
841              
842             sub Value_C
843             {
844 12     12   35 return decimalHex($_[0], 2);
845             }
846              
847              
848             sub unpackValue_v
849             {
850 0     0   0 Value_v(unpack "v", $_[0]);
851             }
852              
853             sub Value_v
854             {
855 60     60   176 return decimalHex($_[0], 4);
856             }
857              
858             sub unpackValue_V
859             {
860 0     0   0 Value_V(unpack "V", $_[0]);
861             }
862              
863             sub Value_V
864             {
865 48   50 48   173 return decimalHex($_[0] // 0, 8);
866             }
867              
868             sub unpackValue_Q
869             {
870 0     0   0 my $v = unpack ("Q<", $_[0]);
871 0         0 Value_Q($v);
872             }
873              
874             sub Value_Q
875             {
876 0     0   0 return decimalHex($_[0], 16);
877             }
878              
879             sub read_Q
880             {
881 0     0   0 my $b ;
882 0         0 myRead($b, 8);
883 0         0 return ($b, unpack ("Q<" , $b));
884             }
885              
886             sub read_V
887             {
888 48     48   77 my $b ;
889 48         168 myRead($b, 4);
890 48         194 return ($b, unpack ("V", $b));
891             }
892              
893             sub read_v
894             {
895 48     48   92 my $b ;
896 48         123 myRead($b, 2);
897 48         381 return ($b, unpack "v", $b);
898             }
899              
900              
901             sub read_C
902             {
903 0     0   0 my $b ;
904 0         0 myRead($b, 1);
905 0         0 return ($b, unpack "C", $b);
906             }
907              
908             sub seekTo
909             {
910 84     84   129 my $offset = shift ;
911 84         223 my $loc = shift ;
912              
913 84 50       197 $loc = SEEK_SET
914             if ! defined $loc ;
915              
916 84         327 $FH->seek($offset, $loc);
917 84         1034 $OFFSET = $FH->tell();
918             }
919              
920             sub rewindRelative
921             {
922 0     0   0 my $offset = shift ;
923              
924 0         0 $FH->seek(-$offset, SEEK_CUR);
925             # $OFFSET -= $offset;
926 0         0 $OFFSET = $FH->tell();
927             }
928              
929             sub deltaToNextSignature
930             {
931 0     0   0 my $start = $FH->tell();
932              
933 0         0 my $got = scanForSignature(1);
934              
935 0         0 my $delta = $FH->tell() - $start ;
936 0         0 seekTo($start);
937              
938 0 0       0 if ($got)
939             {
940 0         0 return $delta ;
941             }
942              
943 0         0 return 0 ;
944             }
945              
946             sub scanForSignature
947             {
948 26   100 26   181 my $walk = shift // 0;
949              
950             # $count is only used to when 'walk' is enabled.
951             # Want to scan for a PK header at the start of the file.
952             # All other PK headers are should be directly after the previous PK record.
953 26         46 state $count = 0;
954 26         40 $count += $walk;
955              
956 26         71 my %sigs = Signatures::getSigsForScan();
957              
958 26         155 my $start = $FH->tell();
959              
960             # TODO -- Fix this?
961 26         172 if (1 || $count <= 1) {
962              
963 26         45 my $last = '';
964 26         39 my $offset = 0;
965 26         57 my $buffer ;
966              
967             BUFFER:
968 26         116 while ($FH->read($buffer, 1024 * 1000))
969             {
970 24         791 my $combine = $last . $buffer ;
971              
972 24         42 my $ix = 0;
973 24         40 while (1)
974             {
975 24         56 $ix = index($combine, "PK", $ix) ;
976              
977 24 100       68 if ($ix == -1)
978             {
979 2         7 $last = '';
980 2         9 next BUFFER;
981             }
982              
983 22         60 my $rest = substr($combine, $ix + 2, 2);
984              
985 22 50       75 if (! $sigs{$rest})
986             {
987 0         0 $ix += 2;
988 0         0 next;
989             }
990              
991             # possible match
992 22         70 my $here = $FH->tell();
993 22         151 seekTo($here - length($combine) + $ix);
994              
995 22         164 my $name = Signatures::name($sigs{$rest});
996 22         189 return $sigs{$rest};
997             }
998              
999 0         0 $last = substr($combine, $ix+4);
1000             }
1001             }
1002             else {
1003             die "FIX THIS";
1004             return ! $FH->eof();
1005             }
1006              
1007             # printf("scanForSignature %X\t%X (%X)\t%s\n", $start, $FH->tell(), $FH->tell() - $start, 'NO MATCH') ;
1008              
1009 4         56 return 0;
1010             }
1011              
1012 6         21 my $is64In32 = 0;
1013              
1014 6         33 my $opt_verbose = 0;
1015 6         15 my $opt_scan = 0;
1016 6         12 my $opt_walk = 0;
1017 6         12 my $opt_Redact = 0;
1018 6         14 my $opt_utc = 0;
1019 6         12 my $opt_want_info_messages = 1;
1020 6         11 my $opt_want_warning_messages = 1;
1021 6         13 my $opt_want_error_messages = 1;
1022 6         12 my $opt_want_message_exit_status = 0;
1023 6         14 my $exit_status_code = 0;
1024 6         12 my $opt_help =0;
1025              
1026 6         15 $Getopt::Long::bundling = 1 ;
1027              
1028 6         28 TextEncoding::setDefaults();
1029              
1030             GetOptions("h|help" => \$opt_help,
1031             "v" => \$opt_verbose,
1032             "scan" => \$opt_scan,
1033             "walk" => \$opt_walk,
1034             "redact" => \$opt_Redact,
1035             "utc" => \$opt_utc,
1036 0     0   0 "version" => sub { print "$VERSION\n"; exit },
  0         0  
1037              
1038             # Filename/comment encoding
1039             "encoding=s" => \&TextEncoding::parseEncodingOption,
1040             "no-encoding" => \&TextEncoding::NoEncoding,
1041             "debug-encoding" => \&TextEncoding::debugEncoding,
1042             "output-encoding=s" => \&TextEncoding::parseEncodingOption,
1043             "language-encoding!" => \&TextEncoding::LanguageEncodingFlag,
1044              
1045             # Message control
1046             "exit-bitmask!" => \$opt_want_message_exit_status,
1047             "messages!" => sub {
1048 0     0   0 my ($opt_name, $opt_value) = @_;
1049 0         0 $opt_want_info_messages =
1050             $opt_want_warning_messages =
1051             $opt_want_error_messages = $opt_value;
1052             },
1053             )
1054 6 50       132 or exit 255 ;
1055              
1056 6 50       520 Usage()
1057             if $opt_help;
1058              
1059 6 50       23 die("No zipfile\n")
1060             unless @ARGV == 1;
1061              
1062 6 50 66     33 die("Cannot specify both '--walk' and '--scan'\n")
1063             if $opt_walk && $opt_scan ;
1064              
1065 6         18 my $filename = shift @ARGV;
1066              
1067 6 50       174 topLevelFatal "No such file"
1068             unless -e $filename ;
1069              
1070 6 50       73 topLevelFatal "'$filename' is a directory"
1071             if -d $filename ;
1072              
1073 6 50       59 topLevelFatal "'$filename' is not a standard file"
1074             unless -f $filename ;
1075              
1076 6 50       75 $FH = IO::File->new( "<$filename" )
1077             or topLevelFatal "Cannot open '$filename': $!";
1078 6         715 binmode($FH);
1079              
1080 6         123 displayFileInfo($filename);
1081 6         25 TextEncoding::encodingInfo();
1082              
1083 6         76 my $FILELEN = -s $filename ;
1084 6         50 $TRAILING = -s $filename ;
1085 6         64 $NIBBLES = nibbles(-s $filename) ;
1086              
1087 6 50       20 topLevelFatal "'$filename' is empty"
1088             if $FILELEN == 0 ;
1089              
1090 6 50       20 topLevelFatal "file is too short to be a zip file"
1091             if $FILELEN < ZIP_EOCD_MIN_SIZE ;
1092              
1093 6         53 setupFormat($opt_verbose, $NIBBLES);
1094              
1095 6         18 my @Messages = ();
1096              
1097 6 100 100     40 if ($opt_scan || $opt_walk)
1098             {
1099             # Main loop for walk/scan processing
1100              
1101 4         9 my $foundZipRecords = 0;
1102 4         9 my $foundCentralHeader = 0;
1103 4         7 my $lastEndsAt = 0;
1104 4         22 my $lastSignature = 0;
1105 4         10 my $lastHeader = {};
1106              
1107 4         38 $CentralDirectory->{alreadyScanned} = 1 ;
1108              
1109 4         9 my $output_encryptedCD = 0;
1110              
1111 4         21 reportPrefixData();
1112 4         15 while(my $s = scanForSignature($opt_walk))
1113             {
1114 20         66 my $here = $FH->tell();
1115 20         95 my $delta = $here - $lastEndsAt ;
1116              
1117             # delta can only be negative when '--scan' is used
1118 20 50       80 if ($delta < 0 )
    50          
1119             {
1120             # nested or overlap
1121             # check if nested
1122             # remember & check if matching entry in CD
1123             # printf("### WARNING: OVERLAP/NESTED Record found 0x%X 0x%X $delta\n", $here, $lastEndsAt) ;
1124             }
1125             elsif ($here != $lastEndsAt)
1126             {
1127             # scanForSignature had to skip bytes to find the next signature
1128              
1129             # some special cases that don't have signatures need to be checked first
1130              
1131 0         0 seekTo($lastEndsAt);
1132              
1133 0 0 0     0 if (! $output_encryptedCD && $CentralDirectory->isEncryptedCD())
    0 0        
    0 0        
1134             {
1135 0         0 displayEncryptedCD();
1136 0         0 $output_encryptedCD = 1;
1137 0         0 $lastEndsAt = $FH->tell();
1138 0         0 next;
1139             }
1140             elsif ($lastSignature == ZIP_LOCAL_HDR_SIG && $lastHeader->{'streamed'} )
1141             {
1142             # Check for size of possible malformed Data Descriptor before outputting payload
1143 0 0       0 if (! $lastHeader->{'gotDataDescriptorSize'})
1144             {
1145 0         0 my $hdrSize = checkForBadlyFormedDataDescriptor($lastHeader, $delta) ;
1146              
1147 0 0       0 if ($hdrSize)
1148             {
1149             # remove size of Data Descriptor from payload
1150 0         0 $delta -= $hdrSize;
1151 0         0 $lastHeader->{'gotDataDescriptorSize'} = $hdrSize;
1152             }
1153             }
1154              
1155 0 0 0     0 if(defined($lastHeader->{'payloadOutput'}) && ($lastEndsAt = BadlyFormedDataDescriptor($lastHeader, $delta)))
1156             {
1157 0         0 $HeaderOffsetIndex->rewindIndex();
1158 0         0 $lastHeader->{entry}->readDataDescriptor(1) ;
1159 0         0 next;
1160             }
1161              
1162             # Assume we have the payload when streaming is enabled
1163 0         0 outSomeData($delta, "PAYLOAD", $opt_Redact) ;
1164 0         0 $lastHeader->{'payloadOutput'} = 1;
1165 0         0 $lastEndsAt = $FH->tell();
1166              
1167 0         0 next;
1168             }
1169             elsif (Signatures::isCentralHeader($s) && $foundCentralHeader == 0)
1170             {
1171             # check for an APK header directly before the first Central Header
1172 0         0 $foundCentralHeader = 1;
1173              
1174 0         0 ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here, 0) ;
1175              
1176 0 0       0 if ($START_APK)
1177             {
1178 0         0 seekTo($lastEndsAt+4);
1179              
1180 0         0 scanApkBlock();
1181 0         0 $lastEndsAt = $FH->tell();
1182 0         0 next;
1183             }
1184              
1185 0         0 seekTo($lastEndsAt);
1186             }
1187              
1188             # Not a special case, so output generic padding message
1189 0 0       0 if ($delta > 0)
1190             {
1191 0 0       0 reportPrefixData($delta)
1192             if $lastEndsAt == 0 ;
1193 0         0 outSomeDataParagraph($delta, "UNEXPECTED PADDING");
1194 0 0       0 info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
1195             if $FH->tell() - $delta ;
1196 0 0       0 $POSSIBLE_PREFIX_DELTA = $delta
1197             if $lastEndsAt == 0;
1198 0         0 $lastEndsAt = $FH->tell();
1199 0         0 next;
1200             }
1201             else
1202             {
1203 0         0 seekTo($here);
1204             }
1205              
1206             }
1207              
1208 20         49 my ($buffer, $signature) = read_V();
1209              
1210 20         45 $lastSignature = $signature;
1211              
1212 20         52 my $handler = Signatures::decoder($signature);
1213 20 50       56 if (!defined $handler) {
1214 0         0 internalFatal undef, "xxx";
1215             }
1216              
1217 20         28 $foundZipRecords = 1;
1218 20   50     58 $lastHeader = $handler->($signature, $buffer, $FH->tell() - 4) // {'streamed' => 0};
1219              
1220 20         200 $lastEndsAt = $FH->tell();
1221              
1222 20 100       189 seekTo($here + 4)
1223             if $opt_scan;
1224             }
1225              
1226 4 50       19 topLevelFatal "'$filename' is not a zip file"
1227             unless $foundZipRecords ;
1228              
1229             }
1230             else
1231             {
1232             # Main loop for non-walk/scan processing
1233              
1234             # check for prefix data
1235 2         10 my $s = scanForSignature();
1236 2 50 33     12 if ($s && $FH->tell() != 0)
1237             {
1238 0         0 $POSSIBLE_PREFIX_DELTA = $FH->tell();
1239             }
1240              
1241 2         20 seekTo(0);
1242              
1243 2         16 scanCentralDirectory($FH);
1244              
1245 2 0 33     32 fatal_tryWalk undef, "No Zip metadata found at end of file"
1246             if ! $CentralDirectory->exists() && ! $EOCD_Present ;
1247              
1248 2         6 $CentralDirectory->{alreadyScanned} = 1 ;
1249              
1250 2         7 Nesting::clearStack();
1251              
1252             # $HeaderOffsetIndex->dump();
1253              
1254 2         3 $OFFSET = 0 ;
1255 2         20 $FH->seek(0, SEEK_SET) ;
1256              
1257 2         15 my $expectedOffset = 0;
1258 2         4 my $expectedSignature = 0;
1259 2         4 my $expectedBuffer = 0;
1260 2         4 my $foundCentralHeader = 0;
1261 2         4 my $processedAPK = 0;
1262 2         3 my $processedECD = 0;
1263 2         4 my $lastHeader ;
1264              
1265             # my $lastWasLocalHeader = 0;
1266             # my $inCentralHeader = 0;
1267              
1268 2         8 while (1)
1269             {
1270 12 100       49 last if $FH->eof();
1271              
1272 10         89 my $here = $FH->tell();
1273              
1274 10 50       49 if ($here >= $TRAILING) {
1275 0         0 my $delta = $FILELEN - $TRAILING;
1276 0         0 outSomeDataParagraph($delta, "TRAILING DATA");
1277 0         0 info $FH->tell(), "Unexpected Trailing Data: " . decimalHex0x($delta) . " bytes";
1278              
1279 0         0 last;
1280             }
1281              
1282 10         25 my ($buffer, $signature) = read_V();
1283              
1284 10         19 $expectedOffset = undef;
1285 10         15 $expectedSignature = undef;
1286              
1287             # Check for split archive marker at start of file
1288 10 50 66     34 if ($here == 0 && $signature == ZIP_SINGLE_SEGMENT_MARKER)
1289             {
1290             # let it drop through
1291 0         0 $expectedSignature = ZIP_SINGLE_SEGMENT_MARKER;
1292 0         0 $expectedOffset = 0;
1293             }
1294             else
1295             {
1296 10         28 my $expectedEntry = $HeaderOffsetIndex->getNextIndex() ;
1297 10 50       24 if ($expectedEntry)
1298             {
1299 10         21 $expectedOffset = $expectedEntry->offset();
1300 10         22 $expectedSignature = $expectedEntry->signature();
1301 10         34 $expectedBuffer = pack "V", $expectedSignature ;
1302             }
1303             }
1304              
1305 10         17 my $delta = $expectedOffset - $here ;
1306              
1307             # if ($here != $expectedOffset && $signature != ZIP_DATA_HDR_SIG)
1308             # {
1309             # rewindRelative(4);
1310             # my $delta = $expectedOffset - $here ;
1311             # outSomeDataParagraph($delta, "UNEXPECTED PADDING");
1312             # $HeaderOffsetIndex->rewindIndex();
1313             # next;
1314             # }
1315              
1316             # Need to check for use-case where
1317             # * there is a ZIP_DATA_HDR_SIG directly after a ZIP_LOCAL_HDR_SIG.
1318             # The HeaderOffsetIndex object doesn't have visibility of it.
1319             # * APK header directly before the CD
1320             # * zipbomb
1321              
1322 10 0 33     45 if (defined $expectedOffset && $here != $expectedOffset && ( $CentralDirectory->exists() || $EOCD_Present) )
      0        
      33        
1323             {
1324 0 0       0 if ($here > $expectedOffset)
1325             {
1326             # Probable zipbomb
1327              
1328             # Cursor $OFFSET need to rewind
1329 0         0 $OFFSET = $expectedOffset;
1330 0         0 $FH->seek($OFFSET + 4, SEEK_SET) ;
1331              
1332 0         0 $signature = $expectedSignature;
1333 0         0 $buffer = $expectedBuffer ;
1334             }
1335              
1336             # If get here then $here is less than $expectedOffset
1337              
1338              
1339             # check for an APK header directly before the first Central Header
1340             # Make sure not to miss a streaming data descriptor
1341 0 0 0     0 if ($signature != ZIP_DATA_HDR_SIG && Signatures::isCentralHeader($expectedSignature) && $START_APK && ! $processedAPK )
      0        
      0        
1342             {
1343 0         0 seekTo($here+4);
1344             # rewindRelative(4);
1345 0         0 scanApkBlock();
1346 0         0 $HeaderOffsetIndex->rewindIndex();
1347 0         0 $processedAPK = 1;
1348 0         0 next;
1349             }
1350              
1351             # Check Encrypted Central Directory
1352             # if ($CentralHeaderSignatures{$expectedSignature} && $CentralDirectory->isEncryptedCD() && ! $processedECD)
1353             # {
1354             # # rewind the invalid signature
1355             # seekTo($here);
1356             # # rewindRelative(4);
1357             # displayEncryptedCD();
1358             # $processedECD = 1;
1359             # next;
1360             # }
1361              
1362 0 0 0     0 if ($signature != ZIP_DATA_HDR_SIG && $delta >= 0)
1363             {
1364 0         0 rewindRelative(4);
1365 0 0 0     0 if($lastHeader->{'streamed'} && BadlyFormedDataDescriptor($lastHeader, $delta))
1366             {
1367 0         0 $lastHeader->{entry}->readDataDescriptor(1) ;
1368 0         0 $HeaderOffsetIndex->rewindIndex();
1369 0         0 next;
1370             }
1371              
1372 0 0       0 reportPrefixData($delta)
1373             if $here == 0;
1374 0         0 outSomeDataParagraph($delta, "UNEXPECTED PADDING");
1375 0 0       0 info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes"
1376             if $FH->tell() - $delta ;
1377 0         0 $HeaderOffsetIndex->rewindIndex();
1378 0         0 next;
1379             }
1380              
1381             # ZIP_DATA_HDR_SIG drops through
1382             }
1383              
1384 10         23 my $handler = Signatures::decoder($signature);
1385              
1386 10 50       20 if (!defined $handler)
1387             {
1388             # if ($CentralDirectory->exists()) {
1389              
1390             # # Should be at offset that central directory says
1391             # my $locOffset = $CentralDirectory->getNextLocalOffset();
1392             # my $delta = $locOffset - $here ;
1393              
1394             # if ($here + 4 == $locOffset ) {
1395             # for (0 .. 3) {
1396             # $FH->ungetc(ord(substr($buffer, $_, 1)))
1397             # }
1398             # outSomeData($delta, "UNEXPECTED PADDING");
1399             # next;
1400             # }
1401             # }
1402              
1403              
1404             # if ($here == $CentralDirectory->{CentralDirectoryOffset} && $EOCD_Present && $CentralDirectory->isEncryptedCD())
1405             # {
1406             # # rewind the invalid signature
1407             # rewindRelative(4);
1408             # displayEncryptedCD();
1409             # next;
1410             # }
1411             # elsif ($here < $CentralDirectory->{CentralDirectoryOffset})
1412             # {
1413             # # next
1414             # # if scanForSignature() ;
1415              
1416             # my $skippedFrom = $FH->tell() ;
1417             # my $skippedContent = $CentralDirectory->{CentralDirectoryOffset} - $skippedFrom ;
1418              
1419             # printf "\nWARNING!\nExpected Zip header not found at offset 0x%X\n", $here;
1420             # printf "Skipping 0x%X bytes to Central Directory...\n", $skippedContent;
1421              
1422             # push @Messages,
1423             # sprintf("Expected Zip header not found at offset 0x%X, ", $skippedFrom) .
1424             # sprintf("skipped 0x%X bytes\n", $skippedContent);
1425              
1426             # seekTo($CentralDirectory->{CentralDirectoryOffset});
1427              
1428             # next;
1429             # }
1430             # else
1431             {
1432 0         0 fatal $here, sprintf "Unexpected Zip Signature '%s' at offset %s", Value_V($signature), decimalHex0x($here) ;
  0         0  
1433 0         0 last;
1434             }
1435             }
1436              
1437 10 50       22 $ZIP64 = 0 if $signature != ZIP_DATA_HDR_SIG ;
1438 10         23 $lastHeader = $handler->($signature, $buffer, $FH->tell() - 4);
1439             # $lastWasLocalHeader = $signature == ZIP_LOCAL_HDR_SIG ;
1440 10 50       75 $HeaderOffsetIndex->rewindIndex()
1441             if $signature == ZIP_DATA_HDR_SIG ;
1442             }
1443             }
1444              
1445              
1446 6 50       71 displayMessages()
1447             if $opt_want_error_messages ;
1448              
1449 6         0 exit $exit_status_code ;
1450              
1451             sub displayMessages
1452             {
1453              
1454             # Compare Central & Local for discrepancies
1455              
1456 6 50 33 6   28 if ($CentralDirectory->isMiniZipEncrypted)
    50          
    0          
    0          
1457             {
1458             # don't compare local & central entries when minizip-ng encryption is in play
1459 0         0 info undef, "Zip file uses minizip-ng central directory encryption"
1460             }
1461              
1462             elsif ($CentralDirectory->exists() && $LocalDirectory->exists())
1463             {
1464             # TODO check number of entries matches eocd
1465             # TODO check header length matches reality
1466              
1467             # Nesting::dump();
1468              
1469 6         27 $LocalDirectory->sortByLocalOffset();
1470 6         12 my %cleanCentralEntries = %{ $CentralDirectory->{byCentralOffset} };
  6         35  
1471              
1472 6 50       28 if ($NESTING_DEBUG)
1473             {
1474 0 0       0 if (Nesting::encapsulationCount())
1475             {
1476 0         0 say "# ENCAPSULATIONS";
1477              
1478 0         0 for my $index (sort { $a <=> $b } keys %{ Nesting::encapsulations() })
  0         0  
  0         0  
1479             {
1480 0         0 my $outer = Nesting::entryByIndex($index) ;
1481              
1482 0         0 say "# Nesting " . $outer->outputFilename . " " . $outer->offsetStart . " " . $outer->offsetEnd ;
1483              
1484 0         0 for my $inner (sort { $a <=> $b } @{ Nesting::encapsulations()->{$index} } )
  0         0  
  0         0  
1485             {
1486 0         0 say "# " . $inner->outputFilename . " " . $inner->offsetStart . " " . $inner->offsetEnd ;;
1487             }
1488             }
1489             }
1490             }
1491              
1492             {
1493             # check for Local Directory orphans
1494              
1495 0         0 my %orphans = map { $_->localHeaderOffset => $_->outputFilename }
1496             grep { $_->entryType == ZIP_LOCAL_HDR_SIG && # Want Local Headers
1497             ! $_->encapsulated &&
1498 24 100 66     83 @{ $_->getCdEntries } == 0
  12         32  
1499             }
1500 6         12 values %{ Nesting::getEntriesByOffset() };
  6         20  
1501              
1502              
1503 6 50       30 if (keys %orphans)
1504             {
1505 0         0 error undef, "Orphan Local Headers found: " . scalar(keys %orphans) ;
1506              
1507 0         0 my $table = new SimpleTable;
1508 0         0 $table->addHeaderRow('Offset', 'Filename');
1509             $table->addDataRow(decimalHex0x($_), $orphans{$_})
1510 0         0 for sort { $a <=> $b } keys %orphans ;
  0         0  
1511              
1512 0         0 $table->display();
1513             }
1514             }
1515              
1516             {
1517             # check for Central Directory orphans
1518             # probably only an issue with --walk & a zipbomb
1519              
1520 6         16 my %orphans = map { $_->centralHeaderOffset => $_ }
  6         11  
  0         0  
1521 24 50 66     48 grep { $_->entryType == ZIP_CENTRAL_HDR_SIG # Want Central Headers
1522             && ! $_->ldEntry # Filter out orphans
1523             && ! $_->encapsulated # Not encapsulated
1524             }
1525 6         14 values %{ Nesting::getEntriesByOffset() };
  6         19  
1526              
1527 6 50       34 if (keys %orphans)
1528             {
1529 0         0 error undef, "Possible zipbomb -- Orphan Central Headers found: " . scalar(keys %orphans) ;
1530              
1531 0         0 my $table = new SimpleTable;
1532 0         0 $table->addHeaderRow('Offset', 'Filename');
1533 0         0 for (sort { $a <=> $b } keys %orphans )
  0         0  
1534             {
1535 0         0 $table->addDataRow(decimalHex0x($_), $orphans{$_}{filename});
1536 0         0 delete $cleanCentralEntries{ $_ };
1537             }
1538              
1539 0         0 $table->display();
1540             }
1541             }
1542              
1543 6 50       22 if (Nesting::encapsulationCount())
1544             {
1545             # Benign Nested zips
1546             # This is the use-case where a zip file is "stored" in another zip file.
1547             # NOT a zipbomb -- want the benign nested entries
1548              
1549             # Note: this is only active when scan is used
1550              
1551 0         0 my %outerEntries = map { $_->localHeaderOffset => $_->outputFilename }
1552             grep {
1553 0 0 0     0 $_->entryType == ZIP_CENTRAL_HDR_SIG &&
      0        
      0        
1554             ! $_->encapsulated && # not encapsulated
1555             $_->ldEntry && # Central Header has a local sibling
1556             $_->ldEntry->childrenCount && # local entry has embedded entries
1557             ! Nesting::childrenInCentralDir($_->ldEntry)
1558             }
1559 0         0 values %{ Nesting::getEntriesByOffset() };
  0         0  
1560              
1561 0 0       0 if (keys %outerEntries)
1562             {
1563 0         0 my $count = scalar keys %outerEntries;
1564 0         0 info undef, "Nested Zip files found: $count";
1565              
1566 0         0 my $table = new SimpleTable;
1567 0         0 $table->addHeaderRow('Offset', 'Filename');
1568             $table->addDataRow(decimalHex0x($_), $outerEntries{$_})
1569 0         0 for sort { $a <=> $b } keys %outerEntries ;
  0         0  
1570              
1571 0         0 $table->display();
1572             }
1573             }
1574              
1575 6 50       32 if ($LocalDirectory->anyStreamedEntries)
1576             {
1577             # Check for a missing Data Descriptors
1578              
1579 0         0 my %missingDataDescriptor = map { $_->localHeaderOffset => $_->outputFilename }
1580 0 0 0     0 grep { $_->entryType == ZIP_LOCAL_HDR_SIG &&
1581             $_->streamed &&
1582             ! $_->readDataDescriptor
1583             }
1584 0         0 values %{ Nesting::getEntriesByOffset() };
  0         0  
1585              
1586              
1587 0         0 for my $offset (sort keys %missingDataDescriptor)
1588             {
1589 0         0 my $filename = $missingDataDescriptor{$offset};
1590 0         0 error $offset, "Filename '$filename': Missing 'Data Descriptor'" ;
1591             }
1592             }
1593              
1594             {
1595             # compare local & central for duplicate entries (CD entries point to same local header)
1596              
1597 6         22 my %ByLocalOffset = map { $_->localHeaderOffset => $_ }
  0         0  
1598             grep {
1599             $_->entryType == ZIP_LOCAL_HDR_SIG # Want Local Headers
1600             && ! $_->encapsulated # Not encapsulated
1601 24 100 66     48 && @{ $_->getCdEntries } > 1
  12         29  
1602             }
1603 6         13 values %{ Nesting::getEntriesByOffset() };
  6         36  
1604              
1605 6         100 for my $offset (sort keys %ByLocalOffset)
1606             {
1607 0         0 my @entries = @{ $ByLocalOffset{$offset}->getCdEntries };
  0         0  
1608 0 0       0 if (@entries > 1)
1609             {
1610             # found duplicates
1611 0         0 my $localEntry = $LocalDirectory->getByLocalOffset($offset) ;
1612 0 0       0 if ($localEntry)
1613             {
1614 0         0 error undef, "Possible zipbomb -- Duplicate Central Headers referring to one Local header for '" . $localEntry->outputFilename . "' at offset " . decimalHex0x($offset);
1615             }
1616             else
1617             {
1618 0         0 error undef, "Possible zipbomb -- Duplicate Central Headers referring to one Local header at offset " . decimalHex0x($offset);
1619             }
1620              
1621 0         0 my $table = new SimpleTable;
1622 0         0 $table->addHeaderRow('Offset', 'Filename');
1623 0         0 for (sort { $a->centralHeaderOffset <=> $b->centralHeaderOffset } @entries)
  0         0  
1624             {
1625 0         0 $table->addDataRow(decimalHex0x($_->centralHeaderOffset), $_->outputFilename);
1626 0         0 delete $cleanCentralEntries{ $_->centralHeaderOffset };
1627             }
1628              
1629 0         0 $table->display();
1630             }
1631             }
1632             }
1633              
1634 6 50       22 if (Nesting::encapsulationCount())
1635             {
1636             # compare local & central for nested entries
1637              
1638             # get the local offsets referenced in the CD
1639             # this deliberately ignores any valid nested local entries
1640 0         0 my @localOffsets = sort { $a <=> $b } keys %{ $CentralDirectory->{byLocalOffset} };
  0         0  
  0         0  
1641              
1642             # now check for nesting
1643              
1644 0         0 my %nested ;
1645             my %bomb;
1646              
1647 0         0 for my $offset (@localOffsets)
1648             {
1649 0         0 my $innerEntry = $LocalDirectory->{byLocalOffset}{$offset};
1650 0 0       0 if ($innerEntry)
1651             {
1652 0         0 my $outerLocalEntry = Nesting::getOuterEncapsulation($innerEntry);
1653 0 0       0 if (defined $outerLocalEntry)
1654             {
1655 0         0 my $outerOffset = $outerLocalEntry->localHeaderOffset();
1656 0 0       0 if ($CentralDirectory->{byLocalOffset}{ $offset })
1657             {
1658 0         0 push @{ $bomb{ $outerOffset } }, $offset ;
  0         0  
1659             }
1660             else
1661             {
1662 0         0 push @{ $nested{ $outerOffset } }, $offset ;
  0         0  
1663             }
1664             }
1665             }
1666             }
1667              
1668 0 0       0 if (keys %nested)
1669             {
1670             # The real central directory at eof does not know about these.
1671             # likely to be a zip file stored in another zip file
1672 0         0 warning undef, "Nested Local Entries found";
1673 0         0 for my $loc (sort keys %nested)
1674             {
1675 0         0 my $count = scalar @{ $nested{$loc} };
  0         0  
1676 0         0 my $outerEntry = $LocalDirectory->getByLocalOffset($loc);
1677 0         0 say "Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) . " has $count nested Local Headers";
1678 0         0 for my $n ( @{ $nested{$loc} } )
  0         0  
1679             {
1680 0         0 my $innerEntry = $LocalDirectory->getByLocalOffset($n);
1681              
1682 0         0 say "# Nested Local Header for filename '" . $innerEntry->outputFilename . "' is at Offset " . decimalHex0x($n) ;
1683             }
1684             }
1685             }
1686              
1687 0 0       0 if (keys %bomb)
1688             {
1689             # Central Directory knows about these, so this is a zipbomb
1690              
1691 0         0 error undef, "Possible zipbomb -- Nested Local Entries found";
1692 0         0 for my $loc (sort keys %bomb)
1693             {
1694 0         0 my $count = scalar @{ $bomb{$loc} };
  0         0  
1695 0         0 my $outerEntry = $LocalDirectory->getByLocalOffset($loc);
1696 0         0 say "# Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) . " has $count nested Local Headers";
1697              
1698 0         0 my $table = new SimpleTable;
1699 0         0 $table->addHeaderRow('Offset', 'Filename');
1700             $table->addDataRow(decimalHex0x($_), $LocalDirectory->getByLocalOffset($_)->outputFilename)
1701 0         0 for sort @{ $bomb{$loc} } ;
  0         0  
1702              
1703 0         0 $table->display();
1704              
1705             delete $cleanCentralEntries{ $_ }
1706 0         0 for grep { defined $_ }
  0         0  
1707 0         0 map { $CentralDirectory->{byLocalOffset}{$_}{centralHeaderOffset} }
1708 0         0 @{ $bomb{$loc} } ;
1709             }
1710             }
1711             }
1712              
1713             # Check if contents of local headers match with Central Headers
1714             #
1715             # When Central Header encryption is used the local header values are masked (see APPNOTE 6.3.10, sec 4)
1716             # In this usecase the Central Header will appear to be absent
1717             #
1718             # key fields
1719             # filename, compressed/uncompressed lengths, crc, compression method
1720             {
1721 6         12 for my $centralEntry ( sort { $a->centralHeaderOffset() <=> $b->centralHeaderOffset() } values %cleanCentralEntries )
  6         35  
  6         34  
1722             {
1723 12         31 my $localOffset = $centralEntry->localHeaderOffset;
1724 12         44 my $localEntry = $LocalDirectory->getByLocalOffset($localOffset);
1725              
1726             next
1727 12 50       52 unless $localEntry;
1728              
1729             state $fields = [
1730             # field name offset display name stringify
1731             ['filename', ZIP_CD_FILENAME_OFFSET,
1732             'Filename', undef, ],
1733 0     0   0 ['extractVersion', 7, 'Extract Zip Spec', sub { decimalHex0xUndef($_[0]) . " " . decodeZipVer($_[0]) }, ],
1734             ['generalPurposeFlags', 8, 'General Purpose Flag', \&decimalHex0xUndef, ],
1735 0     0   0 ['compressedMethod', 10, 'Compression Method', sub { decimalHex0xUndef($_[0]) . " " . getcompressionMethodName($_[0]) }, ],
1736 12     0   196 ['lastModDateTime', 12, 'Modification Time', sub { decimalHex0xUndef($_[0]) . " " . LastModTime($_[0]) }, ],
  0         0  
1737             ['crc32', 16, 'CRC32', \&decimalHex0xUndef, ],
1738             ['compressedSize', 20, 'Compressed Size', \&decimalHex0xUndef, ],
1739             ['uncompressedSize', 24, 'Uncompressed Size', \&decimalHex0xUndef, ],
1740              
1741             ] ;
1742              
1743 12         67 my $table = new SimpleTable;
1744 12         44 $table->addHeaderRow('Field Name', 'Central Offset', 'Central Value', 'Local Offset', 'Local Value');
1745              
1746 12         27 for my $data (@$fields)
1747             {
1748 96         177 my ($field, $offset, $name, $stringify) = @$data;
1749             # if the local header uses streaming and we are running a scan/walk, the compressed/uncompressed sizes will not be known
1750 96         172 my $localValue = $localEntry->{$field} ;
1751 96         160 my $centralValue = $centralEntry->{$field};
1752              
1753 96 50 50     345 if (($localValue // '-1') ne ($centralValue // '-2'))
      50        
1754             {
1755 0 0       0 if ($stringify)
1756             {
1757 0         0 $localValue = $stringify->($localValue);
1758 0         0 $centralValue = $stringify->($centralValue);
1759             }
1760              
1761 0         0 $table->addDataRow($name,
1762             decimalHex0xUndef($centralEntry->centralHeaderOffset() + $offset),
1763             $centralValue,
1764             decimalHex0xUndef($localOffset+$offset),
1765             $localValue);
1766             }
1767             }
1768              
1769 12         48 my $badFields = $table->hasData;
1770 12 50       111 if ($badFields)
1771             {
1772 0         0 error undef, "Found $badFields Field Mismatch for Filename '". $centralEntry->outputFilename . "'";
1773 0         0 $table->display();
1774             }
1775             }
1776             }
1777              
1778             }
1779             elsif ($CentralDirectory->exists())
1780             {
1781 0         0 my @messages = "Central Directory exists, but Local Directory not found" ;
1782 0 0 0     0 push @messages , "Try running with --walk' or '--scan' options"
1783             unless $opt_scan || $opt_walk ;
1784 0         0 error undef, @messages;
1785             }
1786             elsif ($LocalDirectory->exists())
1787             {
1788 0 0       0 if ($CentralDirectory->isEncryptedCD())
1789             {
1790 0         0 warning undef, "Local Directory exists, but Central Directory is encrypted"
1791             }
1792             else
1793             {
1794 0         0 error undef, "Local Directory exists, but Central Directory not found"
1795             }
1796              
1797             }
1798              
1799 6 50 33     53 if ($ErrorCount ||$WarningCount || $InfoCount )
      33        
1800             {
1801 0 0       0 say "#"
1802             unless $lastWasMessage ;
1803              
1804 0 0       0 say "# Error Count: $ErrorCount"
1805             if $ErrorCount;
1806 0 0       0 say "# Warning Count: $WarningCount"
1807             if $WarningCount;
1808 0 0       0 say "# Info Count: $InfoCount"
1809             if $InfoCount;
1810             }
1811              
1812 6 50       23 if (@Messages)
1813             {
1814 0         0 my $count = scalar @Messages ;
1815 0         0 say "#\nWARNINGS";
1816 0         0 say "# * $_\n" for @Messages ;
1817             }
1818              
1819 6         221 say "#\n# Done";
1820             }
1821              
1822             sub checkForBadlyFormedDataDescriptor
1823             {
1824 0     0   0 my $lastHeader = shift;
1825 0   0     0 my $delta = shift // 0;
1826              
1827             # check size of delta - a DATA HDR without a signature can only be
1828             # 12 bytes for 32-bit
1829             # 20 bytes for 64-bit
1830              
1831 0         0 my $here = $FH->tell();
1832              
1833 0         0 my $localEntry = $lastHeader->{entry};
1834              
1835 0 0 0     0 return 0
1836             unless $opt_scan || $opt_walk ;
1837              
1838             # delta can be the actual payload + a data descriptor without a sig
1839              
1840 0         0 my $signature = unpack "V", peekAtOffset($here + $delta, 4);
1841              
1842 0 0       0 if ($signature == ZIP_DATA_HDR_SIG)
1843             {
1844 0         0 return 0;
1845             }
1846              
1847 0         0 my $cl32 = unpack "V", peekAtOffset($here + $delta - 8, 4);
1848 0         0 my $cl64 = unpack "Q<", peekAtOffset($here + $delta - 16, 8);
1849              
1850 0 0       0 if ($cl32 == $delta - 12)
1851             {
1852 0         0 return 12;
1853             }
1854              
1855 0 0       0 if ($cl64 == $delta - 20)
1856             {
1857 0         0 return 20 ;
1858             }
1859              
1860 0         0 return 0;
1861             }
1862              
1863              
1864             sub BadlyFormedDataDescriptor
1865             {
1866 0     0   0 my $lastHeader= shift;
1867 0         0 my $delta = shift;
1868              
1869             # check size of delta - a DATA HDR without a signature can only be
1870             # 12 bytes for 32-bit
1871             # 20 bytes for 64-bit
1872              
1873 0         0 my $here = $FH->tell();
1874              
1875 0         0 my $localEntry = $lastHeader->{entry};
1876 0         0 my $compressedSize = $lastHeader->{payloadLength} ;
1877              
1878 0         0 my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG);
1879              
1880 0 0 0     0 if ($opt_scan || $opt_walk)
1881             {
1882             # delta can be the actual payload + a data descriptor without a sig
1883              
1884 0 0       0 if ($lastHeader->{'gotDataDescriptorSize'} == 12)
1885             {
1886             # seekTo($FH->tell() + $delta - 12) ;
1887              
1888             # outSomeData($delta - 12, "PAYLOAD", $opt_Redact) ;
1889              
1890 0         0 print "\n";
1891 0         0 out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG);
1892              
1893 0         0 error $FH->tell(), "Missing $sigName Signature";
1894 0         0 $localEntry->crc32( out_V "CRC");
1895 0         0 $localEntry->compressedSize( out_V "Compressed Size");
1896 0         0 $localEntry->uncompressedSize( out_V "Uncompressed Size");
1897              
1898 0 0       0 if ($localEntry->zip64)
1899             {
1900 0         0 error $here, "'$sigName': expected 64-bit values, got 32-bit";
1901             }
1902              
1903 0         0 return $FH->tell();
1904             }
1905              
1906 0 0       0 if ($lastHeader->{'gotDataDescriptorSize'} == 20)
1907             {
1908             # seekTo($FH->tell() + $delta - 20) ;
1909              
1910             # outSomeData($delta - 20, "PAYLOAD", $opt_Redact) ;
1911              
1912 0         0 print "\n";
1913 0         0 out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG);
1914              
1915 0         0 error $FH->tell(), "Missing $sigName Signature";
1916 0         0 $localEntry->crc32( out_V "CRC");
1917 0         0 $localEntry->compressedSize( out_Q "Compressed Size");
1918 0         0 $localEntry->uncompressedSize( out_Q "Uncompressed Size");
1919              
1920 0 0       0 if (! $localEntry->zip64)
1921             {
1922 0         0 error $here, "'$sigName': expected 32-bit values, got 64-bit";
1923             }
1924              
1925 0         0 return $FH->tell();
1926             }
1927              
1928 0         0 error 0, "MISSING $sigName";
1929              
1930 0         0 seekTo($here);
1931 0         0 return 0;
1932             }
1933              
1934 0         0 my $cdEntry = $localEntry->getCdEntry;
1935              
1936 0 0       0 if ($delta == 12)
1937             {
1938 0         0 $FH->seek($lastHeader->{payloadOffset} + $lastHeader->{payloadLength}, SEEK_SET) ;
1939              
1940 0         0 my $cl = unpack "V", peekAtOffset($FH->tell() + 4, 4);
1941 0 0       0 if ($cl == $compressedSize)
1942             {
1943 0         0 print "\n";
1944 0         0 out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG);
1945              
1946 0         0 error $FH->tell(), "Missing $sigName Signature";
1947 0         0 $localEntry->crc32( out_V "CRC");
1948 0         0 $localEntry->compressedSize( out_V "Compressed Size");
1949 0         0 $localEntry->uncompressedSize( out_V "Uncompressed Size");
1950              
1951 0 0       0 if ($localEntry->zip64)
1952             {
1953 0         0 error $here, "'$sigName': expected 64-bit values, got 32-bit";
1954             }
1955              
1956 0         0 return $FH->tell();
1957             }
1958             }
1959              
1960 0 0       0 if ($delta == 20)
1961             {
1962 0         0 $FH->seek($lastHeader->{payloadOffset} + $lastHeader->{payloadLength}, SEEK_SET) ;
1963              
1964 0         0 my $cl = unpack "Q<", peekAtOffset($FH->tell() + 4, 8);
1965              
1966 0 0       0 if ($cl == $compressedSize)
1967             {
1968 0         0 print "\n";
1969 0         0 out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG);
1970              
1971 0         0 error $FH->tell(), "Missing $sigName Signature";
1972 0         0 $localEntry->crc32( out_V "CRC");
1973 0         0 $localEntry->compressedSize( out_Q "Compressed Size");
1974 0         0 $localEntry->uncompressedSize( out_Q "Uncompressed Size");
1975              
1976 0 0 0     0 if (! $localEntry->zip64 && ( $cdEntry && ! $cdEntry->zip64))
      0        
1977             {
1978 0         0 error $here, "'$sigName': expected 32-bit values, got 64-bit";
1979             }
1980              
1981 0         0 return $FH->tell();
1982             }
1983             }
1984              
1985 0         0 seekTo($here);
1986              
1987 0         0 error $here, "Missing $sigName";
1988 0         0 return 0;
1989             }
1990              
1991             sub getcompressionMethodName
1992             {
1993 24     24   44 my $id = shift ;
1994 24   50     174 " '" . ($ZIP_CompressionMethods{$id} || "Unknown Method") . "'" ;
1995             }
1996              
1997             sub compressionMethod
1998             {
1999 24     24   54 my $id = shift ;
2000 24         54 Value_v($id) . getcompressionMethodName($id);
2001             }
2002              
2003             sub LocalHeader
2004             {
2005 12     12   88 my $signature = shift ;
2006 12         26 my $data = shift ;
2007 12         25 my $startRecordOffset = shift ;
2008              
2009 12         33 my $locHeaderOffset = $FH->tell() -4 ;
2010              
2011 12         57 ++ $LocalHeaderCount;
2012 12         573 print "\n";
2013 12         73 out $data, "LOCAL HEADER #$LocalHeaderCount" , Value_V($signature);
2014              
2015 12         63 need 26, Signatures::name($signature);
2016              
2017 12         21 my $buffer;
2018 12         21 my $orphan = 0;
2019              
2020 12         28 my ($loc, $CDcompressedSize, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) ;
2021 12         49 my $CentralEntryExists = $CentralDirectory->localOffset($startRecordOffset);
2022 12         78 my $localEntry = LocalDirectoryEntry->new();
2023              
2024 12         24 my $cdEntry;
2025              
2026 12 100 100     90 if (! $opt_scan && ! $opt_walk && $CentralEntryExists)
      66        
2027             {
2028 4         14 $cdEntry = $CentralDirectory->getByLocalOffset($startRecordOffset);
2029              
2030 4 50       21 if (! $cdEntry)
2031             {
2032 0         0 out1 "Orphan Entry: No matching central directory" ;
2033 0         0 $orphan = 1 ;
2034             }
2035              
2036 4         12 $cdZip64 = $cdEntry->zip64ExtraPresent;
2037 4         10 $zip64Sizes = $cdEntry->zip64SizesPresent;
2038 4         9 $cdEntryOffset = $cdEntry->centralHeaderOffset ;
2039 4         13 $localEntry->addCdEntry($cdEntry) ;
2040              
2041 4 50 33     14 if ($cdIndex && $cdIndex != $LocalHeaderCount)
2042             {
2043             # fatal undef, "$cdIndex != $LocalHeaderCount"
2044             }
2045             }
2046              
2047 12         131 my $extractVer = out_C "Extract Zip Spec", \&decodeZipVer;
2048 12         55 out_C "Extract OS", \&decodeOS;
2049              
2050 12         41 my ($bgp, $gpFlag) = read_v();
2051 12         33 my ($bcm, $compressedMethod) = read_v();
2052              
2053 12         269 out $bgp, "General Purpose Flag", Value_v($gpFlag) ;
2054 12         55 GeneralPurposeBits($compressedMethod, $gpFlag);
2055 12         22 my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ;
2056 12         22 my $streaming = $gpFlag & ZIP_GP_FLAG_STREAMING_MASK ;
2057 12         139 $localEntry->languageEncodingFlag($LanguageEncodingFlag) ;
2058              
2059 12         42 out $bcm, "Compression Method", compressionMethod($compressedMethod) ;
2060             info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2)
2061 12 50       74 if ! defined $ZIP_CompressionMethods{$compressedMethod} ;
2062              
2063 12     12   72 my $lastMod = out_V "Modification Time", sub { LastModTime($_[0]) };
  12         49  
2064              
2065 12         57 my $crc = out_V "CRC";
2066              
2067             # Weak encryption if
2068             # * encrypt flag (bit 0) set in General Purpose Flags
2069             # * strong encrypt (bit ) not set in General Purpose Flags
2070             # * not using AES encryption (compression method 99)
2071 12   33     113 my $weakEncryption = ($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ENCRYPTED_MASK &&
2072             $compressedMethod != ZIP_CM_AES;
2073             # Weak encryption uses the CRC value even when streaming is in play.
2074             # This conflicts with appnote 6.3.10 section 4.4.4
2075 12 0 33     40 warning $FH->tell() - 4, "CRC field should be zero when streaming is enabled"
      33        
2076             if $streaming && $crc != 0 && ! $weakEncryption;
2077              
2078 12         28 my $compressedSize = out_V "Compressed Size";
2079             # warning $FH->tell(), "Compressed Size should be zero when streaming is enabled";
2080              
2081 12         35 my $uncompressedSize = out_V "Uncompressed Size";
2082             # warning $FH->tell(), "Uncompressed Size should be zero when streaming is enabled";
2083              
2084 12         45 my $filenameLength = out_v "Filename Length";
2085              
2086 12 50       39 if ($filenameLength == 0)
2087             {
2088 0         0 info $FH->tell()- 2, "Zero Length filename";
2089             }
2090              
2091 12         47 my $extraLength = out_v "Extra Length";
2092              
2093 12         30 my $filename = '';
2094 12 50       38 if ($filenameLength)
2095             {
2096 12         39 need $filenameLength, Signatures::name($signature), 'Filename';
2097              
2098 12         123 myRead(my $raw_filename, $filenameLength);
2099 12         45 $localEntry->filename($raw_filename) ;
2100 12         60 $filename = outputFilename($raw_filename, $LanguageEncodingFlag);
2101 12         60 $localEntry->outputFilename($filename);
2102             }
2103              
2104 12         40 $localEntry->localHeaderOffset($locHeaderOffset) ;
2105 12         31 $localEntry->offsetStart($locHeaderOffset) ;
2106 12         30 $localEntry->compressedSize($compressedSize) ;
2107 12         31 $localEntry->uncompressedSize($uncompressedSize) ;
2108 12         33 $localEntry->extractVersion($extractVer);
2109 12         41 $localEntry->generalPurposeFlags($gpFlag);
2110 12         62 $localEntry->lastModDateTime($lastMod);
2111 12         31 $localEntry->crc32($crc) ;
2112 12         47 $localEntry->zip64ExtraPresent($cdZip64) ;
2113 12         48 $localEntry->zip64SizesPresent($zip64Sizes) ;
2114              
2115 12         32 $localEntry->compressedMethod($compressedMethod) ;
2116 12         42 $localEntry->streamed($gpFlag & ZIP_GP_FLAG_STREAMING_MASK) ;
2117              
2118 12         37 $localEntry->std_localHeaderOffset($locHeaderOffset + $PREFIX_DELTA) ;
2119 12         32 $localEntry->std_compressedSize($compressedSize) ;
2120 12         32 $localEntry->std_uncompressedSize($uncompressedSize) ;
2121 12         31 $localEntry->std_diskNumber(0) ;
2122              
2123 12 50       50 if ($extraLength)
2124             {
2125 0         0 need $extraLength, Signatures::name($signature), 'Extra';
2126 0         0 walkExtra($extraLength, $localEntry);
2127             }
2128              
2129             # Defer test for directory payload until Central Header processing.
2130             # Need to have external file attributes to deal with sme edge conditions.
2131             # # APPNOTE 6.3.10, sec 4.3.8
2132             # warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload"
2133             # if ! $streaming && $filename =~ m#/$# && $localEntry->uncompressedSize ;
2134              
2135 12         23 my @msg ;
2136             # if ($cdZip64 && ! $ZIP64)
2137             # {
2138             # # Central directory said this was Zip64
2139             # # some zip files don't have the Zip64 field in the local header
2140             # # seems to be a streaming issue.
2141             # push @msg, "Missing Zip64 extra field in Local Header #$hexHdrCount\n";
2142              
2143             # if (! $zip64Sizes)
2144             # {
2145             # # Central has a ZIP64 entry that doesn't have sizes
2146             # # Local doesn't have a Zip 64 at all
2147             # push @msg, "Unzip may complain about 'overlapped components' #$hexHdrCount\n";
2148             # }
2149             # else
2150             # {
2151             # $ZIP64 = 1
2152             # }
2153             # }
2154              
2155              
2156 12         30 my $minizip_encrypted = $localEntry->minizip_secure;
2157 12   33     47 my $pk_encrypted = ($gpFlag & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK) && $compressedMethod != ZIP_CM_AES && ! $minizip_encrypted;
2158              
2159             # Detecting PK strong encryption from a local header is a bit convoluted.
2160             # Cannot just use ZIP_GP_FLAG_ENCRYPTED_CD because minizip also uses this bit.
2161             # so jump through some hoops
2162             # extract ver is >= 5.0'
2163             # all the encryption flags are set in gpflags
2164             # TODO - add zero lengths for crc, compressed & uncompressed
2165              
2166 12 50 33     38 if (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ALL_ENCRYPT && $extractVer >= 0x32 )
2167             {
2168 0         0 $CentralDirectory->setPkEncryptedCD()
2169             }
2170              
2171 12         24 my $size = 0;
2172              
2173             # If no CD scanned, get compressed Size from local header.
2174             # Zip64 extra field takes priority
2175 12 100       36 my $cdl = defined $cdEntry
2176             ? $cdEntry->compressedSize()
2177             : undef;
2178              
2179 12         26 $CDcompressedSize = $localEntry->compressedSize ;
2180 12 50 66     44 $CDcompressedSize = $cdl
2181             if defined $cdl && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;
2182              
2183             my $cdu = defined $CentralDirectory->{byLocalOffset}{$locHeaderOffset}
2184             ? $CentralDirectory->{byLocalOffset}{$locHeaderOffset}{uncompressedSize}
2185 12 100       52 : undef;
2186 12         27 my $CDuncompressedSize = $localEntry->uncompressedSize ;
2187              
2188 12 50 66     39 $CDuncompressedSize = $cdu
2189             if defined $cdu && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK;
2190              
2191 12         21 my $fullCompressedSize = $CDcompressedSize;
2192              
2193 12         125 my $payloadOffset = $FH->tell();
2194 12         90 $localEntry->payloadOffset($payloadOffset) ;
2195 12         39 $localEntry->offsetEnd($payloadOffset + $fullCompressedSize -1) ;
2196              
2197 12 50       33 if ($CDcompressedSize)
2198             {
2199             # check if enough left in file for the payload
2200 12         36 my $available = $FILELEN - $FH->tell;
2201 12 50       91 if ($available < $CDcompressedSize )
2202             {
2203 0         0 error $FH->tell,
2204             "file truncated while reading 'PAYLOAD'",
2205             expectedMessage($CDcompressedSize, $available);
2206              
2207 0         0 $CDcompressedSize = $available;
2208             }
2209             }
2210              
2211             # Next block can decrement the CDcompressedSize
2212             # possibly to zero. Need to remember if it started out
2213             # as a non-zero value
2214 12         24 my $haveCDcompressedSize = $CDcompressedSize;
2215              
2216 12 50 33     117 if ($compressedMethod == ZIP_CM_AES && $localEntry->aesValid) # AES Encryption
    50          
    0          
2217             {
2218 0         0 $CDcompressedSize -= printAes($localEntry)
2219             }
2220             elsif (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == 0)
2221             {
2222 12 50       44 if ($compressedMethod == ZIP_CM_LZMA)
2223             {
2224              
2225 0         0 $size = printLzmaProperties()
2226             }
2227              
2228 12         26 $CDcompressedSize -= $size;
2229             }
2230             elsif ($pk_encrypted)
2231             {
2232 0         0 $CDcompressedSize -= DecryptionHeader();
2233             }
2234              
2235 12 50       27 if ($haveCDcompressedSize) {
2236              
2237 12 50 33     62 if ($compressedMethod == ZIP_CM_REFERENCE && $CDcompressedSize == 20) {
    50 33        
2238             # Payload for a Reference is the SHA-1 hash of the uncompressed content
2239 0         0 myRead(my $sha1, 20);
2240 0         0 out $sha1, "PAYLOAD", "SHA-1 Hash: " . hexDump($sha1);
2241             }
2242             elsif ($compressedMethod == ZIP_CM_AES && $localEntry->aesValid ) {
2243 0         0 outSomeData($CDcompressedSize, "PAYLOAD", $opt_Redact) ;
2244 0         0 my $auth ;
2245 0         0 myRead($auth, 10);
2246 0         0 out $auth, "AES Auth", hexDump16($auth);
2247             }
2248             else {
2249 12         39 outSomeData($CDcompressedSize, "PAYLOAD", $opt_Redact) ;
2250             }
2251             }
2252              
2253             print "WARNING: $_"
2254 12         42 for @msg;
2255              
2256 12         24 push @Messages, @msg ;
2257              
2258 12         56 $LocalDirectory->addEntry($localEntry);
2259              
2260             return {
2261 12         104 'localHeader' => 1,
2262             'streamed' => $gpFlag & ZIP_GP_FLAG_STREAMING_MASK,
2263             'offset' => $startRecordOffset,
2264             'length' => $FH->tell() - $startRecordOffset,
2265             'payloadLength' => $fullCompressedSize,
2266             'payloadOffset' => $payloadOffset,
2267             'entry' => $localEntry,
2268             } ;
2269             }
2270              
2271 6     6   77 use constant Pack_ZIP_DIGITAL_SIGNATURE_SIG => pack("V", ZIP_DIGITAL_SIGNATURE_SIG);
  6         12  
  6         10495  
2272              
2273             sub findDigitalSignature
2274             {
2275 0     0   0 my $cdSize = shift;
2276              
2277 0         0 my $here = $FH->tell();
2278              
2279 0         0 my $data ;
2280 0         0 myRead($data, $cdSize);
2281              
2282 0         0 seekTo($here);
2283              
2284             # find SIG
2285 0         0 my $ix = index($data, Pack_ZIP_DIGITAL_SIGNATURE_SIG);
2286 0 0       0 if ($ix > -1)
2287             {
2288             # check size of signature means it is directly after the encrypted CD
2289 0         0 my $sigSize = unpack "v", substr($data, $ix+4, 2);
2290 0 0       0 if ($ix + 4 + 2 + $sigSize == $cdSize)
2291             {
2292             # return size of digital signature record
2293 0         0 return 4 + 2 + $sigSize ;
2294             }
2295             }
2296              
2297 0         0 return 0;
2298             }
2299              
2300             sub displayEncryptedCD
2301             {
2302             # First thing in the encrypted CD is the Decryption Header
2303 0     0   0 my $decryptHeaderSize = DecryptionHeader(1);
2304              
2305             # Check for digital signature record in the CD
2306             # It needs to be the very last thing in the CD
2307              
2308 0         0 my $delta = deltaToNextSignature();
2309 0         0 print "\n";
2310 0 0       0 outSomeData($delta, "ENCRYPTED CENTRAL DIRECTORY")
2311             if $delta;
2312             }
2313              
2314             sub DecryptionHeader
2315             {
2316             # APPNOTE 6.3.10, sec 7.2.4
2317              
2318             # -Decryption Header:
2319             # Value Size Description
2320             # ----- ---- -----------
2321             # IVSize 2 bytes Size of initialization vector (IV)
2322             # IVData IVSize Initialization vector for this file
2323             # Size 4 bytes Size of remaining decryption header data
2324             # Format 2 bytes Format definition for this record
2325             # AlgID 2 bytes Encryption algorithm identifier
2326             # Bitlen 2 bytes Bit length of encryption key
2327             # Flags 2 bytes Processing flags
2328             # ErdSize 2 bytes Size of Encrypted Random Data
2329             # ErdData ErdSize Encrypted Random Data
2330             # Reserved1 4 bytes Reserved certificate processing data
2331             # Reserved2 (var) Reserved for certificate processing data
2332             # VSize 2 bytes Size of password validation data
2333             # VData VSize-4 Password validation data
2334             # VCRC32 4 bytes Standard ZIP CRC32 of password validation data
2335              
2336 0     0   0 my $central = shift ;
2337              
2338 0 0       0 if ($central)
2339             {
2340 0         0 print "\n";
2341 0         0 out "", "CENTRAL HEADER DECRYPTION RECORD";
2342              
2343             }
2344             else
2345             {
2346 0         0 print "\n";
2347 0         0 out "", "DECRYPTION HEADER RECORD";
2348             }
2349              
2350 0         0 my $bytecount = 2;
2351              
2352 0         0 my $IVSize = out_v "IVSize";
2353 0         0 outHexdump($IVSize, "IVData");
2354 0         0 $bytecount += $IVSize;
2355              
2356 0         0 my $Size = out_V "Size";
2357 0         0 $bytecount += $Size + 4;
2358              
2359 0         0 out_v "Format";
2360 0   0 0   0 out_v "AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ;
  0         0  
2361 0         0 out_v "BitLen";
2362 0   0 0   0 out_v "Flags", sub { $FlagsLookup{ $_[0] } // "Reserved for certificate processing" } ;
  0         0  
2363              
2364 0         0 my $ErdSize = out_v "ErdSize";
2365 0         0 outHexdump($ErdSize, "ErdData");
2366              
2367 0         0 my $Reserved1_RCount = out_V "RCount";
2368 0         0 Reserved2($Reserved1_RCount);
2369              
2370 0         0 my $VSize = out_v "VSize";
2371 0         0 outHexdump($VSize-4, "VData");
2372              
2373 0         0 out_V "VCRC32";
2374              
2375 0         0 return $bytecount ;
2376             }
2377              
2378             sub Reserved2
2379             {
2380             # APPNOTE 6.3.10, sec 7.4.3 & 7.4.4
2381              
2382 0     0   0 my $recipients = shift;
2383              
2384 0 0       0 return 0
2385             if $recipients == 0;
2386              
2387 0   0 0   0 out_v "HashAlg", sub { $HashAlgLookup{ $_[0] } // "Unknown algorithm" } ;
  0         0  
2388 0         0 my $HSize = out_v "HSize" ;
2389              
2390 0         0 my $ix = 1;
2391 0         0 for (0 .. $recipients-1)
2392             {
2393 0         0 my $hex = sprintf("Key #%X", $ix) ;
2394 0         0 my $RESize = out_v "RESize $hex";
2395              
2396 0         0 outHexdump($HSize, "REHData $hex");
2397 0         0 outHexdump($RESize - $HSize, "REKData $hex");
2398              
2399 0         0 ++ $ix;
2400             }
2401             }
2402              
2403             sub redactData
2404             {
2405 0     0   0 my $data = shift;
2406              
2407             # Redact everything apart from directory separators
2408 0 0       0 $data =~ s(.)(X)g
2409             if $opt_Redact;
2410              
2411 0         0 return $data;
2412             }
2413              
2414             sub redactFilename
2415             {
2416 0     0   0 my $filename = shift;
2417              
2418             # Redact everything apart from directory separators
2419 0 0       0 $filename =~ s(.)(X)g
2420             if $opt_Redact;
2421              
2422 0         0 return $filename;
2423             }
2424              
2425             sub validateDirectory
2426             {
2427             # Check that Directories are stored correctly
2428             #
2429             # 1. Filename MUST end with a "/"
2430             # see APPNOTE 6.3.10, sec 4.3.8
2431             # 2. Uncompressed size == 0
2432             # see APPNOTE 6.3.10, sec 4.3.8
2433             # 3. Warn if compressed size > 0 and Uncompressed size == 0
2434             # 4. Check for presence of DOS directory attrib in External Attributes
2435             # 5. Check for Unix external attribute S_IFDIR
2436             #
2437             # Edge condition is a Windows directory symlink
2438             # In this case a payload is expected.
2439             # TODO - decode the payload to get the target of the symlink
2440              
2441 12     12   24 my $offset = shift ;
2442 12         22 my $filename = shift ;
2443 12         20 my $extractVersion = shift;
2444 12         27 my $versionMadeBy = shift;
2445 12         24 my $compressedSize = shift;
2446 12         16 my $uncompressedSize = shift;
2447 12         23 my $externalAttributes = shift;
2448              
2449 12         25 my $dosAttributes = $externalAttributes & 0xFFFF;
2450 12         26 my $otherAttributes = ($externalAttributes >> 16 ) & 0xFFFF;
2451              
2452 12         21 my $probablyDirectory = 0;
2453 12         16 my $filenameOK = 0;
2454 12         21 my $dosDirectoryAttributeSet = 0;
2455 12         46 my $unixDirectoryAttributeSet = 0;
2456              
2457 12 50       58 if ($filename =~ m#/$#)
2458             {
2459             # filename claims it is a directory.
2460 0         0 $probablyDirectory = 1;
2461 0         0 $filenameOK = 1;
2462             }
2463              
2464 12 50       62 if ($dosAttributes & WIN_FILE_ATTRIBUTE_DIRECTORY) # ATTR_DIRECTORY
2465             {
2466 0         0 $probablyDirectory = 1;
2467 0         0 $dosDirectoryAttributeSet = 1 ;
2468             }
2469              
2470 12 50 33     101 if ($versionMadeBy == 3 && $otherAttributes & 0x4000) # Unix & S_IFDIR
2471             {
2472 0         0 $probablyDirectory = 1;
2473 0         0 $unixDirectoryAttributeSet = 1;
2474             }
2475              
2476             return
2477 12 50       45 unless $probablyDirectory ;
2478              
2479             # Probably dealing with a directory from here on
2480              
2481 0 0 0     0 error $offset + CentralDirectoryEntry::Offset_Filename(),
2482             "Directory '$filename' must end in a '/'",
2483             "'External Attributes' flag this as a directory"
2484             if ! $filenameOK && $uncompressedSize == 0;
2485              
2486 0 0 0     0 info $offset + CentralDirectoryEntry::Offset_ExternalAttributes(),
2487             "DOS Directory flag not set in 'External Attributes' for Directory '$filename'"
2488             if $filenameOK && ! $dosDirectoryAttributeSet;
2489              
2490 0 0 0     0 info $offset + CentralDirectoryEntry::Offset_ExternalAttributes(),
      0        
2491             "Unix Directory flag not set in 'External Attributes' for Directory '$filename'"
2492             if $filenameOK && $versionMadeBy == 3 && ! $unixDirectoryAttributeSet;
2493              
2494             # Windows symlink directories will have a payload, so check for that now.
2495             # Windows attributes can sometimes be missing so check for unix directory bit + symlink bit
2496 6     6   53 use constant UNIX_SYMLINK_DIRECTORY => 0x4000 | 0xA0000;
  6         9  
  6         5123  
2497 0 0 0     0 if (! ($dosAttributes & WIN_FILE_ATTRIBUTE_SYMBOLIC_LINK_MASK || $otherAttributes & UNIX_SYMLINK_DIRECTORY == UNIX_SYMLINK_DIRECTORY))
2498             {
2499 0 0       0 if ($uncompressedSize != 0)
    0          
2500             {
2501             # APPNOTE 6.3.10, sec 4.3.8
2502 0         0 error $offset + CentralDirectoryEntry::Offset_UncompressedSize(),
2503             "Directory '$filename' must not have a payload. Uncompressed size is " . decimalHex0x($uncompressedSize)
2504             }
2505             elsif ($compressedSize != 0)
2506             {
2507 0         0 info $offset + CentralDirectoryEntry::Offset_CompressedSize(),
2508             "Directory '$filename' has compressed payload that uncompress to nothing"
2509             }
2510             }
2511              
2512 0 0       0 if ($extractVersion < 20)
2513             {
2514             # APPNOTE 6.3.10, sec 4.4.3.2
2515 0         0 my $got = decodeZipVer($extractVersion);
2516 0         0 warning $offset + CentralDirectoryEntry::Offset_VersionNeededToExtract(),
2517             "'Extract Zip Spec' is '$got'. Need value >= '2.0' for Directory '$filename'"
2518             }
2519             }
2520              
2521             sub validateFilename
2522             {
2523 24     24   46 my $filename = shift ;
2524              
2525 24 50       73 return "Zero length filename"
2526             if $filename eq '' ;
2527              
2528             # TODO
2529             # - check length of filename
2530             # getconf NAME_MAX . and getconf PATH_MAX . on Linux
2531              
2532             # Start with APPNOTE restrictions
2533              
2534             # APPNOTE 6.3.10, sec 4.4.17.1
2535             #
2536             # No absolute path
2537             # No backslash delimiters
2538             # No drive letters
2539              
2540 24 50       125 return "Filename must not be an absolute path"
2541             if $filename =~ m#^/#;
2542              
2543 24 50       94 return ["Backslash detected in filename", "Possible Windows path."]
2544             if $filename =~ m#\\#;
2545              
2546 24 50       71 return "Windows Drive Letter '$1' not allowed in filename"
2547             if $filename =~ /^([a-z]:)/i ;
2548              
2549             # Slip Vulnerability with use of ".." in a relative path
2550             # https://security.snyk.io/research/zip-slip-vulnerability
2551 24 50 33     215 return ["Use of '..' in filename is a Zip Slip Vulnerability",
      33        
2552             "See https://security.snyk.io/research/zip-slip-vulnerability" ]
2553             if $filename =~ m#^\.\./# || $filename =~ m#/\.\./# || $filename =~ m#/\.\.# ;
2554              
2555             # Cannot have "." or ".." as the full filename
2556 24 50       89 return "Use of current-directory filename '.' may not unzip correctly"
2557             if $filename eq '.' ;
2558              
2559 24 50       60 return "Use of parent-directory filename '..' may not unzip correctly"
2560             if $filename eq '..' ;
2561              
2562             # Portability (mostly with Windows)
2563              
2564             {
2565             # see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2566 24         39 state $badDosFilename = join '|', map { quotemeta }
  24         59  
  132         417  
2567             qw(CON PRN AUX NUL
2568             COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9
2569             LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9
2570             ) ;
2571              
2572             # if $filename contains any invalid codepoints, we will get a warning like this
2573             #
2574             # Operation "pattern match (m//)" returns its argument for non-Unicode code point
2575             #
2576             # so silence it for now.
2577              
2578 6     6   54 no warnings;
  6         12  
  6         51286  
2579              
2580 24 50       1101 return "Portability Issue: '$1' is a reserved Windows device name"
2581             if $filename =~ /^($badDosFilename)$/io ;
2582              
2583             # Can't have the device name with an extension either
2584 24 50       1314 return "Portability Issue: '$1' is a reserved Windows device name"
2585             if $filename =~ /^($badDosFilename)\./io ;
2586             }
2587              
2588 24         66 state $illegal_windows_chars = join '|', map { quotemeta } qw( < > : " | ? * );
  42         113  
2589 24 50       329 return "Portability Issue: Windows filename cannot contain '$1'"
2590             if $filename =~ /($illegal_windows_chars)/o ;
2591              
2592 24 50       77 return "Portability Issue: Null character '\\x00' is not allowed in a Windows or Linux filename"
2593             if $filename =~ /\x00/ ;
2594              
2595 24 50       126 return sprintf "Portability Issue: Control character '\\x%02X' is not allowed in a Windows filename", ord($1)
2596             if $filename =~ /([\x00-\x1F])/ ;
2597              
2598 24         62 return undef;
2599             }
2600              
2601             sub getOutputFilename
2602             {
2603 24     24   54 my $raw_filename = shift;
2604 24         40 my $LanguageEncodingFlag = shift;
2605 24   50     115 my $message = shift // "Filename";
2606              
2607 24         46 my $filename ;
2608             my $decoded_filename;
2609              
2610 24 50       116 if ($raw_filename eq '')
    50          
2611             {
2612 0 0       0 if ($message eq 'Filename')
2613             {
2614 0         0 warning $FH->tell() ,
2615             "Filename ''",
2616             "Zero Length Filename" ;
2617             }
2618              
2619 0         0 return '', '', 0;
2620             }
2621             elsif ($opt_Redact)
2622             {
2623 0         0 return redactFilename($raw_filename), '', 0 ;
2624             }
2625             else
2626             {
2627 24         79 $decoded_filename = TextEncoding::decode($raw_filename, $message, $LanguageEncodingFlag) ;
2628 24         67 $filename = TextEncoding::encode($decoded_filename, $message, $LanguageEncodingFlag) ;
2629             }
2630              
2631 24         99 return $filename, $decoded_filename, $filename ne $raw_filename ;
2632             }
2633              
2634             sub outputFilename
2635             {
2636 24     24   45 my $raw_filename = shift;
2637 24         44 my $LanguageEncodingFlag = shift;
2638 24   50     195 my $message = shift // "Filename";
2639              
2640 24         91 my ($filename, $decoded_filename, $modified) = getOutputFilename($raw_filename, $LanguageEncodingFlag);
2641              
2642 24         104 out $raw_filename, $message, "'". $filename . "'";
2643              
2644 24 50 33     163 if (! $opt_Redact && TextEncoding::debugEncoding())
2645             {
2646             # use Devel::Peek;
2647             # print "READ " ; Dump($raw_filename);
2648             # print "INTERNAL " ; Dump($decoded_filename);
2649             # print "OUTPUT " ; Dump($filename);
2650              
2651 0 0       0 debug $FH->tell() - length($raw_filename),
2652             "$message Encoding Change"
2653             if $modified ;
2654              
2655             # use Unicode::Normalize;
2656             # my $NormalizedForm ;
2657             # if (defined $decoded_filename)
2658             # {
2659             # $NormalizedForm .= Unicode::Normalize::checkNFD $decoded_filename ? 'NFD ' : '';
2660             # $NormalizedForm .= Unicode::Normalize::checkNFC $decoded_filename ? 'NFC ' : '';
2661             # $NormalizedForm .= Unicode::Normalize::checkNFKD $decoded_filename ? 'NFKD ' : '';
2662             # $NormalizedForm .= Unicode::Normalize::checkNFKC $decoded_filename ? 'NFKC ' : '';
2663             # $NormalizedForm .= Unicode::Normalize::checkFCD $decoded_filename ? 'FCD ' : '';
2664             # $NormalizedForm .= Unicode::Normalize::checkFCC $decoded_filename ? 'FCC ' : '';
2665             # }
2666              
2667 0         0 debug $FH->tell() - length($raw_filename),
2668             "Encoding Debug for $message",
2669             "Octets Read from File [$raw_filename][" . length($raw_filename). "] [" . charDump2($raw_filename) . "]",
2670             "Via Unicode Codepoints [$decoded_filename][" . length($decoded_filename) . "] [" . charDump($decoded_filename) . "]",
2671             # "Unicode Normalization $NormalizedForm",
2672             "Octets Written [$filename][" . length($filename). "] [" . charDump2($filename) . "]";
2673             }
2674              
2675 24 50 33     125 if ($message eq 'Filename' && $opt_want_warning_messages)
2676             {
2677             # Check for bad, unsafe & not portable filenames
2678 24         68 my $v = validateFilename($decoded_filename);
2679              
2680 24 50       63 if ($v)
2681             {
2682 0 0       0 my @v = ref $v eq 'ARRAY'
2683             ? @$v
2684             : $v;
2685              
2686 0         0 warning $FH->tell() - length($raw_filename),
2687             "Filename '$filename'",
2688             @v
2689             }
2690             }
2691              
2692 24         76 return $filename;
2693             }
2694              
2695             sub CentralHeader
2696             {
2697 12     12   66 my $signature = shift ;
2698 12         24 my $data = shift ;
2699 12         19 my $startRecordOffset = shift ;
2700              
2701 12         29 my $cdEntryOffset = $FH->tell() - 4 ;
2702              
2703 12         52 ++ $CentralHeaderCount;
2704              
2705 12         295 print "\n";
2706 12         67 out $data, "CENTRAL HEADER #$CentralHeaderCount", Value_V($signature);
2707 12         26 my $buffer;
2708              
2709 12         36 need 42, Signatures::name($signature);
2710              
2711 12         40 out_C "Created Zip Spec", \&decodeZipVer;
2712 12         40 my $made_by = out_C "Created OS", \&decodeOS;
2713 12         94 my $extractVer = out_C "Extract Zip Spec", \&decodeZipVer;
2714 12         48 out_C "Extract OS", \&decodeOS;
2715              
2716 12         34 my ($bgp, $gpFlag) = read_v();
2717 12         33 my ($bcm, $compressedMethod) = read_v();
2718              
2719 12         71 my $cdEntry = CentralDirectoryEntry->new($cdEntryOffset);
2720              
2721 12         122 out $bgp, "General Purpose Flag", Value_v($gpFlag) ;
2722 12         63 GeneralPurposeBits($compressedMethod, $gpFlag);
2723 12         20 my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ;
2724 12         67 $cdEntry->languageEncodingFlag($LanguageEncodingFlag) ;
2725              
2726 12         68 out $bcm, "Compression Method", compressionMethod($compressedMethod) ;
2727             info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2)
2728 12 50       58 if ! defined $ZIP_CompressionMethods{$compressedMethod} ;
2729              
2730 12     12   100 my $lastMod = out_V "Modification Time", sub { LastModTime($_[0]) };
  12         42  
2731              
2732 12         68 my $crc = out_V "CRC";
2733 12         32 my $compressedSize = out_V "Compressed Size";
2734 12         21 my $std_compressedSize = $compressedSize;
2735 12         35 my $uncompressedSize = out_V "Uncompressed Size";
2736 12         26 my $std_uncompressedSize = $uncompressedSize;
2737 12         57 my $filenameLength = out_v "Filename Length";
2738 12 50       42 if ($filenameLength == 0)
2739             {
2740 0         0 info $FH->tell()- 2, "Zero Length filename";
2741             }
2742 12         28 my $extraLength = out_v "Extra Length";
2743 12         34 my $comment_length = out_v "Comment Length";
2744 12         34 my $disk_start = out_v "Disk Start";
2745 12         22 my $std_disk_start = $disk_start;
2746              
2747 12         31 my $int_file_attrib = out_v "Int File Attributes";
2748 12 50       76 out1 "[Bit 0]", $int_file_attrib & 1 ? "1 'Text Data'" : "0 'Binary Data'";
2749 12 50       79 out1 "[Bits 1-15]", Value_v($int_file_attrib & 0xFE) . " 'Unknown'"
2750             if $int_file_attrib & 0xFE ;
2751              
2752 12         35 my $ext_file_attrib = out_V "Ext File Attributes";
2753              
2754             {
2755             # MS-DOS Attributes are bottom two bytes
2756 12         50 my $dos_attrib = $ext_file_attrib & 0xFFFF;
  12         30  
2757              
2758             # See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
2759             # and https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/65e0c225-5925-44b0-8104-6b91339c709f
2760              
2761 12 50       37 out1 "[Bit 0]", "Read-Only" if $dos_attrib & WIN_FILE_ATTRIBUTE_READONLY ;
2762 12 50       80 out1 "[Bit 1]", "Hidden" if $dos_attrib & WIN_FILE_ATTRIBUTE_HIDDEN ;
2763 12 50       34 out1 "[Bit 2]", "System" if $dos_attrib & WIN_FILE_ATTRIBUTE_SYSTEM ;
2764 12 50       29 out1 "[Bit 3]", "Unused" if $dos_attrib & WIN_FILE_ATTRIBUTE_LABEL ; # bit not used
2765 12 50       30 out1 "[Bit 4]", "Directory" if $dos_attrib & WIN_FILE_ATTRIBUTE_DIRECTORY ;
2766 12 50       38 out1 "[Bit 5]", "Archive" if $dos_attrib & WIN_FILE_ATTRIBUTE_ARCHIVE ;
2767 12 50       44 out1 "[Bit 6]", "Device/Symbolic Link" if $dos_attrib & WIN_FILE_ATTRIBUTE_DEVICE_OR_SYMBOLIC_LINK ; # Tentative use of Symbolic Link.
2768 12 50       31 out1 "[Bit 7]", "Normal/Executable" if $dos_attrib & WIN_FILE_ATTRIBUTE_NORMAL_OR_EXECUTABLE ; # Tentative use of Executable
2769 12 50       53 out1 "[Bit 8]", "Temporary" if $dos_attrib & WIN_FILE_ATTRIBUTE_TEMPORARY ;
2770 12 50       28 out1 "[Bit 9]", "Sparse" if $dos_attrib & WIN_FILE_ATTRIBUTE_SPARSE_FILE ;
2771 12 50       31 out1 "[Bit 10]", "Reparse Point" if $dos_attrib & WIN_FILE_ATTRIBUTE_REPARSE_POINT ;
2772 12 50       36 out1 "[Bit 11]", "Compressed" if $dos_attrib & WIN_FILE_ATTRIBUTE_COMPRESSED ;
2773 12 50       110 out1 "[Bit 12]", "Offline" if $dos_attrib & WIN_FILE_ATTRIBUTE_OFFLINE ;
2774 12 50       39 out1 "[Bit 13]", "Not Indexed" if $dos_attrib & WIN_FILE_ATTRIBUTE_NOT_CONTENT_INDEXED ;
2775              
2776             # Zip files created on Mac seem to set this bit. Not reason why found yet.
2777 12 50       46 out1 "[Bit 14]", "Encrypted" if $dos_attrib & WIN_FILE_ATTRIBUTE_ENCRYPTED ;
2778              
2779             # p7Zip & 7z set this bit to flag that the high 16-bits are Unix attributes
2780 12 50       45 out1 "[Bit 15]", "Possible p7zip/7z Unix Flag" if $dos_attrib & 0x8000 ;
2781              
2782             }
2783              
2784 12         29 my $native_attrib = ($ext_file_attrib >> 16 ) & 0xFFFF;
2785              
2786             # Some Windows zip file (created OS == 0), have th eUnix attributes populated.
2787 12 50 33     63 if ($made_by == 3 || $made_by == 0) # Unix
    0          
2788             {
2789 12         92 state $mask = {
2790             0 => '---',
2791             1 => '--x',
2792             2 => '-w-',
2793             3 => '-wx',
2794             4 => 'r--',
2795             5 => 'r-x',
2796             6 => 'rw-',
2797             7 => 'rwx',
2798             } ;
2799              
2800 12         32 my $rwx = ($native_attrib & 0777);
2801              
2802 12 50       32 if ($rwx)
2803             {
2804 12         26 my $output = '';
2805 12         56 $output .= $mask->{ ($rwx >> 6) & 07 } ;
2806 12         32 $output .= $mask->{ ($rwx >> 3) & 07 } ;
2807 12         50 $output .= $mask->{ ($rwx >> 0) & 07 } ;
2808              
2809 12         35 out1 "[Bits 16-24]", Value_v($rwx) . " 'Unix attrib: $output'" ;
2810 12 50       44 out1 "[Bit 25]", "1 'Sticky'"
2811             if $rwx & 0x200 ;
2812 12 50       31 out1 "[Bit 26]", "1 'Set GID'"
2813             if $rwx & 0x400 ;
2814 12 50       54 out1 "[Bit 27]", "1 'Set UID'"
2815             if $rwx & 0x800 ;
2816              
2817 12         55 my $not_rwx = (($native_attrib >> 12) & 0xF);
2818 12 50       36 if ($not_rwx)
2819             {
2820 12         77 state $masks = {
2821             0x0C => 'Socket', # 0x0C 0b1100
2822             0x0A => 'Symbolic Link', # 0x0A 0b1010
2823             0x08 => 'Regular File', # 0x08 0b1000
2824             0x06 => 'Block Device', # 0x06 0b0110
2825             0x04 => 'Directory', # 0x04 0b0100
2826             0x02 => 'Character Device', # 0x02 0b0010
2827             0x01 => 'FIFO', # 0x01 0b0001
2828             };
2829              
2830 12   50     46 my $got = $masks->{$not_rwx} // 'Unknown Unix attrib' ;
2831 12         45 out1 "[Bits 28-31]", Value_C($not_rwx) . " '$got'"
2832             }
2833             }
2834             }
2835             elsif ($native_attrib)
2836             {
2837 0         0 out1 "[Bits 24-31]", Value_v($native_attrib) . " 'Unknown attributes for OS ID $made_by'"
2838             }
2839              
2840 12         57 my ($d, $locHeaderOffset) = read_V();
2841 12         34 my $out = Value_V($locHeaderOffset);
2842 12         22 my $std_localHeaderOffset = $locHeaderOffset;
2843              
2844 12 50       49 if ($locHeaderOffset != MAX32)
2845             {
2846 12         46 testPossiblePrefix($locHeaderOffset, ZIP_LOCAL_HDR_SIG);
2847 12 50       71 if ($PREFIX_DELTA)
2848             {
2849 0         0 $out .= " [Actual Offset is " . Value_V($locHeaderOffset + $PREFIX_DELTA) . "]"
2850             }
2851             }
2852              
2853 12         45 out $d, "Local Header Offset", $out;
2854              
2855 12 50       41 if ($locHeaderOffset != MAX32)
2856             {
2857 12         36 my $commonMessage = "'Local Header Offset' field in '" . Signatures::name($signature) . "' is invalid";
2858 12         105 $locHeaderOffset = checkOffsetValue($locHeaderOffset, $startRecordOffset, 0, $commonMessage, $startRecordOffset + CentralDirectoryEntry::Offset_RelativeOffsetToLocal(), ZIP_LOCAL_HDR_SIG) ;
2859             }
2860              
2861 12         26 my $filename = '';
2862 12 50       34 if ($filenameLength)
2863             {
2864 12         43 need $filenameLength, Signatures::name($signature), 'Filename';
2865              
2866 12         33 myRead(my $raw_filename, $filenameLength);
2867 12         46 $cdEntry->filename($raw_filename) ;
2868 12         44 $filename = outputFilename($raw_filename, $LanguageEncodingFlag);
2869 12         49 $cdEntry->outputFilename($filename);
2870             }
2871              
2872 12         33 $cdEntry->centralHeaderOffset($cdEntryOffset) ;
2873 12         36 $cdEntry->localHeaderOffset($locHeaderOffset) ;
2874 12         63 $cdEntry->compressedSize($compressedSize) ;
2875 12         32 $cdEntry->uncompressedSize($uncompressedSize) ;
2876 12         33 $cdEntry->zip64ExtraPresent(undef) ; #$cdZip64; ### FIX ME
2877 12         33 $cdEntry->zip64SizesPresent(undef) ; # $zip64Sizes; ### FIX ME
2878 12         33 $cdEntry->extractVersion($extractVer);
2879 12         33 $cdEntry->generalPurposeFlags($gpFlag);
2880 12         33 $cdEntry->compressedMethod($compressedMethod) ;
2881 12         30 $cdEntry->lastModDateTime($lastMod);
2882 12         35 $cdEntry->crc32($crc) ;
2883 12         32 $cdEntry->inCentralDir(1) ;
2884              
2885 12         33 $cdEntry->std_localHeaderOffset($std_localHeaderOffset) ;
2886 12         31 $cdEntry->std_compressedSize($std_compressedSize) ;
2887 12         31 $cdEntry->std_uncompressedSize($std_uncompressedSize) ;
2888 12         32 $cdEntry->std_diskNumber($std_disk_start) ;
2889              
2890 12 50       27 if ($extraLength)
2891             {
2892 0         0 need $extraLength, Signatures::name($signature), 'Extra';
2893              
2894 0         0 walkExtra($extraLength, $cdEntry);
2895             }
2896              
2897             # $cdEntry->endCentralHeaderOffset($FH->tell() - 1);
2898              
2899             # Can only validate for directory after zip64 data is read
2900 12         171 validateDirectory($cdEntryOffset, $filename, $extractVer, $made_by,
2901             $cdEntry->compressedSize, $cdEntry->uncompressedSize, $ext_file_attrib);
2902              
2903 12 50       40 if ($comment_length)
2904             {
2905 0         0 need $comment_length, Signatures::name($signature), 'Comment';
2906              
2907 0         0 my $comment ;
2908 0         0 myRead($comment, $comment_length);
2909 0         0 outputFilename $comment, $LanguageEncodingFlag, "Comment";
2910 0         0 $cdEntry->comment($comment);
2911             }
2912              
2913 12         38 $cdEntry->offsetStart($cdEntryOffset) ;
2914 12         52 $cdEntry->offsetEnd($FH->tell() - 1) ;
2915              
2916 12         52 $CentralDirectory->addEntry($cdEntry);
2917              
2918 12 50       42 return { 'encapsulated' => $cdEntry ? $cdEntry->encapsulated() : 0};
2919             }
2920              
2921             sub decodeZipVer
2922             {
2923 36     36   64 my $ver = shift ;
2924              
2925 36 50       79 return ""
2926             if ! defined $ver;
2927              
2928 36         98 my $sHi = int($ver /10) ;
2929 36         65 my $sLo = $ver % 10 ;
2930              
2931 36         123 "$sHi.$sLo";
2932             }
2933              
2934             sub decodeOS
2935             {
2936 36     36   61 my $ver = shift ;
2937              
2938 36 50       171 $OS_Lookup{$ver} || "Unknown" ;
2939             }
2940              
2941             sub Zip64EndCentralHeader
2942             {
2943             # Extra ID is 0x0001
2944              
2945             # APPNOTE 6.3.10, section 4.3.14, 7.3.3, 7.3.4 & APPENDIX C
2946              
2947             # TODO - APPNOTE allows an extensible data sector at end of this record (see APPNOTE 6.3.10, section 4.3.14.4)
2948             # The code below does NOT take this into account.
2949              
2950 0     0   0 my $signature = shift ;
2951 0         0 my $data = shift ;
2952 0         0 my $startRecordOffset = shift ;
2953              
2954 0         0 print "\n";
2955 0         0 out $data, "ZIP64 END CENTRAL DIR RECORD", Value_V($signature);
2956              
2957 0         0 need 8, Signatures::name($signature);
2958              
2959 0         0 my $size = out_Q "Size of record";
2960              
2961 0         0 need $size, Signatures::name($signature);
2962              
2963 0         0 out_C "Created Zip Spec", \&decodeZipVer;
2964 0         0 out_C "Created OS", \&decodeOS;
2965 0         0 my $extractSpec = out_C "Extract Zip Spec", \&decodeZipVer;
2966 0         0 out_C "Extract OS", \&decodeOS;
2967 0         0 my $diskNumber = out_V "Number of this disk";
2968 0         0 my $cdDiskNumber = out_V "Central Dir Disk no";
2969 0         0 my $entriesOnThisDisk = out_Q "Entries in this disk";
2970 0         0 my $totalEntries = out_Q "Total Entries";
2971 0         0 my $centralDirSize = out_Q "Size of Central Dir";
2972              
2973 0         0 my ($d, $centralDirOffset) = read_Q();
2974 0         0 my $out = Value_Q($centralDirOffset);
2975 0         0 testPossiblePrefix($centralDirOffset, ZIP_CENTRAL_HDR_SIG);
2976              
2977 0 0       0 $out .= " [Actual Offset is " . Value_Q($centralDirOffset + $PREFIX_DELTA) . "]"
2978             if $PREFIX_DELTA ;
2979 0         0 out $d, "Offset to Central dir", $out;
2980              
2981 0 0       0 if (! emptyArchive($startRecordOffset, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset))
2982             {
2983 0         0 my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name($signature) . "' is invalid";
2984 0         0 $centralDirOffset = checkOffsetValue($centralDirOffset, $startRecordOffset, $centralDirSize, $commonMessage, $startRecordOffset + 48, ZIP_CENTRAL_HDR_SIG, 0, $extractSpec < 0x3E) ;
2985             }
2986              
2987             # Length of 44 means typical version 1 header
2988             return
2989 0 0       0 if $size == 44 ;
2990              
2991 0         0 my $remaining = $size - 44;
2992              
2993             # pkzip sets the extract zip spec to 6.2 (0x3E) to signal a v2 record
2994             # See APPNOTE 6.3.10, section, 7.3.3
2995              
2996 0 0       0 if ($extractSpec >= 0x3E)
2997             {
2998             # Version 2 header (see APPNOTE 6.3.7, section 7.3.4, )
2999             # Can use version 2 header to infer presence of encrypted CD
3000 0         0 $CentralDirectory->setPkEncryptedCD();
3001              
3002              
3003             # Compression Method 2 bytes Method used to compress the
3004             # Central Directory
3005             # Compressed Size 8 bytes Size of the compressed data
3006             # Original Size 8 bytes Original uncompressed size
3007             # AlgId 2 bytes Encryption algorithm ID
3008             # BitLen 2 bytes Encryption key length
3009             # Flags 2 bytes Encryption flags
3010             # HashID 2 bytes Hash algorithm identifier
3011             # Hash Length 2 bytes Length of hash data
3012             # Hash Data (variable) Hash data
3013              
3014 0         0 my ($bcm, $compressedMethod) = read_v();
3015 0         0 out $bcm, "Compression Method", compressionMethod($compressedMethod) ;
3016             info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2)
3017 0 0       0 if ! defined $ZIP_CompressionMethods{$compressedMethod} ;
3018 0         0 out_Q "Compressed Size";
3019 0         0 out_Q "Uncompressed Size";
3020 0   0 0   0 out_v "AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ;
  0         0  
3021 0         0 out_v "BitLen";
3022 0   0 0   0 out_v "Flags", sub { $FlagsLookup{ $_[0] } // "reserved for certificate processing" } ;
  0         0  
3023 0   0 0   0 out_v "HashID", sub { $HashIDLookup{ $_[0] } // "Unknown ID" } ;
  0         0  
3024              
3025 0         0 my $hashLen = out_v "Hash Length ";
3026 0         0 outHexdump($hashLen, "Hash Data");
3027              
3028 0         0 $remaining -= $hashLen + 28;
3029             }
3030              
3031 0         0 my $entry = Zip64EndCentralHeaderEntry->new();
3032              
3033 0 0       0 if ($remaining)
3034             {
3035             # Handle 'zip64 extensible data sector' here
3036             # See APPNOTE 6.3.10, section 4.3.14.3, 4.3.14.4 & APPENDIX C
3037             # Not seen a real example of this. Tested with hand crafted files.
3038 0         0 walkExtra($remaining, $entry);
3039             }
3040              
3041 0         0 return {};
3042             }
3043              
3044              
3045             sub Zip64EndCentralLocator
3046             {
3047             # APPNOTE 6.3.10, sec 4.3.15
3048              
3049 0     0   0 my $signature = shift ;
3050 0         0 my $data = shift ;
3051 0         0 my $startRecordOffset = shift ;
3052              
3053 0         0 print "\n";
3054 0         0 out $data, "ZIP64 END CENTRAL DIR LOCATOR", Value_V($signature);
3055              
3056 0         0 need 16, Signatures::name($signature);
3057              
3058             # my ($nextRecord, $deltaActuallyAvailable) = $HeaderOffsetIndex->checkForOverlap(16);
3059              
3060             # if ($deltaActuallyAvailable)
3061             # {
3062             # fatal_truncated_record(
3063             # sprintf("ZIP64 END CENTRAL DIR LOCATOR \@%X truncated", $FH->tell() - 4),
3064             # sprintf("Need 0x%X bytes, have 0x%X available", 16, $deltaActuallyAvailable),
3065             # sprintf("Next Record is %s \@0x%X", $nextRecord->name(), $nextRecord->offset())
3066             # )
3067             # }
3068              
3069             # TODO - check values for traces of multi-part + crazy offsets
3070 0         0 out_V "Central Dir Disk no";
3071              
3072 0         0 my ($d, $zip64EndCentralDirOffset) = read_Q();
3073 0         0 my $out = Value_Q($zip64EndCentralDirOffset);
3074 0         0 testPossiblePrefix($zip64EndCentralDirOffset, ZIP64_END_CENTRAL_REC_HDR_SIG);
3075              
3076 0 0       0 $out .= " [Actual Offset is " . Value_Q($zip64EndCentralDirOffset + $PREFIX_DELTA) . "]"
3077             if $PREFIX_DELTA ;
3078 0         0 out $d, "Offset to Zip64 EOCD", $out;
3079              
3080 0         0 my $totalDisks = out_V "Total no of Disks";
3081              
3082 0 0       0 if ($totalDisks > 0)
3083             {
3084 0         0 my $commonMessage = "'Offset to Zip64 End of Central Directory Record' field in '" . Signatures::name($signature) . "' is invalid";
3085 0         0 $zip64EndCentralDirOffset = checkOffsetValue($zip64EndCentralDirOffset, $startRecordOffset, 0, $commonMessage, $FH->tell() - 12, ZIP64_END_CENTRAL_REC_HDR_SIG) ;
3086             }
3087              
3088 0         0 return {};
3089             }
3090              
3091             sub needZip64EOCDLocator
3092             {
3093             # zip64 end of central directory field needed if any of the fields
3094             # in the End Central Header record are maxed out
3095              
3096 8     8   19 my $diskNumber = shift ;
3097 8         14 my $cdDiskNumber = shift ;
3098 8         13 my $entriesOnThisDisk = shift ;
3099 8         15 my $totalEntries = shift ;
3100 8         23 my $centralDirSize = shift ;
3101 8         14 my $centralDirOffset = shift ;
3102              
3103 8   33     36 return (full16($diskNumber) || # 4.4.19
3104             full16($cdDiskNumber) || # 4.4.20
3105             full16($entriesOnThisDisk) || # 4.4.21
3106             full16($totalEntries) || # 4.4.22
3107             full32($centralDirSize) || # 4.4.23
3108             full32($centralDirOffset) # 4.4.24
3109             ) ;
3110             }
3111              
3112             sub emptyArchive
3113             {
3114 6     6   14 my $offset = shift;
3115 6         11 my $diskNumber = shift ;
3116 6         12 my $cdDiskNumber = shift ;
3117 6         10 my $entriesOnThisDisk = shift ;
3118 6         14 my $totalEntries = shift ;
3119 6         105 my $centralDirSize = shift ;
3120 6         14 my $centralDirOffset = shift ;
3121              
3122             return (#$offset == 0 &&
3123 6   0     196 $diskNumber == 0 &&
3124             $cdDiskNumber == 0 &&
3125             $entriesOnThisDisk == 0 &&
3126             $totalEntries == 0 &&
3127             $centralDirSize == 0 &&
3128             $centralDirOffset== 0
3129             ) ;
3130             }
3131              
3132             sub EndCentralHeader
3133             {
3134             # APPNOTE 6.3.10, sec 4.3.16
3135              
3136 6     6   43 my $signature = shift ;
3137 6         17 my $data = shift ;
3138 6         13 my $startRecordOffset = shift ;
3139              
3140 6         156 print "\n";
3141 6         27 out $data, "END CENTRAL HEADER", Value_V($signature);
3142              
3143 6         26 need 18, Signatures::name($signature);
3144              
3145             # TODO - check values for traces of multi-part + crazy values
3146 6         22 my $diskNumber = out_v "Number of this disk";
3147 6         23 my $cdDiskNumber = out_v "Central Dir Disk no";
3148 6         51 my $entriesOnThisDisk = out_v "Entries in this disk";
3149 6         49 my $totalEntries = out_v "Total Entries";
3150 6         68 my $centralDirSize = out_V "Size of Central Dir";
3151              
3152 6         23 my ($d, $centralDirOffset) = read_V();
3153 6         23 my $out = Value_V($centralDirOffset);
3154 6         37 testPossiblePrefix($centralDirOffset, ZIP_CENTRAL_HDR_SIG);
3155              
3156 6 50 33     38 $out .= " [Actual Offset is " . Value_V($centralDirOffset + $PREFIX_DELTA) . "]"
3157             if $PREFIX_DELTA && $centralDirOffset != MAX32 ;
3158 6         34 out $d, "Offset to Central Dir", $out;
3159              
3160 6         24 my $comment_length = out_v "Comment Length";
3161              
3162 6 50       30 if ($comment_length)
3163             {
3164 0         0 my $here = $FH->tell() ;
3165 0         0 my $available = $FILELEN - $here ;
3166 0 0       0 if ($available < $comment_length)
3167             {
3168 0         0 error $here,
3169             "file truncated while reading 'Comment' field in '" . Signatures::name($signature) . "'",
3170             expectedMessage($comment_length, $available);
3171 0         0 $comment_length = $available;
3172             }
3173              
3174 0 0       0 if ($comment_length)
3175             {
3176 0         0 my $comment ;
3177 0         0 myRead($comment, $comment_length);
3178 0         0 outputFilename $comment, 0, "Comment";
3179             }
3180             }
3181              
3182 6 50       74 if ( ! Nesting::isNested($startRecordOffset, $FH->tell() -1))
3183             {
3184             # Not nested
3185 6 50 33     31 if (! needZip64EOCDLocator($diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset) &&
3186             ! emptyArchive($startRecordOffset, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset))
3187             {
3188 6         25 my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name($signature) . "' is invalid";
3189 6         25 $centralDirOffset = checkOffsetValue($centralDirOffset, $startRecordOffset, $centralDirSize, $commonMessage, $startRecordOffset + 16, ZIP_CENTRAL_HDR_SIG) ;
3190             }
3191             }
3192             # else do nothing
3193              
3194 6         37 return {};
3195             }
3196              
3197             sub DataDescriptor
3198             {
3199              
3200             # Data header record or Spanned archive marker.
3201             #
3202              
3203             # ZIP_DATA_HDR_SIG at start of file flags a spanned zip file.
3204             # If it is a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG
3205             # See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5
3206              
3207             # If not at start of file, assume a Data Header Record
3208             # See APPNOTE 6.3.10, sec 4.3.9 & 4.3.9.3
3209              
3210 0     0   0 my $signature = shift ;
3211 0         0 my $data = shift ;
3212 0         0 my $startRecordOffset = shift ;
3213              
3214 0         0 my $here = $FH->tell();
3215              
3216 0 0       0 if ($here == 4)
3217             {
3218             # Spanned Archive Marker
3219 0         0 out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
3220 0         0 return;
3221              
3222             # my (undef, $next_sig) = read_V();
3223             # seekTo(0);
3224              
3225             # if ($next_sig == ZIP_LOCAL_HDR_SIG)
3226             # {
3227             # print "\n";
3228             # out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature);
3229             # seekTo($here);
3230             # return;
3231             # }
3232             }
3233              
3234 0         0 my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG);
3235              
3236 0         0 print "\n";
3237 0         0 out $data, $sigName, Value_V($signature);
3238              
3239 0         0 need 24, Signatures::name($signature);
3240              
3241             # Ignore header payload if nested (assume 64-bit descriptor)
3242 0 0       0 if (Nesting::isNested( $here - 4, $here - 4 + 24 - 1))
3243             {
3244 0         0 out "", "Skipping Nested Payload";
3245 0         0 return {};
3246             }
3247              
3248 0         0 my $compressedSize;
3249             my $uncompressedSize;
3250              
3251 0         0 my $localEntry = $LocalDirectory->lastStreamedEntryAdded();
3252 0   0     0 my $centralEntry = $localEntry && $localEntry->getCdEntry ;
3253              
3254 0 0       0 if (!$localEntry)
3255             {
3256             # found a Data Descriptor without a local header
3257 0         0 out "", "Skipping Data Descriptor", "No matching Local header with streaming bit set";
3258 0         0 error $here - 4, "Orphan '$sigName' found", "No matching Local header with streaming bit set";
3259 0         0 return {};
3260             }
3261              
3262 0         0 my $crc = out_V "CRC";
3263 0         0 my $payloadLength = $here - 4 - $localEntry->payloadOffset;
3264              
3265 0         0 my $deltaToNext = deltaToNextSignature();
3266 0         0 my $cl32 = unpack "V", peekAtOffset($here + 4, 4);
3267 0         0 my $cl64 = unpack "Q<", peekAtOffset($here + 4, 8);
3268              
3269             # use delta to next header & payload length
3270             # deals with use case where the payload length < 32 bit
3271             # will use a 32-bit value rather than the 64-bit value
3272              
3273             # see if delta & payload size match
3274 0 0 0     0 if ($deltaToNext == 16 && $cl64 == $payloadLength)
    0 0        
    0          
    0          
    0          
    0          
3275             {
3276 0 0 0     0 if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
      0        
3277             {
3278 0         0 error $here, "'$sigName': expected 32-bit values, got 64-bit";
3279             }
3280              
3281 0         0 $compressedSize = out_Q "Compressed Size" ;
3282 0         0 $uncompressedSize = out_Q "Uncompressed Size" ;
3283             }
3284             elsif ($deltaToNext == 8 && $cl32 == $payloadLength)
3285             {
3286 0 0       0 if ($localEntry->zip64)
3287             {
3288 0         0 error $here, "'$sigName': expected 64-bit values, got 32-bit";
3289             }
3290              
3291 0         0 $compressedSize = out_V "Compressed Size" ;
3292 0         0 $uncompressedSize = out_V "Uncompressed Size" ;
3293             }
3294              
3295             # Try matching just payload lengths
3296             elsif ($cl32 == $payloadLength)
3297             {
3298 0 0       0 if ($localEntry->zip64)
3299             {
3300 0         0 error $here, "'$sigName': expected 64-bit values, got 32-bit";
3301             }
3302              
3303 0         0 $compressedSize = out_V "Compressed Size" ;
3304 0         0 $uncompressedSize = out_V "Uncompressed Size" ;
3305              
3306 0         0 warning $here, "'$sigName': Zip Header not directly after Data Descriptor";
3307             }
3308             elsif ($cl64 == $payloadLength)
3309             {
3310 0 0 0     0 if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
      0        
3311             {
3312 0         0 error $here, "'$sigName': expected 32-bit values, got 64-bit";
3313             }
3314              
3315 0         0 $compressedSize = out_Q "Compressed Size" ;
3316 0         0 $uncompressedSize = out_Q "Uncompressed Size" ;
3317              
3318 0         0 warning $here, "'$sigName': Zip Header not directly after Data Descriptor";
3319             }
3320              
3321             # payloads don't match, so try delta
3322             elsif ($deltaToNext == 16)
3323             {
3324 0 0 0     0 if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64))
      0        
3325             {
3326 0         0 error $here, "'$sigName': expected 32-bit values, got 64-bit";
3327             }
3328              
3329 0         0 $compressedSize = out_Q "Compressed Size" ;
3330             # compressed size is wrong
3331 0         0 error $here, "'$sigName': Compressed size" . decimalHex0x($compressedSize) . " doesn't match with payload size " . decimalHex0x($payloadLength);
3332              
3333 0         0 $uncompressedSize = out_Q "Uncompressed Size" ;
3334             }
3335             elsif ($deltaToNext == 8 )
3336             {
3337 0 0       0 if ($localEntry->zip64)
3338             {
3339 0         0 error $here, "'$sigName': expected 64-bit values, got 32-bit";
3340             }
3341              
3342 0         0 $compressedSize = out_V "Compressed Size" ;
3343             # compressed size is wrong
3344 0         0 error $here, "'$sigName': Compressed Size " . decimalHex0x($compressedSize) . " doesn't match with payload size " . decimalHex0x($payloadLength);
3345              
3346 0         0 $uncompressedSize = out_V "Uncompressed Size" ;
3347             }
3348              
3349             # no payload or delta match at all, so likely a false positive or data corruption
3350             else
3351             {
3352 0         0 warning $here, "Cannot determine size of Data Descriptor record";
3353             }
3354              
3355             # TODO - neither payload size or delta to next signature match
3356              
3357 0 0       0 if ($localEntry)
3358             {
3359 0         0 $localEntry->readDataDescriptor(1) ;
3360 0         0 $localEntry->crc32($crc) ;
3361 0         0 $localEntry->compressedSize($compressedSize) ;
3362 0         0 $localEntry->uncompressedSize($uncompressedSize) ;
3363             }
3364              
3365             # Defer test for directory payload until central header processing.
3366             # Need to have external file attributes to deal with sme edge conditions.
3367             # # APPNOTE 6.3.10, sec 4.3.8
3368             # my $filename = $localEntry->filename;
3369             # warning undef, "Directory '$filename' must not have a payload"
3370             # if $filename =~ m#/$# && $uncompressedSize ;
3371              
3372             return {
3373 0         0 crc => $crc,
3374             compressedSize => $compressedSize,
3375             uncompressedSize => $uncompressedSize,
3376             };
3377             }
3378              
3379             sub SingleSegmentMarker
3380             {
3381             # ZIP_SINGLE_SEGMENT_MARKER at start of file flags a spanned zip file.
3382             # If this ia a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG
3383             # See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5
3384              
3385 0     0   0 my $signature = shift ;
3386 0         0 my $data = shift ;
3387 0         0 my $startRecordOffset = shift ;
3388              
3389 0         0 my $here = $FH->tell();
3390              
3391 0 0       0 if ($here == 4)
3392             {
3393 0         0 my (undef, $next_sig) = read_V();
3394 0 0       0 if ($next_sig == ZIP_LOCAL_HDR_SIG)
3395             {
3396 0         0 print "\n";
3397 0         0 out $data, "SPLIT ARCHIVE SINGLE-SEGMENT MARKER", Value_V($signature);
3398             }
3399 0         0 seekTo($here);
3400             }
3401              
3402 0         0 return {};
3403             }
3404              
3405             sub ArchiveExtraDataRecord
3406             {
3407             # TODO - not seen an example of this record
3408              
3409             # APPNOTE 6.3.10, sec 4.3.11
3410              
3411 0     0   0 my $signature = shift ;
3412 0         0 my $data = shift ;
3413 0         0 my $startRecordOffset = shift ;
3414              
3415 0         0 out $data, "ARCHIVE EXTRA DATA RECORD", Value_V($signature);
3416              
3417 0         0 need 2, Signatures::name($signature);
3418              
3419 0         0 my $size = out_v "Size of record";
3420              
3421 0         0 need $size, Signatures::name($signature);
3422              
3423 0         0 outHexdump($size, "Field data", 1);
3424              
3425 0         0 return {};
3426             }
3427              
3428             sub DigitalSignature
3429             {
3430 0     0   0 my $signature = shift ;
3431 0         0 my $data = shift ;
3432 0         0 my $startRecordOffset = shift ;
3433              
3434 0         0 print "\n";
3435 0         0 out $data, "DIGITAL SIGNATURE RECORD", Value_V($signature);
3436              
3437 0         0 need 2, Signatures::name($signature);
3438 0         0 my $Size = out_v "Size of record";
3439              
3440 0         0 need $Size, Signatures::name($signature);
3441              
3442              
3443 0         0 myRead(my $payload, $Size);
3444 0         0 out $payload, "Signature", hexDump16($payload);
3445              
3446 0         0 return {};
3447             }
3448              
3449             sub GeneralPurposeBits
3450             {
3451 24     24   193 my $method = shift;
3452 24         41 my $gp = shift;
3453              
3454 24 50       262 out1 "[Bit 0]", "1 'Encryption'" if $gp & ZIP_GP_FLAG_ENCRYPTED_MASK;
3455              
3456 24         143 my %lookup = (
3457             0 => "Normal Compression",
3458             1 => "Maximum Compression",
3459             2 => "Fast Compression",
3460             3 => "Super Fast Compression");
3461              
3462              
3463 24 50       62 if ($method == ZIP_CM_DEFLATE)
3464             {
3465 0         0 my $mid = ($gp >> 1) & 0x03 ;
3466              
3467 0         0 out1 "[Bits 1-2]", "$mid '$lookup{$mid}'";
3468             }
3469              
3470 24 50       72 if ($method == ZIP_CM_LZMA)
3471             {
3472 0 0       0 if ($gp & ZIP_GP_FLAG_LZMA_EOS_PRESENT) {
3473 0         0 out1 "[Bit 1]", "1 'LZMA EOS Marker Present'" ;
3474             }
3475             else {
3476 0         0 out1 "[Bit 1]", "0 'LZMA EOS Marker Not Present'" ;
3477             }
3478             }
3479              
3480 24 50       60 if ($method == ZIP_CM_IMPLODE) # Imploding
3481             {
3482 0 0       0 out1 "[Bit 1]", ($gp & (1 << 1) ? "1 '8k" : "0 '4k") . " Sliding Dictionary'" ;
3483 0 0       0 out1 "[Bit 2]", ($gp & (2 << 1) ? "1 '3" : "0 '2" ) . " Shannon-Fano Trees'" ;
3484             }
3485              
3486 24 50       54 out1 "[Bit 3]", "1 'Streamed'" if $gp & ZIP_GP_FLAG_STREAMING_MASK;
3487 24 50       68 out1 "[Bit 4]", "1 'Enhanced Deflating'" if $gp & 1 << 4;
3488 24 50       149 out1 "[Bit 5]", "1 'Compressed Patched'" if $gp & ZIP_GP_FLAG_PATCHED_MASK ;
3489 24 50       58 out1 "[Bit 6]", "1 'Strong Encryption'" if $gp & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK;
3490 24 50       59 out1 "[Bit 11]", "1 'Language Encoding'" if $gp & ZIP_GP_FLAG_LANGUAGE_ENCODING;
3491 24 50       57 out1 "[Bit 12]", "1 'Pkware Enhanced Compression'" if $gp & ZIP_GP_FLAG_PKWARE_ENHANCED_COMP ;
3492 24 50       52 out1 "[Bit 13]", "1 'Encrypted Central Dir'" if $gp & ZIP_GP_FLAG_ENCRYPTED_CD ;
3493              
3494 24         66 return ();
3495             }
3496              
3497              
3498             sub seekSet
3499             {
3500 0     0   0 my $fh = $_[0] ;
3501 0         0 my $size = $_[1];
3502              
3503 6     6   65 use Fcntl qw(SEEK_SET);
  6         12  
  6         1307  
3504 0         0 seek($fh, $size, SEEK_SET);
3505              
3506             }
3507              
3508             sub skip
3509             {
3510 4     4   8 my $fh = $_[0] ;
3511 4         7 my $size = $_[1];
3512              
3513 6     6   42 use Fcntl qw(SEEK_CUR);
  6         12  
  6         14082  
3514 4         91 seek($fh, $size, SEEK_CUR);
3515              
3516             }
3517              
3518              
3519             sub myRead
3520             {
3521 458     458   779 my $got = \$_[0] ;
3522 458         778 my $size = $_[1];
3523              
3524 458         707 my $wantSize = $size;
3525 458         796 $$got = '';
3526              
3527 458 50       1031 if ($size == 0)
3528             {
3529 0         0 return ;
3530             }
3531              
3532 458 50       1412 if ($size > 0)
3533             {
3534 458         630 my $buff ;
3535 458         1592 my $status = $FH->read($buff, $size);
3536 458 50       4686 return $status
3537             if $status < 0;
3538 458         1121 $$got .= $buff ;
3539             }
3540              
3541 458         662 my $len = length $$got;
3542             # fatal undef, "Truncated file (got $len, wanted $wantSize): $!"
3543 458 50       1095 fatal undef, "Unexpected zip file truncation",
3544             expectedMessage($wantSize, $len)
3545             if length $$got != $wantSize;
3546             }
3547              
3548             sub expectedMessage
3549             {
3550 0     0   0 my $expected = shift;
3551 0         0 my $got = shift;
3552 0         0 return "Expected " . decimalHex0x($expected) . " bytes, but only " . decimalHex0x($got) . " available"
3553             }
3554              
3555             sub need
3556             {
3557 56     56   106 my $byteCount = shift ;
3558 56         126 my $message = shift ;
3559 56   100     205 my $field = shift // '';
3560              
3561             # return $FILELEN - $FH->tell() >= $byteCount;
3562 56         192 my $here = $FH->tell() ;
3563 56         329 my $available = $FILELEN - $here ;
3564 56 50       164 if ($available < $byteCount)
3565             {
3566 0         0 my @message ;
3567              
3568 0 0       0 if ($field)
3569             {
3570 0         0 push @message, "Unexpected zip file truncation while reading '$field' field in '$message'";
3571             }
3572             else
3573             {
3574 0         0 push @message, "Unexpected zip file truncation while reading '$message'";
3575             }
3576              
3577              
3578 0         0 push @message, expectedMessage($byteCount, $available);
3579             # push @message, sprintf("Expected 0x%X bytes, but only 0x%X available", $byteCount, $available);
3580 0 0 0     0 push @message, "Try running with --walk' or '--scan' options"
3581             if ! $opt_scan && ! $opt_walk ;
3582              
3583 0         0 fatal $here, @message;
3584             }
3585             }
3586              
3587             sub testPossiblePrefix
3588             {
3589 18     18   32 my $offset = shift;
3590 18         37 my $expectedSignature = shift ;
3591              
3592 18 50       53 if (testPossiblePrefixNoPREFIX_DELTA($offset, $expectedSignature))
3593             {
3594 0         0 $PREFIX_DELTA = $POSSIBLE_PREFIX_DELTA;
3595 0         0 $POSSIBLE_PREFIX_DELTA = 0;
3596              
3597 0         0 reportPrefixData();
3598              
3599 0         0 return 1
3600             }
3601              
3602 18         33 return 0
3603             }
3604              
3605             sub testPossiblePrefixNoPREFIX_DELTA
3606             {
3607 18     18   31 my $offset = shift;
3608 18         28 my $expectedSignature = shift ;
3609              
3610 18 50 33     200 return 0
      33        
3611             if $offset + 4 > $FILELEN || ! $POSSIBLE_PREFIX_DELTA || $PREFIX_DELTA;
3612              
3613 0         0 my $currentOFFSET = $OFFSET;
3614 0         0 my $gotSig = readSignatureFromOffset($offset);
3615              
3616 0 0       0 if ($gotSig == $expectedSignature)
3617             {
3618             # do have possible prefix data, but the offset is correct
3619 0         0 $POSSIBLE_PREFIX_DELTA = $PREFIX_DELTA = 0;
3620 0         0 $OFFSET = $currentOFFSET;
3621              
3622 0         0 return 0;
3623             }
3624              
3625 0         0 $gotSig = readSignatureFromOffset($offset + $POSSIBLE_PREFIX_DELTA);
3626              
3627 0         0 $OFFSET = $currentOFFSET;
3628              
3629 0         0 return ($gotSig == $expectedSignature) ;
3630             }
3631              
3632             sub offsetIsValid
3633             {
3634 24     24   38 my $offset = shift;
3635 24         37 my $headerStart = shift;
3636 24         45 my $centralDirSize = shift;
3637 24         41 my $commonMessage = shift ;
3638 24         50 my $expectedSignature = shift ;
3639 24         41 my $dereferencePointer = shift;
3640              
3641 24         34 my $must_point_back = 1;
3642              
3643 24         49 my $delta = $offset - $FILELEN + 1 ;
3644              
3645 24 50       87 $offset += $PREFIX_DELTA
3646             if $PREFIX_DELTA ;
3647              
3648 24 50       72 return sprintf("value %s is %s bytes past EOF", decimalHex0x($offset), decimalHex0x($delta))
3649             if $delta > 0 ;
3650              
3651 24 50 33     93 return sprintf "value %s must be less that %s", decimalHex0x($offset), decimalHex0x($headerStart)
3652             if $must_point_back && $offset >= $headerStart;
3653              
3654 24 50       83 if ($dereferencePointer)
3655             {
3656 24         45 my $actual = $headerStart - $centralDirSize;
3657 24         44 my $cdSizeOK = ($actual == $offset);
3658 24         38 my $possibleDelta = $actual - $offset;
3659              
3660 24 50 66     193 if ($centralDirSize && ! $cdSizeOK && $possibleDelta > 0 && readSignatureFromOffset($possibleDelta) == ZIP_LOCAL_HDR_SIG)
      33        
      33        
3661             {
3662             # If testing end of central dir, check if the location of the first CD header
3663             # is consistent with the central dir size.
3664             # Common use case is a SFX zip file
3665              
3666 0         0 my $gotSig = readSignatureFromOffset($actual);
3667 0         0 my $v = hexValue32($gotSig);
3668 0 0       0 return 'value @ ' . hexValue($actual) . " should decode to signature for " . Signatures::nameAndHex($expectedSignature) . ". Got $v" # . hexValue32($gotSig)
3669             if $gotSig != $expectedSignature ;
3670              
3671 0         0 $PREFIX_DELTA = $possibleDelta;
3672 0         0 reportPrefixData();
3673              
3674 0         0 return undef;
3675             }
3676             else
3677             {
3678 24         118 my $gotSig = readSignatureFromOffset($offset);
3679 24         67 my $v = hexValue32($gotSig);
3680 24 50       81 return 'value @ ' . hexValue($offset) . " should decode to signature for " . Signatures::nameAndHex($expectedSignature) . ". Got $v" # . hexValue32($gotSig)
3681             if $gotSig != $expectedSignature ;
3682             }
3683             }
3684              
3685 24         54 return undef ;
3686             }
3687              
3688             sub checkOffsetValue
3689             {
3690 24     24   71 my $offset = shift;
3691 24         35 my $headerStart = shift;
3692 24         42 my $centralDirSize = shift;
3693 24         44 my $commonMessage = shift ;
3694 24         35 my $messageOffset = shift;
3695 24         48 my $expectedSignature = shift ;
3696 24   100     111 my $fatal = shift // 0;
3697 24   50     117 my $dereferencePointer = shift // 1;
3698              
3699 24         42 my $keepOFFSET = $OFFSET ;
3700              
3701 24         73 my $message = offsetIsValid($offset, $headerStart, $centralDirSize, $commonMessage, $expectedSignature, $dereferencePointer);
3702 24 50       54 if ($message)
3703             {
3704 0 0       0 fatal_tryWalk($messageOffset, $commonMessage, $message)
3705             if $fatal;
3706              
3707 0 0       0 error $messageOffset, $commonMessage, $message
3708             if ! $fatal;
3709             }
3710              
3711 24         40 $OFFSET = $keepOFFSET;
3712              
3713 24         60 return $offset + $PREFIX_DELTA;
3714              
3715             }
3716              
3717             sub fatal_tryWalk
3718             {
3719 0     0   0 my $offset = shift ;
3720 0         0 my $message = shift;
3721              
3722 0         0 fatal($offset, $message, @_, "Try running with --walk' or '--scan' options");
3723             }
3724              
3725             sub fatal
3726             {
3727 0     0   0 my $offset = shift ;
3728 0         0 my $message = shift;
3729              
3730 0 0       0 return if $fatalDisabled;
3731              
3732 0 0       0 if (defined $offset)
3733             {
3734 0         0 warn "#\n# FATAL: Offset " . hexValue($offset) . ": $message\n";
3735             }
3736             else
3737             {
3738 0         0 warn "#\n# FATAL: $message\n";
3739             }
3740              
3741             warn "# $_ . \n"
3742 0         0 for @_;
3743 0         0 warn "#\n" ;
3744              
3745 0         0 exit 1;
3746             }
3747              
3748             sub disableFatal
3749             {
3750 0     0   0 $fatalDisabled = 1 ;
3751             }
3752              
3753             sub enableFatal
3754             {
3755 0     0   0 $fatalDisabled = 0 ;
3756             }
3757              
3758             sub topLevelFatal
3759             {
3760 0     0   0 my $message = shift ;
3761              
3762 6     6   59 no warnings 'utf8';
  6         12  
  6         1155  
3763              
3764 0         0 warn "FATAL: $message\n";
3765              
3766             warn "$_ . \n"
3767 0         0 for @_;
3768              
3769 0         0 exit 1;
3770             }
3771              
3772             sub internalFatal
3773             {
3774 0     0   0 my $offset = shift ;
3775 0         0 my $message = shift;
3776              
3777 6     6   46 no warnings 'utf8';
  6         13  
  6         1828  
3778              
3779 0 0       0 if (defined $offset)
3780             {
3781 0         0 warn "# FATAL: Offset " . hexValue($offset) . ": Internal Error: $message\n";
3782             }
3783             else
3784             {
3785 0         0 warn "# FATAL: Internal Error: $message\n";
3786             }
3787              
3788             warn "# $_ \n"
3789 0         0 for @_;
3790              
3791 0         0 warn "# Please report error at https://github.com/pmqs/zipdetails/issues\n";
3792 0         0 exit 1;
3793             }
3794              
3795             sub warning
3796             {
3797 0     0   0 my $offset = shift ;
3798 0         0 my $message = shift;
3799              
3800 6     6   48 no warnings 'utf8';
  6         13  
  6         2002  
3801              
3802             return
3803 0 0       0 unless $opt_want_warning_messages ;
3804              
3805 0 0       0 say "#"
3806             unless $lastWasMessage ++ ;
3807              
3808 0 0       0 if (defined $offset)
3809             {
3810 0         0 say "# WARNING: Offset " . hexValue($offset) . ": $message";
3811             }
3812             else
3813             {
3814 0         0 say "# WARNING: $message";
3815             }
3816              
3817              
3818 0         0 say "# $_" for @_ ;
3819 0         0 say "#";
3820 0         0 ++ $WarningCount ;
3821              
3822 0 0       0 $exit_status_code |= 2
3823             if $opt_want_message_exit_status ;
3824             }
3825              
3826             sub error
3827             {
3828 0     0   0 my $offset = shift ;
3829 0         0 my $message = shift;
3830              
3831 6     6   45 no warnings 'utf8';
  6         12  
  6         1905  
3832              
3833             return
3834 0 0       0 unless $opt_want_error_messages ;
3835              
3836 0 0       0 say "#"
3837             unless $lastWasMessage ++ ;
3838              
3839 0 0       0 if (defined $offset)
3840             {
3841 0         0 say "# ERROR: Offset " . hexValue($offset) . ": $message";
3842             }
3843             else
3844             {
3845 0         0 say "# ERROR: $message";
3846             }
3847              
3848              
3849 0         0 say "# $_" for @_ ;
3850 0         0 say "#";
3851              
3852 0         0 ++ $ErrorCount ;
3853              
3854 0 0       0 $exit_status_code |= 4
3855             if $opt_want_message_exit_status ;
3856             }
3857              
3858             sub debug
3859             {
3860 0     0   0 my $offset = shift ;
3861 0         0 my $message = shift;
3862              
3863 6     6   51 no warnings 'utf8';
  6         15  
  6         1614  
3864              
3865 0 0       0 say "#"
3866             unless $lastWasMessage ++ ;
3867              
3868 0 0       0 if (defined $offset)
3869             {
3870 0         0 say "# DEBUG: Offset " . hexValue($offset) . ": $message";
3871             }
3872             else
3873             {
3874 0         0 say "# DEBUG: $message";
3875             }
3876              
3877              
3878 0         0 say "# $_" for @_ ;
3879 0         0 say "#";
3880             }
3881              
3882             sub internalError
3883             {
3884 0     0   0 my $message = shift;
3885              
3886 6     6   45 no warnings 'utf8';
  6         15  
  6         1868  
3887              
3888 0         0 say "#";
3889 0         0 say "# ERROR: $message";
3890 0         0 say "# $_" for @_ ;
3891 0         0 say "# Please report error at https://github.com/pmqs/zipdetails/issues";
3892 0         0 say "#";
3893              
3894 0         0 ++ $ErrorCount ;
3895             }
3896              
3897             sub reportPrefixData
3898             {
3899 4   33 4   42 my $delta = shift // $PREFIX_DELTA ;
3900 4         10 state $reported = 0;
3901 4 50 33     35 return if $reported || $delta == 0;
3902              
3903 0         0 info 0, "found " . decimalHex0x($delta) . " bytes before beginning of zipfile" ;
3904 0         0 $reported = 1;
3905             }
3906              
3907             sub info
3908             {
3909 0     0   0 my $offset = shift;
3910 0         0 my $message = shift;
3911              
3912 6     6   47 no warnings 'utf8';
  6         12  
  6         48770  
3913              
3914             return
3915 0 0       0 unless $opt_want_info_messages ;
3916              
3917 0 0       0 say "#"
3918             unless $lastWasMessage ++ ;
3919              
3920 0 0       0 if (defined $offset)
3921             {
3922 0         0 say "# INFO: Offset " . hexValue($offset) . ": $message";
3923             }
3924             else
3925             {
3926 0         0 say "# INFO: $message";
3927             }
3928              
3929 0         0 say "# $_" for @_ ;
3930 0         0 say "#";
3931              
3932 0         0 ++ $InfoCount ;
3933              
3934 0 0       0 $exit_status_code |= 1
3935             if $opt_want_message_exit_status ;
3936             }
3937              
3938             sub walkExtra
3939             {
3940             # APPNOTE 6.3.10, sec 4.4.11, 4.4.28, 4.5
3941 0     0   0 my $XLEN = shift;
3942 0         0 my $entry = shift;
3943              
3944             # Caller has determined that there are $XLEN bytes available to read
3945              
3946 0         0 my $buff ;
3947 0         0 my $offset = 0 ;
3948              
3949 0         0 my $id;
3950             my $subLen;
3951 0         0 my $payload ;
3952              
3953 0         0 my $count = 0 ;
3954 0         0 my $endExtraOffset = $FH->tell() + $XLEN ;
3955              
3956 0         0 while ($offset < $XLEN) {
3957              
3958 0         0 ++ $count;
3959              
3960             # Detect if there is not enough data for an extra ID and length.
3961             # Android zipalign and zipflinger are prime candidates for these
3962             # non-standard extra sub-fields.
3963 0         0 my $remaining = $XLEN - $offset;
3964 0 0       0 if ($remaining < ZIP_EXTRA_SUBFIELD_HEADER_SIZE) {
3965             # There is not enough left.
3966             # Consume whatever is there and return so parsing
3967             # can continue.
3968              
3969 0         0 myRead($payload, $remaining);
3970 0         0 my $data = hexDump($payload);
3971              
3972 0 0       0 if ($payload =~ /^\x00+$/)
3973             {
3974             # All nulls
3975 0         0 out $payload, "Null Padding in Extra";
3976 0         0 info $FH->tell() - length($payload), decimalHex0x(length $payload) . " Null Padding Bytes in Extra Field" ;
3977             }
3978             else
3979             {
3980 0         0 out $payload, "Extra Data", $data;
3981 0         0 error $FH->tell() - length($payload), "'Extra Data' Malformed";
3982             }
3983              
3984 0         0 return undef;
3985             }
3986              
3987 0         0 myRead($id, ZIP_EXTRA_SUBFIELD_ID_SIZE);
3988 0         0 $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE;
3989 0         0 my $lookID = unpack "v", $id ;
3990 0 0       0 if ($lookID == 0)
3991             {
3992             # check for null padding at end of extra
3993 0         0 my $here = $FH->tell();
3994 0         0 my $rest;
3995 0         0 myRead($rest, $XLEN - $offset);
3996 0 0       0 if ($rest =~ /^\x00+$/)
3997             {
3998 0         0 my $len = length ($id . $rest) ;
3999 0         0 out $id . $rest, "Null Padding in Extra";
4000 0         0 info $FH->tell() - $len, decimalHex0x($len) . " Null Padding Bytes in Extra Field";
4001 0         0 return undef;
4002             }
4003              
4004 0         0 seekTo($here);
4005             }
4006              
4007 0   0     0 my ($who, $decoder, $local_min, $local_max, $central_min, $central_max) = @{ $Extras{$lookID} // ['', undef, undef, undef, undef, undef ] };
  0         0  
4008              
4009 0         0 my $idString = Value_v($lookID) ;
4010 0 0       0 $idString .= " '$who'"
4011             if $who;
4012              
4013 0         0 out $id, "Extra ID #$count", $idString ;
4014             info $FH->tell() - 2, "Unknown Extra ID $idString"
4015 0 0       0 if ! exists $Extras{$lookID} ;
4016              
4017 0         0 myRead($buff, ZIP_EXTRA_SUBFIELD_LEN_SIZE);
4018 0         0 $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE;
4019              
4020 0         0 $subLen = unpack("v", $buff);
4021 0         0 out2 $buff, "Length", Value_v($subLen) ;
4022              
4023 0         0 $remaining = $XLEN - $offset;
4024 0 0       0 if ($subLen > $remaining )
4025             {
4026 0         0 error $FH->tell() -2,
4027             extraFieldIdentifier($lookID) . ": 'Length' field invalid",
4028             sprintf("value %s > %s bytes remaining", decimalHex0x($subLen), decimalHex0x($remaining));
4029 0         0 outSomeData $remaining, " Extra Payload";
4030 0         0 return undef;
4031             }
4032              
4033 0 0       0 if (! defined $decoder)
4034             {
4035 0 0       0 if ($subLen)
4036             {
4037 0         0 myRead($payload, $subLen);
4038 0         0 my $data = hexDump16($payload);
4039              
4040 0         0 out2 $payload, "Extra Payload", $data;
4041             }
4042             }
4043             else
4044             {
4045 0 0       0 if (testExtraLimits($lookID, $subLen, $entry->inCentralDir))
4046             {
4047 0         0 my $endExtraOffset = $FH->tell() + $subLen;
4048 0         0 $decoder->($lookID, $subLen, $entry) ;
4049              
4050             # Belt & Braces - should now be at $endExtraOffset
4051             # error here means issue in an extra handler
4052             # should noy happen, but just in case
4053             # TODO -- need tests for this
4054 0         0 my $here = $FH->tell() ;
4055 0 0       0 if ($here > $endExtraOffset)
    0          
4056             {
4057             # gone too far, so need to bomb out now
4058 0         0 internalFatal $here, "Overflow processing " . extraFieldIdentifier($lookID) . ".",
4059             sprintf("Should be at offset %s, actually at %s", decimalHex0x($endExtraOffset), decimalHex0x($here));
4060             }
4061             elsif ($here < $endExtraOffset)
4062             {
4063             # not gone far enough, can recover
4064 0         0 error $here,
4065             sprintf("Expected to be at offset %s after processing %s, actually at %s", decimalHex0x($endExtraOffset), extraFieldIdentifier($lookID), decimalHex0x($here)),
4066             "Skipping " . decimalHex0x($endExtraOffset - $here) . " bytes";
4067 0         0 outSomeData $endExtraOffset - $here, " Extra Data";
4068             }
4069             }
4070             }
4071              
4072 0         0 $offset += $subLen ;
4073             }
4074              
4075 0         0 return undef ;
4076             }
4077              
4078             sub testExtraLimits
4079             {
4080 0     0   0 my $lookID = shift;
4081 0         0 my $size = shift;
4082 0         0 my $inCentralDir = shift;
4083              
4084 0   0     0 my ($who, undef, $local_min, $local_max, $central_min, $central_max) = @{ $Extras{$lookID} // ['', undef, undef, undef, undef, undef ] };
  0         0  
4085              
4086 0 0       0 my ($min, $max) = $inCentralDir
4087             ? ($central_min, $central_max)
4088             : ($local_min, $local_max) ;
4089              
4090 0 0 0     0 return 1
4091             if ! defined $min && ! defined $max ;
4092              
4093 0 0 0     0 if (defined $min && defined $max)
4094             {
4095             # both the same
4096 0 0       0 if ($min == $max)
4097             {
4098 0 0       0 if ($size != $min)
4099             {
4100 0         0 error $FH->tell() -2, sprintf "%s: 'Length' field invalid: expected %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($size);
4101 0 0       0 outSomeData $size, " Extra Payload" if $size;
4102 0         0 return 0;
4103             }
4104             }
4105             else # min != max
4106             {
4107 0 0 0     0 if ($size < $min || $size > $max)
4108             {
4109 0         0 error $FH->tell() -2, sprintf "%s: 'Length' field invalid: value must be between %s and %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($max), decimalHex0x($size);
4110 0 0       0 outSomeData $size, " Extra Payload" if $size ;
4111 0         0 return 0;
4112             }
4113             }
4114              
4115             }
4116             else # must be defined $min & undefined max
4117             {
4118 0 0       0 if ($size < $min)
4119             {
4120 0         0 error $FH->tell() -2, sprintf "%s: 'Length' field invalid: value must be at least %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($size);
4121 0 0       0 outSomeData $size, " Extra Payload" if $size;
4122 0         0 return 0;
4123             }
4124             }
4125              
4126 0         0 return 1;
4127              
4128             }
4129              
4130             sub full32
4131             {
4132 20   50 20   151 return ($_[0] // 0) == MAX32 ;
4133             }
4134              
4135             sub full16
4136             {
4137 32   50 32   319 return ($_[0] // 0) == MAX16 ;
4138             }
4139              
4140             sub decode_Zip64
4141             {
4142 0     0   0 my $extraID = shift ;
4143 0         0 my $len = shift;
4144 0         0 my $entry = shift;
4145              
4146 0         0 myRead(my $payload, $len);
4147 0 0       0 if ($entry->inCentralDir() )
4148             {
4149 0         0 walk_Zip64_in_CD($extraID, $payload, $entry, 1) ;
4150             }
4151             else
4152             {
4153 0         0 walk_Zip64_in_LD($extraID, $payload, $entry, 1) ;
4154              
4155             }
4156             }
4157              
4158             sub walk_Zip64_in_LD
4159             {
4160 0     0   0 my $extraID = shift ;
4161 0         0 my $zip64Extended = shift;
4162 0         0 my $entry = shift;
4163 0   0     0 my $display = shift // 1 ;
4164              
4165 0         0 my $fieldStart = $FH->tell() - length $zip64Extended;
4166 0         0 my $fieldOffset = $fieldStart ;
4167              
4168 0         0 $ZIP64 = 1;
4169 0         0 $entry->zip64(1);
4170              
4171 0 0       0 if (length $zip64Extended == 0)
4172             {
4173 0         0 info $fieldOffset, extraFieldIdentifier($extraID) . ": Length is Zero";
4174 0         0 return;
4175             }
4176              
4177 0         0 my $assumeLengthsPresent = (length($zip64Extended) == 16) ;
4178 0         0 my $assumeAllFieldsPresent = (length($zip64Extended) == 28) ;
4179              
4180 0 0 0     0 if ($assumeLengthsPresent || $assumeAllFieldsPresent || full32 $entry->std_uncompressedSize )
      0        
4181             {
4182             # TODO defer a warning if in local header & central/local don't have std_uncompressedSizeset to 0xffffffff
4183 0         0 my $fieldName = 'Uncompressed Size';
4184 0 0       0 if (length $zip64Extended < 8)
4185             {
4186 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4187 0         0 error $fieldOffset, $message;
4188 0         0 out2 $zip64Extended, $message;
4189 0         0 return;
4190             }
4191              
4192 0         0 $fieldOffset += 8;
4193              
4194 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4195 0         0 my $value = unpack "Q<", $data;
4196 0 0       0 out2 $data, "$fieldName", Value_Q($value)
4197             if $display;
4198 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_uncompressedSize)
      0        
4199             {
4200 0         0 info $FH->tell() - 8, "'$fieldName' should not be present in the ZIP64 extra field.",
4201             "Corresponding field from Central Header is not set to 0xFFFFFFFF, value is " . decimalHex0x($entry->std_uncompressedSize)
4202             }
4203 0         0 $entry->uncompressedSize($value);
4204             }
4205              
4206 0 0 0     0 if ($assumeLengthsPresent || $assumeAllFieldsPresent || full32 $entry->std_compressedSize)
      0        
4207             {
4208 0         0 my $fieldName = 'Compressed Size';
4209 0 0       0 if (length $zip64Extended < 8)
4210             {
4211 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4212 0         0 error $fieldOffset, $message;
4213 0         0 out2 $zip64Extended, $message;
4214 0         0 return;
4215             }
4216              
4217 0         0 $fieldOffset += 8;
4218              
4219 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4220 0         0 my $value = unpack "Q<", $data;
4221 0 0       0 out2 $data, "$fieldName", Value_Q($value)
4222             if $display;
4223 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_compressedSize)
      0        
4224             {
4225 0         0 info $FH->tell() - 8, "'$fieldName' should not be present in the ZIP64 extra field.",
4226             "Corresponding field from Central Header is not set to 0xFFFFFFFF, value is " . decimalHex0x($entry->std_compressedSize)
4227             }
4228 0         0 $entry->compressedSize($value);
4229             }
4230              
4231             # Zip64 in local header should not have localHeaderOffset or disk number
4232             # but some zip files do
4233              
4234 0 0       0 if ($assumeAllFieldsPresent)
4235             {
4236 0         0 $fieldOffset += 8;
4237              
4238 0         0 my $fieldName = 'Offset to Local Dir';
4239 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4240 0         0 my $value = unpack "Q<", $data;
4241 0 0       0 out2 $data, "$fieldName", Value_Q($value)
4242             if $display;
4243 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_localHeaderOffset)
      0        
4244             {
4245 0         0 info $FH->tell() - 8, "$fieldName should not be present in the ZIP64 extra field for the Local Header."
4246             }
4247             }
4248              
4249 0 0       0 if ($assumeAllFieldsPresent)
4250             {
4251 0         0 $fieldOffset += 4;
4252              
4253 0         0 my $fieldName = 'Disk Number';
4254 0         0 my $data = substr($zip64Extended, 0, 4, "") ;
4255 0         0 my $value = unpack "V", $data;
4256 0         0 my $diskNumber = $value;
4257 0 0       0 out2 $data, "$fieldName", Value_V($diskNumber)
4258             if $display;
4259 0 0 0     0 if ($display && !$entry->streamed && ! full16 $entry->std_diskNumber)
      0        
4260             {
4261 0         0 info $FH->tell() - 4, "$fieldName should not be present in the ZIP64 extra field for the Local Header."
4262             }
4263             }
4264              
4265 0 0       0 if (length $zip64Extended)
4266             {
4267 0 0       0 if ($display)
4268             {
4269 0         0 out2 $zip64Extended, "Unexpected Data", hexDump16 $zip64Extended ;
4270 0         0 info $fieldOffset, extraFieldIdentifier($extraID) . ": Unexpected Data: " . decimalHex0x(length $zip64Extended) . " bytes";
4271             }
4272             }
4273              
4274             }
4275              
4276             sub walk_Zip64_in_CD
4277             {
4278 0     0   0 my $extraID = shift ;
4279 0         0 my $zip64Extended = shift;
4280 0         0 my $entry = shift;
4281 0   0     0 my $display = shift // 1 ;
4282              
4283 0         0 my $fieldStart = $FH->tell() - length $zip64Extended;
4284 0         0 my $fieldOffset = $fieldStart ;
4285              
4286 0         0 $ZIP64 = 1;
4287 0         0 $entry->zip64(1);
4288              
4289 0 0       0 if (length $zip64Extended == 0)
4290             {
4291 0         0 info $fieldOffset, extraFieldIdentifier($extraID) . ": Length is Zero";
4292 0         0 return;
4293             }
4294              
4295             # The order of the fields in the zip64 extended
4296             # information record is fixed, but the fields MUST
4297             # only appear if the corresponding Local or Central
4298             # directory record field is set to 0xFFFF or 0xFFFFFFFF.
4299 0         0 my $assumeAllFieldsPresent = (length($zip64Extended) == 28);
4300              
4301 0 0 0     0 if ($assumeAllFieldsPresent || full32 $entry->std_uncompressedSize)
4302             {
4303 0         0 my $fieldName ="Uncompressed Size" ;
4304 0 0       0 if (length $zip64Extended < 8)
4305             {
4306 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4307 0         0 error $fieldOffset, $message;
4308 0         0 out2 $zip64Extended, $message;
4309 0         0 return;
4310             }
4311              
4312 0         0 $fieldOffset += 8;
4313              
4314 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4315 0         0 my $value = unpack "Q<", $data;
4316 0 0       0 out2 $data, "$fieldName", Value_Q($value)
4317             if $display;
4318 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_uncompressedSize)
      0        
4319             {
4320 0         0 info $FH->tell() - 8, "'$fieldName' should not be present in the ZIP64 extra field.",
4321             "Corresponding field from Central Header is not set to 0xFFFFFFFF, value is " . decimalHex0x($entry->uncompressedSize);
4322             }
4323 0         0 $entry->uncompressedSize($value);
4324             }
4325              
4326 0 0 0     0 if ($assumeAllFieldsPresent || full32 $entry->std_compressedSize)
4327             {
4328 0         0 my $fieldName ="Compressed Size" ;
4329 0 0       0 if (length $zip64Extended < 8)
4330             {
4331 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4332 0         0 error $fieldOffset, $message;
4333 0         0 out2 $zip64Extended, $message;
4334 0         0 return;
4335             }
4336              
4337 0         0 $fieldOffset += 8;
4338              
4339 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4340 0         0 my $value = unpack "Q<", $data;
4341 0 0       0 out2 $data, "$fieldName", Value_Q($value)
4342             if $display;
4343 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_compressedSize)
      0        
4344             {
4345 0         0 info $FH->tell() - 8, "'$fieldName' should not be present in the ZIP64 extra field.",
4346             "Corresponding field from Central Header is not set to 0xFFFFFFFF, value is " . decimalHex0x($entry->compressedSize);
4347             }
4348 0         0 $entry->compressedSize($value);
4349             }
4350              
4351 0 0 0     0 if ($assumeAllFieldsPresent || full32 $entry->std_localHeaderOffset)
4352             {
4353 0         0 my $fieldName = "Offset to Local Dir";
4354 0 0       0 if (length $zip64Extended < 8)
4355             {
4356 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4357 0         0 error $fieldOffset, $message;
4358 0         0 out2 $zip64Extended, $message;
4359 0         0 return;
4360             }
4361              
4362 0         0 $fieldOffset += 8;
4363              
4364 0         0 my $data = substr($zip64Extended, 0, 8, "") ;
4365 0         0 my $value = unpack("Q<", $data);
4366 0 0       0 out2 $data, $fieldName, Value_Q($value)
4367             if $display;
4368              
4369 0         0 my $commonMessage = "'$fieldName' field in 'Zip64 Extra Field' is invalid";
4370 0         0 $value = checkOffsetValue($value, $fieldStart, 0, $commonMessage, $fieldStart, ZIP_LOCAL_HDR_SIG, 0) ;
4371              
4372 0 0 0     0 if ($display && !$entry->streamed && ! full32 $entry->std_localHeaderOffset)
      0        
4373             {
4374 0         0 info $FH->tell() - 8, "'$fieldName' should not be present in the ZIP64 extra field.",
4375             "Corresponding field from Central Header is not set to 0xFFFFFFFF, value is " . decimalHex0x($value);
4376             }
4377 0         0 $entry->localHeaderOffset($value);
4378             }
4379              
4380 0 0 0     0 if ($assumeAllFieldsPresent || full16 $entry->std_diskNumber)
4381             {
4382 0         0 my $fieldName = 'Disk Number';
4383 0 0       0 if (length $zip64Extended < 4)
4384             {
4385 0         0 my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(4) . " bytes for '$fieldName': only " . decimalHex0x(length $zip64Extended) . " bytes present";
4386 0         0 error $fieldOffset, $message;
4387 0         0 out2 $zip64Extended, $message;
4388 0         0 return;
4389             }
4390              
4391 0         0 $fieldOffset += 4;
4392              
4393 0         0 my $data = substr($zip64Extended, 0, 4, "") ;
4394 0         0 my $value = unpack "V", $data;
4395 0 0       0 out2 $data, "$fieldName", Value_V($value)
4396             if $display;
4397 0 0 0     0 if ($display && !$entry->streamed && ! full16 $entry->std_diskNumber)
      0        
4398             {
4399 0         0 info $FH->tell() - 4, "'$fieldName' should not be present in the ZIP64 extra field.",
4400             "Corresponding field from Central Header is not set to 0xFFFF, value is " . decimalHex0x($entry->diskNumber);
4401             }
4402 0         0 $entry->zip64_diskNumberPresent(1);
4403 0         0 $entry->diskNumber($value);
4404             }
4405              
4406 0 0       0 if (length $zip64Extended)
4407             {
4408 0 0       0 if ($display)
4409             {
4410 0         0 out2 $zip64Extended, "Unexpected Data", hexDump16 $zip64Extended ;
4411 0         0 info $fieldOffset, extraFieldIdentifier($extraID) . ": Unexpected Data: " . decimalHex0x(length $zip64Extended) . " bytes";
4412             }
4413             }
4414             }
4415              
4416             sub Ntfs2Unix
4417             {
4418 0     0   0 my $m = shift;
4419 0         0 my $v = shift;
4420              
4421             # NTFS offset is 19DB1DED53E8000
4422              
4423 0         0 my $hex = Value_Q($v) ;
4424              
4425             # Treat empty value as special case
4426             # Could decode to 1 Jan 1601
4427 0 0       0 return "$hex 'No Date/Time'"
4428             if $v == 0;
4429              
4430 0         0 $v -= 0x19DB1DED53E8000 ;
4431 0         0 my $ns = ($v % 10000000) * 100;
4432 0         0 my $elapse = int ($v/10000000);
4433 0         0 return "$hex '" . getT($elapse) .
4434             " " . sprintf("%0dns'", $ns);
4435             }
4436              
4437             sub decode_NTFS_Filetimes
4438             {
4439 0     0   0 my $extraID = shift ;
4440 0         0 my $len = shift;
4441 0         0 my $entry = shift;
4442              
4443 0         0 out_V " Reserved";
4444 0         0 out_v " Tag1";
4445 0         0 out_v " Size1" ;
4446              
4447 0         0 my ($m, $s1) = read_Q;
4448 0         0 out $m, " Mtime", Ntfs2Unix($m, $s1);
4449              
4450 0         0 my ($a, $s3) = read_Q;
4451 0         0 out $a, " Atime", Ntfs2Unix($a, $s3);
4452              
4453 0         0 my ($c, $s2) = read_Q;
4454 0         0 out $c, " Ctime", Ntfs2Unix($c, $s2);
4455             }
4456              
4457             sub OpenVMS_DateTime
4458             {
4459 0     0   0 my $ix = shift;
4460 0         0 my $tag = shift;
4461 0         0 my $size = shift;
4462              
4463             # VMS epoch is 17 Nov 1858
4464             # Offset to Unix Epoch is -0x7C95674C3DA5C0 (-35067168005400000)
4465              
4466 0         0 my ($data, $value) = read_Q();
4467              
4468 0         0 my $datetime = "No Date Time'";
4469 0 0       0 if ($value != 0)
4470             {
4471 0         0 my $v = $value - 0x007C95674C3DA5C0 ;
4472 0         0 my $ns = ($v % 10000000) * 100 ;
4473 0         0 my $seconds = int($v / 10000000) ;
4474 0         0 $datetime = getT($seconds) .
4475             " " . sprintf("%0dns'", $ns);
4476             }
4477              
4478 0         0 out2 $data, " Attribute", Value_Q($value) . " '$datetime";
4479             }
4480              
4481             sub OpenVMS_DumpBytes
4482             {
4483 0     0   0 my $ix = shift;
4484 0         0 my $tag = shift;
4485 0         0 my $size = shift;
4486              
4487 0         0 myRead(my $data, $size);
4488              
4489 0         0 out($data, " Attribute", hexDump16($data));
4490              
4491             }
4492              
4493             sub OpenVMS_4ByteValue
4494             {
4495 0     0   0 my $ix = shift;
4496 0         0 my $tag = shift;
4497 0         0 my $size = shift;
4498              
4499 0         0 my ($data, $value) = read_V();
4500              
4501 0         0 out2 $data, " Attribute", Value_V($value);
4502             }
4503              
4504             sub OpenVMS_UCHAR
4505             {
4506 0     0   0 my $ix = shift;
4507 0         0 my $tag = shift;
4508 0         0 my $size = shift;
4509              
4510 0         0 state $FCH = {
4511             0 => 'FCH$M_WASCONTIG',
4512             1 => 'FCH$M_NOBACKUP',
4513             2 => 'FCH$M_WRITEBACK',
4514             3 => 'FCH$M_READCHECK',
4515             4 => 'FCH$M_WRITCHECK',
4516             5 => 'FCH$M_CONTIGB',
4517             6 => 'FCH$M_LOCKED',
4518             6 => 'FCH$M_CONTIG',
4519             11 => 'FCH$M_BADACL',
4520             12 => 'FCH$M_SPOOL',
4521             13 => 'FCH$M_DIRECTORY',
4522             14 => 'FCH$M_BADBLOCK',
4523             15 => 'FCH$M_MARKDEL',
4524             16 => 'FCH$M_NOCHARGE',
4525             17 => 'FCH$M_ERASE',
4526             18 => 'FCH$M_SHELVED',
4527             20 => 'FCH$M_SCRATCH',
4528             21 => 'FCH$M_NOMOVE',
4529             22 => 'FCH$M_NOSHELVABLE',
4530             } ;
4531              
4532 0         0 my ($data, $value) = read_V();
4533              
4534 0         0 out2 $data, " Attribute", Value_V($value);
4535              
4536 0         0 for my $bit ( sort { $a <=> $b } keys %{ $FCH } )
  0         0  
  0         0  
4537             {
4538             # print "$bit\n";
4539 0 0       0 if ($value & (1 << $bit) )
4540             {
4541 0         0 out1 " [Bit $bit]", $FCH->{$bit} ;
4542             }
4543             }
4544             }
4545              
4546             sub OpenVMS_2ByteValue
4547             {
4548 0     0   0 my $ix = shift;
4549 0         0 my $tag = shift;
4550 0         0 my $size = shift;
4551              
4552 0         0 my ($data, $value) = read_v();
4553              
4554 0         0 out2 $data, " Attribute", Value_v($value);
4555             }
4556              
4557             sub OpenVMS_revision
4558             {
4559 0     0   0 my $ix = shift;
4560 0         0 my $tag = shift;
4561 0         0 my $size = shift;
4562              
4563 0         0 my ($data, $value) = read_v();
4564              
4565 0         0 out2 $data, " Attribute", Value_v($value) . "'Revision Count " . Value_v($value) . "'";
4566             }
4567              
4568             sub decode_OpenVMS
4569             {
4570 0     0   0 my $extraID = shift ;
4571 0         0 my $len = shift;
4572 0         0 my $entry = shift;
4573              
4574 0         0 state $openVMS_tags = {
4575             0x04 => [ 'ATR$C_RECATTR', \&OpenVMS_DumpBytes ],
4576             0x03 => [ 'ATR$C_UCHAR', \&OpenVMS_UCHAR ],
4577             0x11 => [ 'ATR$C_CREDATE', \&OpenVMS_DateTime ],
4578             0x12 => [ 'ATR$C_REVDATE', \&OpenVMS_DateTime ],
4579             0x13 => [ 'ATR$C_EXPDATE', \&OpenVMS_DateTime ],
4580             0x14 => [ 'ATR$C_BAKDATE', \&OpenVMS_DateTime ],
4581             0x0D => [ 'ATR$C_ASCDATES', \&OpenVMS_revision ],
4582             0x15 => [ 'ATR$C_UIC', \&OpenVMS_4ByteValue ],
4583             0x16 => [ 'ATR$C_FPRO', \&OpenVMS_DumpBytes ],
4584             0x17 => [ 'ATR$C_RPRO', \&OpenVMS_2ByteValue ],
4585             0x1D => [ 'ATR$C_JOURNAL', \&OpenVMS_DumpBytes ],
4586             0x1F => [ 'ATR$C_ADDACLENT', \&OpenVMS_DumpBytes ],
4587             } ;
4588              
4589 0         0 out_V " CRC";
4590 0         0 $len -= 4;
4591              
4592 0         0 my $ix = 1;
4593 0         0 while ($len)
4594             {
4595 0         0 my ($data, $tag) = read_v();
4596 0         0 my $tagname = 'Unknown Tag';
4597 0         0 my $decoder = undef;
4598              
4599 0 0       0 if ($openVMS_tags->{$tag})
4600             {
4601 0         0 ($tagname, $decoder) = @{ $openVMS_tags->{$tag} } ;
  0         0  
4602             }
4603              
4604 0         0 out2 $data, "Tag #$ix", Value_v($tag) . " '" . $tagname . "'" ;
4605 0         0 my $size = out_v " Size";
4606              
4607 0 0       0 if (defined $decoder)
4608             {
4609 0         0 $decoder->($ix, $tag, $size) ;
4610             }
4611             else
4612             {
4613 0         0 outSomeData($size, " Attribute");
4614             }
4615              
4616 0         0 ++ $ix;
4617 0         0 $len -= $size + 2 + 2;
4618             }
4619              
4620             }
4621              
4622             sub getT
4623             {
4624 24     24   50 my $time = shift ;
4625              
4626 24 50       96 if ($opt_utc)
4627 24   50     297 { return scalar gmtime($time) // 'Unknown'}
4628             else
4629 0   0     0 { return scalar localtime($time) // 'Unknown' }
4630             }
4631              
4632             sub getTime
4633             {
4634 24     24   38 my $time = shift ;
4635              
4636 24 50       56 return "'Invalid Date or Time'"
4637             if ! defined $time;
4638              
4639 24         60 return "'" . getT($time) . "'";
4640             }
4641              
4642             sub LastModTime
4643             {
4644 24     24   43 my $value = shift ;
4645              
4646 24 50       60 return "'No Date/Time'"
4647             if $value == 0;
4648              
4649 24         81 return getTime(_dosToUnixTime($value))
4650             }
4651              
4652             sub _dosToUnixTime
4653             {
4654 24     24   39 my $dt = shift;
4655              
4656             # Mozilla xpi files have empty datetime
4657             # This is not a valid Dos datetime value
4658 24 50       54 return 0 if $dt == 0 ;
4659              
4660 24         82 my $year = ( ( $dt >> 25 ) & 0x7f ) + 80;
4661 24         53 my $mon = ( ( $dt >> 21 ) & 0x0f ) - 1;
4662 24         40 my $mday = ( ( $dt >> 16 ) & 0x1f );
4663              
4664 24         42 my $hour = ( ( $dt >> 11 ) & 0x1f );
4665 24         38 my $min = ( ( $dt >> 5 ) & 0x3f );
4666 24         47 my $sec = ( ( $dt << 1 ) & 0x3e );
4667              
4668 6     6   4212 use Time::Local ;
  6         13701  
  6         65548  
4669 24         36 my $time_t;
4670             eval
4671             {
4672             # Use eval to catch crazy dates
4673 24         116 $time_t = Time::Local::timegm( $sec, $min, $hour, $mday, $mon, $year);
4674             }
4675             or do
4676 24 50       41 {
4677 0         0 my $dosDecode = $year+1900 . sprintf "-%02u-%02u %02u:%02u:%02u", $mon, $mday, $hour, $min, $sec;
4678 0         0 warning $FH->tell(), "'Modification Time' value " . decimalHex0x($dt, 4) . " decodes to '$dosDecode': not a valid DOS date/time" ;
4679             return undef
4680 0         0 };
4681              
4682 24         1121 return $time_t;
4683              
4684             }
4685              
4686             sub decode_UT
4687             {
4688             # 0x5455 'UT: Extended Timestamp'
4689              
4690 0     0   0 my $extraID = shift ;
4691 0         0 my $len = shift;
4692 0         0 my $entry = shift;
4693              
4694             # Definition in IZ APPNOTE
4695              
4696             # NOTE: Although the IZ appnote says that the central directory
4697             # doesn't store the Acces & Creation times, there are
4698             # some implementations that do poopulate the CD incorrectly.
4699              
4700             # Caller has determined that at least one byte is available
4701              
4702             # When $full is true assume all timestamps are present
4703 0         0 my $full = ($len == 13) ;
4704              
4705 0         0 my $remaining = $len;
4706              
4707 0         0 my ($data, $flags) = read_C();
4708              
4709 0         0 my $v = Value_C $flags;
4710 0         0 my @f ;
4711 0 0       0 push @f, "Modification" if $flags & 1;
4712 0 0       0 push @f, "Access" if $flags & 2;
4713 0 0       0 push @f, "Creation" if $flags & 4;
4714 0 0       0 $v .= " '" . join(' & ', @f) . "'"
4715             if @f;
4716              
4717 0         0 out $data, " Flags", $v;
4718              
4719 0 0       0 info $FH->tell() - 1, extraFieldIdentifier($extraID) . ": Reserved bits set in 'Flags' field"
4720             if $flags & ~0x7;
4721              
4722 0         0 -- $remaining;
4723              
4724 0 0 0     0 if ($flags & 1 || $full)
4725             {
4726 0 0       0 if ($remaining == 0 )
4727             {
4728             # Central Dir only has Modification Time
4729 0         0 error $FH->tell(), extraFieldIdentifier($extraID) . ": Missing field 'Modification Time'" ;
4730 0         0 return;
4731             }
4732             else
4733             {
4734 0 0       0 info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Modification Time' present"
4735             if ! ($flags & 1) ;
4736              
4737 0 0       0 if ($remaining < 4)
4738             {
4739 0         0 outSomeData $remaining, " Extra Data";
4740 0         0 error $FH->tell() - $remaining,
4741             extraFieldIdentifier($extraID) . ": Truncated reading 'Modification Time'",
4742             expectedMessage(4, $remaining);
4743 0         0 return;
4744             }
4745              
4746 0         0 my ($data, $time) = read_V();
4747              
4748 0         0 out2 $data, "Modification Time", Value_V($time) . " " . getTime($time) ;
4749              
4750 0         0 $remaining -= 4 ;
4751             }
4752             }
4753              
4754             # The remaining sub-fields are only present in the Local Header
4755              
4756 0 0 0     0 if ($flags & 2 || $full)
4757             {
4758 0 0 0     0 if ($remaining == 0 && $entry->inCentralDir)
4759             {
4760             # Central Dir doesn't have access time
4761             }
4762             else
4763             {
4764 0 0 0     0 info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Access Time' present"
4765             if ! ($flags & 2) || $entry->inCentralDir ;
4766              
4767 0 0       0 if ($remaining < 4)
4768             {
4769 0         0 outSomeData $remaining, " Extra Data";
4770 0         0 error $FH->tell() - $remaining,
4771             extraFieldIdentifier($extraID) . ": Truncated reading 'Access Time'" ,
4772             expectedMessage(4, $remaining);
4773              
4774 0         0 return;
4775             }
4776              
4777 0         0 my ($data, $time) = read_V();
4778              
4779 0         0 out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ;
4780 0         0 $remaining -= 4 ;
4781             }
4782             }
4783              
4784 0 0 0     0 if ($flags & 4 || $full)
4785             {
4786 0 0 0     0 if ($remaining == 0 && $entry->inCentralDir)
4787             {
4788             # Central Dir doesn't have creation time
4789             }
4790             else
4791             {
4792 0 0 0     0 info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Creation Time' present"
4793             if ! ($flags & 4) || $entry->inCentralDir ;
4794              
4795 0 0       0 if ($remaining < 4)
4796             {
4797 0         0 outSomeData $remaining, " Extra Data";
4798              
4799 0         0 error $FH->tell() - $remaining,
4800             extraFieldIdentifier($extraID) . ": Truncated reading 'Creation Time'" ,
4801             expectedMessage(4, $remaining);
4802              
4803 0         0 return;
4804             }
4805              
4806 0         0 my ($data, $time) = read_V();
4807              
4808 0         0 out2 $data, "Creation Time", Value_V($time) . " " . getTime($time) ;
4809             }
4810             }
4811             }
4812              
4813              
4814             sub decode_Minizip_Signature
4815             {
4816             # 0x10c5 Minizip CMS Signature
4817              
4818 0     0   0 my $extraID = shift ;
4819 0         0 my $len = shift;
4820 0         0 my $entry = shift;
4821              
4822             # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#cms-signature-0x10c5
4823              
4824 0         0 $CentralDirectory->setMiniZipEncrypted();
4825              
4826 0 0       0 if ($len == 0)
4827             {
4828 0         0 info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Zero length Signature";
4829 0         0 return;
4830             }
4831              
4832 0         0 outHexdump($len, " Signature");
4833              
4834             }
4835              
4836             sub decode_Minizip_Hash
4837             {
4838             # 0x1a51 Minizip Hash
4839             # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#hash-0x1a51
4840              
4841             # caller ckecks there are at least 4 bytes available
4842 0     0   0 my $extraID = shift ;
4843 0         0 my $len = shift;
4844 0         0 my $entry = shift;
4845              
4846 0         0 state $Algorithm = {
4847             10 => 'MD5',
4848             20 => 'SHA1',
4849             23 => 'SHA256',
4850             };
4851              
4852 0         0 my $remaining = $len;
4853              
4854 0         0 $CentralDirectory->setMiniZipEncrypted();
4855              
4856 0         0 my ($data, $alg) = read_v();
4857 0   0     0 my $algorithm = $Algorithm->{$alg} // "Unknown";
4858              
4859 0         0 out $data, " Algorithm", Value_v($alg) . " '$algorithm'";
4860 0 0       0 if (! exists $Algorithm->{$alg})
4861             {
4862 0         0 info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown algorithm ID " .Value_v($alg);
4863             }
4864              
4865 0         0 my ($d, $digestSize) = read_v();
4866 0         0 out $d, " Digest Size", Value_v($digestSize);
4867              
4868 0         0 $remaining -= 4;
4869              
4870 0 0       0 if ($digestSize == 0)
    0          
4871             {
4872 0         0 info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Zero length Digest";
4873             }
4874             elsif ($digestSize > $remaining)
4875             {
4876 0         0 error $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Digest Size " . decimalHex0x($digestSize) . " > " . decimalHex0x($remaining) . " bytes remaining in extra field" ;
4877 0         0 $digestSize = $remaining ;
4878             }
4879              
4880 0         0 outHexdump($digestSize, " Digest");
4881              
4882 0         0 $remaining -= $digestSize;
4883              
4884 0 0       0 if ($remaining)
4885             {
4886 0         0 outHexdump($remaining, " Unexpected Data");
4887 0         0 error $FH->tell() - $remaining, extraFieldIdentifier($extraID) . ": " . decimalHex0x($remaining) . " unexpected trailing bytes" ;
4888             }
4889             }
4890              
4891             sub decode_Minizip_CD
4892             {
4893             # 0xcdcd Minizip Central Directory
4894             # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#central-directory-0xcdcd
4895              
4896 0     0   0 my $extraID = shift ;
4897 0         0 my $len = shift;
4898 0         0 my $entry = shift;
4899              
4900 0         0 $entry->minizip_secure(1);
4901 0         0 $CentralDirectory->setMiniZipEncrypted();
4902              
4903 0         0 my $size = out_Q " Entries";
4904              
4905             }
4906              
4907             sub decode_AES
4908             {
4909             # ref https://www.winzip.com/en/support/aes-encryption/
4910             # Document version: 1.04
4911             # Last modified: January 30, 2009
4912              
4913 0     0   0 my $extraID = shift ;
4914 0         0 my $len = shift;
4915 0         0 my $entry = shift;
4916              
4917 0 0       0 return if $len == 0 ;
4918              
4919 0         0 my $validAES = 1;
4920              
4921 0         0 state $lookup = { 1 => "AE-1", 2 => "AE-2" };
4922 0 0   0   0 my $vendorVersion = out_v " Vendor Version", sub { $lookup->{$_[0]} || "Unknown" } ;
  0         0  
4923 0 0       0 if (! $lookup->{$vendorVersion})
4924             {
4925 0         0 $validAES = 0;
4926 0         0 warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Vendor Version' $vendorVersion. Valid values are 1,2"
4927             }
4928              
4929 0         0 my $id ;
4930 0         0 myRead($id, 2);
4931 0         0 my $idValue = out $id, " Vendor ID", unpackValue_v($id) . " '$id'";
4932              
4933 0 0       0 if ($id ne 'AE')
4934             {
4935 0         0 $validAES = 0;
4936 0         0 warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Vendor ID' '$idValue'. Valid value is 'AE'"
4937             }
4938              
4939 0         0 state $strengths = {1 => "128-bit encryption key",
4940             2 => "192-bit encryption key",
4941             3 => "256-bit encryption key",
4942             };
4943              
4944 0 0   0   0 my $strength = out_C " Encryption Strength", sub {$strengths->{$_[0]} || "Unknown" } ;
  0         0  
4945              
4946 0 0       0 if (! $strengths->{$strength})
4947             {
4948 0         0 $validAES = 0;
4949 0         0 warning $FH->tell() - 1, extraFieldIdentifier($extraID) . ": Unknown 'Encryption Strength' $strength. Valid values are 1,2,3"
4950             }
4951              
4952 0         0 my ($bmethod, $method) = read_v();
4953 0         0 out $bmethod, " Compression Method", compressionMethod($method) ;
4954 0 0       0 if (! defined $ZIP_CompressionMethods{$method})
4955             {
4956 0         0 $validAES = 0;
4957 0         0 warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Compression Method' ID " . decimalHex0x($method, 2)
4958             }
4959              
4960 0         0 $entry->aesStrength($strength) ;
4961 0         0 $entry->aesValid($validAES) ;
4962             }
4963              
4964             sub decode_Reference
4965             {
4966             # ref https://www.winzip.com/en/support/compression-methods/
4967              
4968 0     0   0 my $len = shift;
4969 0         0 my $entry = shift;
4970              
4971 0         0 out_V " CRC";
4972 0         0 myRead(my $uuid, 16);
4973             # UUID is big endian
4974 0         0 out2 $uuid, "UUID",
4975             unpack('H*', substr($uuid, 0, 4)) . '-' .
4976             unpack('H*', substr($uuid, 4, 2)) . '-' .
4977             unpack('H*', substr($uuid, 6, 2)) . '-' .
4978             unpack('H*', substr($uuid, 8, 2)) . '-' .
4979             unpack('H*', substr($uuid, 10, 6)) ;
4980             }
4981              
4982             sub decode_DUMMY
4983             {
4984 0     0   0 my $extraID = shift ;
4985 0         0 my $len = shift;
4986 0         0 my $entry = shift;
4987              
4988 0         0 out_V " Data";
4989             }
4990              
4991             sub decode_GrowthHint
4992             {
4993             # APPNOTE 6.3.10, sec 4.6.10
4994              
4995 0     0   0 my $extraID = shift ;
4996 0         0 my $len = shift;
4997 0         0 my $entry = shift;
4998              
4999             # caller has checked that 4 bytes are available,
5000             # so can output values without checking available space
5001 0         0 out_v " Signature" ;
5002 0         0 out_v " Initial Value";
5003              
5004 0         0 my $padding;
5005 0         0 myRead($padding, $len - 4);
5006              
5007 0         0 out2 $padding, "Padding", hexDump16($padding);
5008              
5009 0 0       0 if ($padding !~ /^\x00+$/)
5010             {
5011 0         0 info $FH->tell(), extraFieldIdentifier($extraID) . ": 'Padding' is not all NULL bytes";
5012             }
5013             }
5014              
5015             sub decode_DataStreamAlignment
5016             {
5017             # APPNOTE 6.3.10, sec 4.6.11
5018              
5019 0     0   0 my $extraID = shift ;
5020 0         0 my $len = shift;
5021 0         0 my $entry = shift;
5022              
5023 0         0 my $inCentralHdr = $entry->inCentralDir ;
5024              
5025 0 0       0 return if $len == 0 ;
5026              
5027 0         0 my ($data, $alignment) = read_v();
5028              
5029 0         0 out $data, " Alignment", Value_v($alignment) ;
5030              
5031 0 0       0 my $recompress_value = $alignment & 0x8000 ? 1 : 0;
5032              
5033 0 0       0 my $recompressing = $recompress_value ? "True" : "False";
5034 0         0 $alignment &= 0x7FFF ;
5035 0         0 my $hexAl = sprintf("%X", $alignment);
5036              
5037 0         0 out1 " [Bit 15]", "$recompress_value 'Recompress $recompressing'";
5038 0         0 out1 " [Bits 0-14]", "$hexAl 'Minimal Alignment $alignment'";
5039              
5040 0 0 0     0 if (! $inCentralHdr && $len - 2 > 0)
5041             {
5042 0         0 my $padding;
5043 0         0 myRead($padding, $len - 2);
5044              
5045 0         0 out2 $padding, "Padding", hexDump16($padding);
5046             }
5047             }
5048              
5049              
5050             sub decode_UX
5051             {
5052 0     0   0 my $extraID = shift ;
5053 0         0 my $len = shift;
5054 0         0 my $entry = shift;
5055              
5056 0         0 my $inCentralHdr = $entry->inCentralDir ;
5057              
5058 0 0       0 return if $len == 0 ;
5059              
5060 0         0 my ($data, $time) = read_V();
5061 0         0 out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ;
5062              
5063 0         0 ($data, $time) = read_V();
5064 0         0 out2 $data, "Modification Time", Value_V($time) . " " . getTime($time) ;
5065              
5066 0 0       0 if (! $inCentralHdr ) {
5067 0         0 out_v " UID" ;
5068 0         0 out_v " GID";
5069             }
5070             }
5071              
5072             sub decode_Ux
5073             {
5074 0     0   0 my $extraID = shift ;
5075 0         0 my $len = shift;
5076 0         0 my $entry = shift;
5077              
5078 0 0       0 return if $len == 0 ;
5079 0         0 out_v " UID" ;
5080 0         0 out_v " GID";
5081             }
5082              
5083             sub decodeLitteEndian
5084             {
5085 0     0   0 my $value = shift ;
5086              
5087 0 0       0 if (length $value == 8)
    0          
    0          
    0          
5088             {
5089 0         0 return unpackValueQ ($value)
5090             }
5091             elsif (length $value == 4)
5092             {
5093 0         0 return unpackValue_V ($value)
5094             }
5095             elsif (length $value == 2)
5096             {
5097 0         0 return unpackValue_v ($value)
5098             }
5099             elsif (length $value == 1)
5100             {
5101 0         0 return unpackValue_C ($value)
5102             }
5103             else {
5104             # TODO - fix this
5105 0         0 internalFatal undef, "unsupported decodeLitteEndian length '" . length ($value) . "'";
5106             }
5107             }
5108              
5109             sub decode_ux
5110             {
5111 0     0   0 my $extraID = shift ;
5112 0         0 my $len = shift;
5113 0         0 my $entry = shift;
5114              
5115             # caller has checked that 3 bytes are available
5116              
5117 0 0       0 return if $len == 0 ;
5118              
5119 0         0 my $version = out_C " Version" ;
5120 0 0       0 info $FH->tell() - 1, extraFieldIdentifier($extraID) . ": 'Version' should be " . decimalHex0x(1) . ", got " . decimalHex0x($version, 1)
5121             if $version != 1 ;
5122              
5123 0         0 my $available = $len - 1 ;
5124              
5125 0         0 my $uidSize = out_C " UID Size";
5126 0         0 $available -= 1;
5127              
5128 0 0       0 if ($uidSize)
5129             {
5130 0 0       0 if ($available < $uidSize)
5131             {
5132 0         0 outSomeData($available, " Bad Extra Data");
5133 0         0 error $FH->tell() - $available,
5134             extraFieldIdentifier($extraID) . ": truncated reading 'UID'",
5135             expectedMessage($uidSize, $available);
5136 0         0 return;
5137             }
5138              
5139 0         0 myRead(my $data, $uidSize);
5140 0         0 out2 $data, "UID", decodeLitteEndian($data);
5141 0         0 $available -= $uidSize ;
5142             }
5143              
5144 0 0       0 if ($available < 1)
5145             {
5146 0         0 error $FH->tell(),
5147             extraFieldIdentifier($extraID) . ": truncated reading 'GID Size'",
5148             expectedMessage($uidSize, $available);
5149 0         0 return ;
5150             }
5151              
5152 0         0 my $gidSize = out_C " GID Size";
5153 0         0 $available -= 1 ;
5154 0 0       0 if ($gidSize)
5155             {
5156 0 0       0 if ($available < $gidSize)
5157             {
5158 0         0 outSomeData($available, " Bad Extra Data");
5159 0         0 error $FH->tell() - $available,
5160             extraFieldIdentifier($extraID) . ": truncated reading 'GID'",
5161             expectedMessage($gidSize, $available);
5162 0         0 return;
5163             }
5164              
5165 0         0 myRead(my $data, $gidSize);
5166 0         0 out2 $data, "GID", decodeLitteEndian($data);
5167 0         0 $available -= $gidSize ;
5168             }
5169              
5170             }
5171              
5172             sub decode_Java_exe
5173             {
5174 0     0   0 my $extraID = shift ;
5175 0         0 my $len = shift;
5176 0         0 my $entry = shift;
5177              
5178             }
5179              
5180             sub decode_up
5181             {
5182             # APPNOTE 6.3.10, sec 4.6.9
5183              
5184 0     0   0 my $extraID = shift ;
5185 0         0 my $len = shift;
5186 0         0 my $entry = shift;
5187              
5188 0         0 out_C " Version";
5189 0         0 out_V " NameCRC32";
5190              
5191 0 0       0 if ($len - 5 > 0)
5192             {
5193 0         0 myRead(my $data, $len - 5);
5194              
5195 0         0 outputFilename($data, 1, " UnicodeName");
5196             }
5197             }
5198              
5199             sub decode_ASi_Unix
5200             {
5201 0     0   0 my $extraID = shift ;
5202 0         0 my $len = shift;
5203 0         0 my $entry = shift;
5204              
5205             # https://stackoverflow.com/questions/76581811/why-does-unzip-ignore-my-zip64-end-of-central-directory-record
5206              
5207 0         0 out_V " CRC";
5208 0         0 my $native_attrib = out_v " Mode";
5209              
5210             # TODO - move to separate sub & tidy
5211 0         0 if (1) # Unix
5212             {
5213              
5214 0         0 state $mask = {
5215             0 => '---',
5216             1 => '--x',
5217             2 => '-w-',
5218             3 => '-wx',
5219             4 => 'r--',
5220             5 => 'r-x',
5221             6 => 'rw-',
5222             7 => 'rwx',
5223             } ;
5224              
5225 0         0 my $rwx = ($native_attrib & 0777);
5226              
5227 0 0       0 if ($rwx)
5228             {
5229 0         0 my $output = '';
5230 0         0 $output .= $mask->{ ($rwx >> 6) & 07 } ;
5231 0         0 $output .= $mask->{ ($rwx >> 3) & 07 } ;
5232 0         0 $output .= $mask->{ ($rwx >> 0) & 07 } ;
5233              
5234 0         0 out1 " [Bits 0-8]", Value_v($rwx) . " 'Unix attrib: $output'" ;
5235 0 0       0 out1 " [Bit 9]", "1 'Sticky'"
5236             if $rwx & 0x200 ;
5237 0 0       0 out1 " [Bit 10]", "1 'Set GID'"
5238             if $rwx & 0x400 ;
5239 0 0       0 out1 " [Bit 11]", "1 'Set UID'"
5240             if $rwx & 0x800 ;
5241              
5242 0         0 my $not_rwx = (($native_attrib >> 12) & 0xF);
5243 0 0       0 if ($not_rwx)
5244             {
5245 0         0 state $masks = {
5246             0x0C => 'Socket', # 0x0C 0b1100
5247             0x0A => 'Symbolic Link', # 0x0A 0b1010
5248             0x08 => 'Regular File', # 0x08 0b1000
5249             0x06 => 'Block Device', # 0x06 0b0110
5250             0x04 => 'Directory', # 0x04 0b0100
5251             0x02 => 'Character Device', # 0x02 0b0010
5252             0x01 => 'FIFO', # 0x01 0b0001
5253             };
5254              
5255 0   0     0 my $got = $masks->{$not_rwx} // 'Unknown Unix attrib' ;
5256 0         0 out1 " [Bits 12-15]", Value_C($not_rwx) . " '$got'"
5257             }
5258             }
5259             }
5260              
5261              
5262 0         0 my $s = out_V " SizDev";
5263 0         0 out_v " UID";
5264 0         0 out_v " GID";
5265              
5266             }
5267              
5268             sub decode_uc
5269             {
5270             # APPNOTE 6.3.10, sec 4.6.8
5271              
5272 0     0   0 my $extraID = shift ;
5273 0         0 my $len = shift;
5274 0         0 my $entry = shift;
5275              
5276 0         0 out_C " Version";
5277 0         0 out_V " ComCRC32";
5278              
5279 0 0       0 if ($len - 5 > 0)
5280             {
5281 0         0 myRead(my $data, $len - 5);
5282              
5283 0         0 outputFilename($data, 1, " UnicodeCom");
5284             }
5285             }
5286              
5287             sub decode_Xceed_unicode
5288             {
5289             # 0x554e
5290              
5291 0     0   0 my $extraID = shift ;
5292 0         0 my $len = shift;
5293 0         0 my $entry = shift;
5294              
5295 0         0 my $data ;
5296 0         0 my $remaining = $len;
5297              
5298             # No public definition available, so reverse engineer the content.
5299              
5300             # See https://github.com/pmqs/zipdetails/issues/13 for C# source that populates
5301             # this field.
5302              
5303             # Fiddler https://www.telerik.com/fiddler) creates this field.
5304              
5305             # Local Header only has UTF16LE filename
5306             #
5307             # Field definition
5308             # 4 bytes Signature always XCUN
5309             # 2 bytes Filename Length (divided by 2)
5310             # Filename
5311              
5312             # Central has UTF16LE filename & comment
5313             #
5314             # Field definition
5315             # 4 bytes Signature always XCUN
5316             # 2 bytes Filename Length (divided by 2)
5317             # 2 bytes Comment Length (divided by 2)
5318             # Filename
5319             # Comment
5320              
5321             # First 4 bytes appear to be little-endian "XCUN" all the time
5322             # Just double check
5323 0         0 my ($idb, $id) = read_V();
5324 0         0 $remaining -= 4;
5325              
5326 0         0 my $outid = decimalHex0x($id);
5327 0 0       0 $outid .= " 'XCUN'"
5328             if $idb eq 'NUCX';
5329              
5330 0         0 out $idb, " ID", $outid;
5331              
5332             # Next 2 bytes contains a count of the filename length divided by 2
5333             # Dividing by 2 gives the number of UTF-16 characters.
5334 0         0 my $filenameLength = out_v " Filename Length";
5335 0         0 $filenameLength *= 2; # Double to get number of bytes to read
5336 0         0 $remaining -= 2;
5337              
5338 0         0 my $commentLength = 0;
5339              
5340 0 0       0 if ($entry->inCentralDir)
5341             {
5342             # Comment length only in Central Directory
5343             # Again stored divided by 2.
5344 0         0 $commentLength = out_v " Comment Length";
5345 0         0 $commentLength *= 2; # Double to get number of bytes to read
5346 0         0 $remaining -= 2;
5347             }
5348              
5349             # next is a UTF16 encoded filename
5350              
5351 0 0       0 if ($filenameLength)
5352             {
5353 0 0       0 if ($filenameLength > $remaining )
5354             {
5355 0         0 myRead($data, $remaining);
5356 0         0 out redactData($data), " UTF16LE Filename", "'" . redactFilename(decode("UTF16LE", $data)) . "'";
5357              
5358 0         0 error $FH->tell() - $remaining,
5359             extraFieldIdentifier($extraID) . ": Truncated reading 'UTF16LE Filename'",
5360             expectedMessage($filenameLength, $remaining);
5361 0         0 return undef;
5362             }
5363              
5364 0         0 myRead($data, $filenameLength);
5365 0         0 out redactData($data), " UTF16LE Filename", "'" . redactFilename(decode("UTF16LE", $data)) . "'";
5366 0         0 $remaining -= $filenameLength;
5367             }
5368              
5369             # next is a UTF16 encoded comment
5370              
5371 0 0       0 if ($commentLength)
5372             {
5373 0 0       0 if ($commentLength > $remaining )
5374             {
5375 0         0 myRead($data, $remaining);
5376 0         0 out redactData($data), " UTF16LE Comment", "'" . redactFilename(decode("UTF16LE", $data)) . "'";
5377              
5378 0         0 error $FH->tell() - $remaining,
5379             extraFieldIdentifier($extraID) . ": Truncated reading 'UTF16LE Comment'",
5380             expectedMessage($filenameLength, $remaining);
5381 0         0 return undef;
5382             }
5383              
5384 0         0 myRead($data, $commentLength);
5385 0         0 out redactData($data), " UTF16LE Comment", "'" . redactFilename(decode("UTF16LE", $data)) . "'";
5386 0         0 $remaining -= $commentLength;
5387             }
5388              
5389 0 0       0 if ($remaining)
5390             {
5391 0         0 outHexdump($remaining, " Unexpected Data");
5392 0         0 error $FH->tell() - $remaining, extraFieldIdentifier($extraID) . ": " . decimalHex0x($remaining) . " unexpected trailing bytes" ;
5393             }
5394             }
5395              
5396             sub decode_Key_Value_Pair
5397             {
5398             # 0x564B 'KV'
5399             # https://github.com/sozip/keyvaluepairs-spec/blob/master/zip_keyvalue_extra_field_specification.md
5400              
5401 0     0   0 my $extraID = shift ;
5402 0         0 my $len = shift;
5403 0         0 my $entry = shift;
5404              
5405 0         0 my $remaining = $len;
5406              
5407 0         0 myRead(my $signature, 13);
5408 0         0 $remaining -= 13;
5409              
5410 0 0       0 if ($signature ne 'KeyValuePairs')
5411             {
5412 0         0 error $FH->tell() - 13, extraFieldIdentifier($extraID) . ": 'Signature' field not 'KeyValuePairs'" ;
5413 0         0 myRead(my $payload, $remaining);
5414 0         0 my $data = hexDump16($signature . $payload);
5415              
5416 0         0 out2 $signature . $payload, "Extra Payload", $data;
5417              
5418 0         0 return ;
5419             }
5420              
5421 0         0 out $signature, ' Signature', "'KeyValuePairs'";
5422 0         0 my $kvPairs = out_C " KV Count";
5423 0         0 $remaining -= 1;
5424              
5425 0         0 for my $index (1 .. $kvPairs)
5426             {
5427 0         0 my $key;
5428 0         0 my $klen = out_v " Key size #$index";
5429 0         0 $remaining -= 4;
5430              
5431 0         0 myRead($key, $klen);
5432 0         0 outputFilename $key, 1, " Key #$index";
5433 0         0 $remaining -= $klen;
5434              
5435 0         0 my $value;
5436 0         0 my $vlen = out_v " Value size #$index";
5437 0         0 $remaining -= 4;
5438              
5439 0         0 myRead($value, $vlen);
5440 0         0 outputFilename $value, 1, " Value #$index";
5441 0         0 $remaining -= $vlen;
5442             }
5443              
5444             # TODO check that
5445             # * count of kv pairs is accurate
5446             # * no truncation in middle of kv data
5447             # * no trailing data
5448             }
5449              
5450             sub decode_NT_security
5451             {
5452             # IZ Appnote
5453 0     0   0 my $extraID = shift ;
5454 0         0 my $len = shift;
5455 0         0 my $entry = shift;
5456              
5457 0         0 my $inCentralHdr = $entry->inCentralDir ;
5458              
5459 0         0 out_V " Uncompressed Size" ;
5460              
5461 0 0       0 if (! $inCentralHdr) {
5462              
5463 0         0 out_C " Version" ;
5464              
5465 0   0 0   0 out_v " CType", sub { "'" . ($ZIP_CompressionMethods{$_[0]} || "Unknown Method") . "'" };
  0         0  
5466              
5467 0         0 out_V " CRC" ;
5468              
5469 0         0 my $plen = $len - 4 - 1 - 2 - 4;
5470 0         0 outHexdump $plen, " Extra Payload";
5471             }
5472             }
5473              
5474             sub decode_MVS
5475             {
5476             # APPNOTE 6.3.10, Appendix
5477 0     0   0 my $extraID = shift ;
5478 0         0 my $len = shift;
5479 0         0 my $entry = shift;
5480              
5481             # data in Big-Endian
5482 0         0 myRead(my $data, $len);
5483 0         0 my $ID = unpack("N", $data);
5484              
5485 0 0       0 if ($ID == 0xE9F3F9F0) # EBCDIC for "Z390"
5486             {
5487 0         0 my $d = substr($data, 0, 4, '') ;
5488 0         0 out($d, " ID", "'Z390'");
5489             }
5490              
5491 0         0 out($data, " Extra Payload", hexDump16($data));
5492             }
5493              
5494             sub decode_strong_encryption
5495             {
5496             # APPNOTE 6.3.10, sec 4.5.12 & 7.4.2
5497              
5498 0     0   0 my $extraID = shift ;
5499 0         0 my $len = shift;
5500 0         0 my $entry = shift;
5501              
5502             # TODO check for overflow is contents > $len
5503 0         0 out_v " Format";
5504 0   0 0   0 out_v " AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ;
  0         0  
5505 0         0 out_v " BitLen";
5506 0   0 0   0 out_v " Flags", sub { $FlagsLookup{ $_[0] } // "reserved for certificate processing" } ;
  0         0  
5507              
5508             # see APPNOTE 6.3.10, sec 7.4.2 for this part
5509 0         0 my $recipients = out_V " Recipients";
5510              
5511 0         0 my $available = $len - 12;
5512              
5513 0 0       0 if ($recipients)
5514             {
5515 0 0       0 if ($available < 2)
5516             {
5517 0         0 outSomeData($available, " Badly formed extra data");
5518             # TODO - need warning
5519 0         0 return;
5520             }
5521              
5522 0   0 0   0 out_v " HashAlg", sub { $HashAlgLookup{ $_[0] } // "Unknown algorithm" } ;
  0         0  
5523 0         0 $available -= 2;
5524              
5525 0 0       0 if ($available < 2)
5526             {
5527 0         0 outSomeData($available, " Badly formed extra data");
5528             # TODO - need warning
5529 0         0 return;
5530             }
5531              
5532 0         0 my $HSize = out_v " HSize" ;
5533 0         0 $available -= 2;
5534              
5535             # should have $recipients * $HSize bytes available
5536 0 0       0 if ($recipients * $HSize != $available)
5537             {
5538 0         0 outSomeData($available, " Badly formed extra data");
5539             # TODO - need warning
5540 0         0 return;
5541             }
5542              
5543 0         0 my $ix = 1;
5544 0         0 for (0 .. $recipients-1)
5545             {
5546 0         0 myRead(my $payload, $HSize);
5547 0         0 my $data = hexDump16($payload);
5548              
5549 0         0 out2 $payload, sprintf("Key #%X", $ix), $data;
5550 0         0 ++ $ix;
5551             }
5552             }
5553             }
5554              
5555              
5556             sub printAes
5557             {
5558             # ref https://www.winzip.com/en/support/aes-encryption/
5559              
5560 0     0   0 my $entry = shift;
5561              
5562 0 0       0 return 0
5563             if ! $entry->aesValid;
5564              
5565 0         0 my %saltSize = (
5566             1 => 8,
5567             2 => 12,
5568             3 => 16,
5569             );
5570              
5571 0   0     0 myRead(my $salt, $saltSize{$entry->aesStrength } // 0);
5572 0         0 out $salt, "AES Salt", hexDump16($salt);
5573 0         0 myRead(my $pwv, 2);
5574 0         0 out $pwv, "AES Pwd Ver", hexDump16($pwv);
5575              
5576 0         0 return $saltSize{$entry->aesStrength} + 2 + 10;
5577             }
5578              
5579             sub printLzmaProperties
5580             {
5581 0     0   0 my $len = 0;
5582              
5583 0         0 my $b1;
5584             my $b2;
5585 0         0 my $buffer;
5586              
5587 0         0 myRead($b1, 2);
5588 0         0 my ($verHi, $verLow) = unpack ("CC", $b1);
5589              
5590 0         0 out $b1, "LZMA Version", sprintf("%02X%02X", $verHi, $verLow) . " '$verHi.$verLow'";
5591 0         0 my $LzmaPropertiesSize = out_v "LZMA Properties Size";
5592 0         0 $len += 4;
5593              
5594 0 0   0   0 my $LzmaInfo = out_C "LZMA Info", sub { $_[0] == 93 ? "(Default)" : ""};
  0         0  
5595              
5596 0         0 my $PosStateBits = 0;
5597 0         0 my $LiteralPosStateBits = 0;
5598 0         0 my $LiteralContextBits = 0;
5599 0         0 $PosStateBits = int($LzmaInfo / (9 * 5));
5600 0         0 $LzmaInfo -= $PosStateBits * 9 * 5;
5601 0         0 $LiteralPosStateBits = int($LzmaInfo / 9);
5602 0         0 $LiteralContextBits = $LzmaInfo - $LiteralPosStateBits * 9;
5603              
5604 0         0 out1 " PosStateBits", $PosStateBits;
5605 0         0 out1 " LiteralPosStateBits", $LiteralPosStateBits;
5606 0         0 out1 " LiteralContextBits", $LiteralContextBits;
5607              
5608 0         0 out_V "LZMA Dictionary Size";
5609              
5610             # TODO - assumption that this is 5
5611 0         0 $len += $LzmaPropertiesSize;
5612              
5613 0 0       0 skip($FH, $LzmaPropertiesSize - 5)
5614             if $LzmaPropertiesSize != 5 ;
5615              
5616 0         0 return $len;
5617             }
5618              
5619             sub peekAtOffset
5620             {
5621             # my $fh = shift;
5622 0     0   0 my $offset = shift;
5623 0         0 my $len = shift;
5624              
5625 0         0 my $here = $FH->tell();
5626              
5627 0         0 seekTo($offset) ;
5628              
5629 0         0 my $buffer;
5630 0         0 myRead($buffer, $len);
5631 0         0 seekTo($here);
5632              
5633 0 0       0 length $buffer == $len
5634             or return '';
5635              
5636 0         0 return $buffer;
5637             }
5638              
5639             sub readFromOffset
5640             {
5641             # my $fh = shift;
5642 26     26   43 my $offset = shift;
5643 26         35 my $len = shift;
5644              
5645 26         81 seekTo($offset) ;
5646              
5647 26         219 my $buffer;
5648 26         99 myRead($buffer, $len);
5649              
5650 26 50       80 length $buffer == $len
5651             or return '';
5652              
5653 26         142 return $buffer;
5654             }
5655              
5656             sub readSignatureFromOffset
5657             {
5658 24     24   40 my $offset = shift ;
5659              
5660             # catch use case where attempting to read past EOF
5661             # sub is expecting to return a 32-bit value so return 54-bit out-of-bound value
5662 24 50       104 return MAX64
5663             if $offset + 4 > $FILELEN ;
5664              
5665 24         106 my $here = $FH->tell();
5666 24         182 my $buffer = readFromOffset($offset, 4);
5667 24         129 my $gotSig = unpack("V", $buffer) ;
5668 24         67 seekTo($here);
5669              
5670 24         120 return $gotSig;
5671             }
5672              
5673              
5674             sub chckForAPKSigningBlock
5675             {
5676 2     2   3 my $fh = shift;
5677 2         4 my $cdOffset = shift;
5678 2         16 my $cdSize = shift;
5679              
5680             # APK Signing Block comes directy before the Central directory
5681             # See https://source.android.com/security/apksigning/v2
5682              
5683             # If offset available is less than 44, it isn't an APK signing block
5684             #
5685             # len1 8
5686             # id 4
5687             # kv with zero len 8
5688             # len1 8
5689             # magic 16
5690             # ----------
5691             # 44
5692              
5693 2 50 33     16 return (0, 0, '')
5694             if $cdOffset < 44 || $FILELEN - $cdSize < 44 ;
5695              
5696             # Step 1 - 16 bytes before CD is literal string "APK Sig Block 42"
5697 2         4 my $magicOffset = $cdOffset - 16;
5698 2         6 my $buffer = readFromOffset($magicOffset, 16);
5699              
5700 2 50       39 return (0, 0, '')
5701             if $buffer ne "APK Sig Block 42" ;
5702              
5703             # Step 2 - read the second length field
5704             # and check that it looks ok
5705 0         0 $buffer = readFromOffset($cdOffset - 16 - 8, 8);
5706 0         0 my $len2 = unpack("Q<", $buffer);
5707              
5708 0 0 0     0 return (0, 0, '')
5709             if $len2 == 0 || $len2 > $FILELEN;
5710              
5711             # Step 3 - read the first length field.
5712             # It should be identical to the second one.
5713              
5714 0         0 my $startApkOffset = $cdOffset - 8 - $len2 ;
5715              
5716 0         0 $buffer = readFromOffset($startApkOffset, 8);
5717 0         0 my $len1 = unpack("Q<", $buffer);
5718              
5719 0 0       0 return (0, 0, '')
5720             if $len1 != $len2;
5721              
5722 0         0 return ($startApkOffset, $cdOffset - 16 - 8, $buffer);
5723             }
5724              
5725             sub scanApkBlock
5726             {
5727 0     0   0 state $IDs = {
5728             0x7109871a => "APK Signature v2",
5729             0xf05368c0 => "APK Signature v3",
5730             0x42726577 => "Verity Padding Block", # from https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
5731             0x6dff800d => "Source Stamp",
5732             0x504b4453 => "Dependency Info",
5733             0x71777777 => "APK Channel Block",
5734             0xff3b5998 => "Zero Block",
5735             0x2146444e => "Play Metadata",
5736             } ;
5737              
5738              
5739 0         0 seekTo($FH->tell() - 4) ;
5740 0         0 print "\n";
5741 0         0 out "", "APK SIGNING BLOCK";
5742              
5743 0         0 scanApkPadding();
5744 0         0 out_Q "Block Length Copy #1";
5745 0         0 my $ix = 1;
5746              
5747 0         0 while ($FH->tell() < $APK - 8)
5748             {
5749 0         0 my ($bytes, $id, $len);
5750 0         0 ($bytes, $len) = read_Q ;
5751 0         0 out $bytes, "ID/Value Length #" . sprintf("%X", $ix), Value_Q($len);
5752              
5753 0         0 ($bytes, $id) = read_V;
5754              
5755 0   0     0 out $bytes, " ID", Value_V($id) . " '" . ($IDs->{$id} // 'Unknown ID') . "'";
5756              
5757 0         0 outSomeData($len-4, " Value");
5758 0         0 ++ $ix;
5759             }
5760              
5761 0         0 out_Q "Block Length Copy #2";
5762              
5763 0         0 my $magic ;
5764 0         0 myRead($magic, 16);
5765              
5766 0         0 out $magic, "Magic", qq['$magic'];
5767             }
5768              
5769             sub scanApkPadding
5770             {
5771 0     0   0 my $here = $FH->tell();
5772              
5773             return
5774 0 0       0 if $here == $START_APK;
5775              
5776             # found some padding
5777              
5778 0         0 my $delta = $START_APK - $here;
5779 0         0 my $padding = peekAtOffset($here, $delta);
5780              
5781 0 0       0 if ($padding =~ /^\x00+$/)
5782             {
5783 0         0 outSomeData($delta, "Null Padding");
5784             }
5785             else
5786             {
5787 0         0 outHexdump($delta, "Unexpected Padding");
5788             }
5789             }
5790              
5791             sub scanCentralDirectory
5792             {
5793 2     2   5 my $fh = shift;
5794              
5795 2         7 my $here = $fh->tell();
5796              
5797             # Use cases
5798             # 1 32-bit CD
5799             # 2 64-bit CD
5800              
5801 2         14 my ($offset, $size) = findCentralDirectoryOffset($fh);
5802 2         19 $CentralDirectory->{CentralDirectoryOffset} = $offset;
5803 2         5 $CentralDirectory->{CentralDirectorySize} = $size;
5804              
5805             return ()
5806 2 50       12 if ! defined $offset;
5807              
5808 2         9 $fh->seek($offset, SEEK_SET) ;
5809              
5810             # Now walk the Central Directory Records
5811 2         25 my $buffer ;
5812 2         4 my $cdIndex = 0;
5813 2         4 my $cdEntryOffset = 0;
5814              
5815 2   66     8 while ($fh->read($buffer, ZIP_CD_FILENAME_OFFSET) == ZIP_CD_FILENAME_OFFSET &&
5816             unpack("V", $buffer) == ZIP_CENTRAL_HDR_SIG) {
5817              
5818 4         96 my $startHeader = $fh->tell() - ZIP_CD_FILENAME_OFFSET;
5819              
5820 4         26 my $cdEntryOffset = $fh->tell() - ZIP_CD_FILENAME_OFFSET;
5821 4         43 $HeaderOffsetIndex->addOffsetNoPrefix($cdEntryOffset, ZIP_CENTRAL_HDR_SIG) ;
5822              
5823 4         8 ++ $cdIndex ;
5824              
5825 4         15 my $extractVer = unpack("v", substr($buffer, 6, 1));
5826 4         11 my $gpFlag = unpack("v", substr($buffer, 8, 2));
5827 4         8 my $lastMod = unpack("V", substr($buffer, 10, 4));
5828 4         11 my $crc = unpack("V", substr($buffer, 16, 4));
5829 4         8 my $compressedSize = unpack("V", substr($buffer, 20, 4));
5830 4         74 my $uncompressedSize = unpack("V", substr($buffer, 24, 4));
5831 4         40 my $filename_length = unpack("v", substr($buffer, 28, 2));
5832 4         114 my $extra_length = unpack("v", substr($buffer, 30, 2));
5833 4         9 my $comment_length = unpack("v", substr($buffer, 32, 2));
5834 4         9 my $diskNumber = unpack("v", substr($buffer, 34, 2));
5835 4         9 my $locHeaderOffset = unpack("V", substr($buffer, 42, 4));
5836              
5837 4         7 my $cdZip64 = 0;
5838 4         6 my $zip64Sizes = 0;
5839              
5840 4 50       12 if (! full32 $locHeaderOffset)
5841             {
5842             # Check for corrupt offset
5843             # 1. ponting paset EOF
5844             # 2. offset points forward in the file
5845             # 3. value at offset is not a CD record signature
5846              
5847 4         113 my $commonMessage = "'Local Header Offset' field in '" . Signatures::name(ZIP_CENTRAL_HDR_SIG) . "' is invalid";
5848 4         22 checkOffsetValue($locHeaderOffset, $startHeader, 0, $commonMessage,
5849             $startHeader + CentralDirectoryEntry::Offset_RelativeOffsetToLocal(),
5850             ZIP_LOCAL_HDR_SIG, 1) ;
5851             }
5852              
5853 4         15 $fh->read(my $filename, $filename_length) ;
5854              
5855 4         56 my $cdEntry = CentralDirectoryEntry->new();
5856              
5857 4         13 $cdEntry->centralHeaderOffset($startHeader) ;
5858 4         9 $cdEntry->localHeaderOffset($locHeaderOffset) ;
5859 4         12 $cdEntry->compressedSize($compressedSize) ;
5860 4         11 $cdEntry->uncompressedSize($uncompressedSize) ;
5861 4         11 $cdEntry->extractVersion($extractVer);
5862 4         10 $cdEntry->generalPurposeFlags($gpFlag);
5863 4         10 $cdEntry->filename($filename) ;
5864 4         10 $cdEntry->lastModDateTime($lastMod);
5865 4         13 $cdEntry->languageEncodingFlag($gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING) ;
5866 4         10 $cdEntry->diskNumber($diskNumber) ;
5867 4         97 $cdEntry->crc32($crc) ;
5868 4         11 $cdEntry->zip64ExtraPresent($cdZip64) ;
5869              
5870 4         11 $cdEntry->std_localHeaderOffset($locHeaderOffset) ;
5871 4         10 $cdEntry->std_compressedSize($compressedSize) ;
5872 4         11 $cdEntry->std_uncompressedSize($uncompressedSize) ;
5873 4         30 $cdEntry->std_diskNumber($diskNumber) ;
5874              
5875              
5876 4 50       16 if ($extra_length)
5877             {
5878 0         0 $fh->read(my $extraField, $extra_length) ;
5879              
5880             # Check for Zip64
5881 0         0 my $zip64Extended = findID(0x0001, $extraField);
5882              
5883 0 0       0 if ($zip64Extended)
5884             {
5885 0         0 $cdZip64 = 1;
5886 0         0 walk_Zip64_in_CD(1, $zip64Extended, $cdEntry, 0);
5887             }
5888             }
5889              
5890 4         12 $cdEntry->offsetStart($startHeader) ;
5891 4         20 $cdEntry->offsetEnd($FH->tell() - 1);
5892              
5893             # don't call addEntry until after the extra fields have been scanned
5894             # the localheader offset value may be updated in th ezip64 extra field.
5895 4         17 $CentralDirectory->addEntry($cdEntry);
5896 4         9 $HeaderOffsetIndex->addOffset($cdEntry->localHeaderOffset, ZIP_LOCAL_HDR_SIG) ;
5897              
5898 4         13 skip($fh, $comment_length ) ;
5899             }
5900              
5901 2         42 $FH->seek($fh->tell() - ZIP_CD_FILENAME_OFFSET, SEEK_SET);
5902              
5903             # Check for Digital Signature
5904 2 50 33     27 $HeaderOffsetIndex->addOffset($fh->tell() - 4, ZIP_DIGITAL_SIGNATURE_SIG)
5905             if $fh->read($buffer, 4) == 4 &&
5906             unpack("V", $buffer) == ZIP_DIGITAL_SIGNATURE_SIG ;
5907              
5908 2         69 $CentralDirectory->sortByLocalOffset();
5909 2         9 $HeaderOffsetIndex->sortOffsets();
5910              
5911 2         11 $fh->seek($here, SEEK_SET) ;
5912              
5913             }
5914              
5915 6     6   75 use constant ZIP64_END_CENTRAL_LOC_HDR_SIZE => 20;
  6         15  
  6         1256  
5916 6     6   39 use constant ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE => 56;
  6         11  
  6         6887  
5917              
5918             sub offsetFromZip64
5919             {
5920 0     0   0 my $fh = shift ;
5921 0         0 my $here = shift;
5922 0         0 my $eocdSize = shift;
5923              
5924             #### Zip64 end of central directory locator
5925              
5926             # check enough bytes available for zip64 locator record
5927 0 0       0 fatal_tryWalk undef, "Cannot find signature for " . Signatures::nameAndHex(ZIP64_END_CENTRAL_LOC_HDR_SIG), # 'Zip64 end of central directory locator': 0x07064b50"
5928             "Possible truncated or corrupt zip file"
5929             if $here < ZIP64_END_CENTRAL_LOC_HDR_SIZE ;
5930              
5931 0         0 $fh->seek($here - ZIP64_END_CENTRAL_LOC_HDR_SIZE, SEEK_SET) ;
5932 0         0 $here = $FH->tell();
5933              
5934 0         0 my $buffer;
5935 0         0 my $got = 0;
5936 0         0 $fh->read($buffer, ZIP64_END_CENTRAL_LOC_HDR_SIZE);
5937              
5938 0         0 my $gotSig = unpack("V", $buffer);
5939 0 0       0 fatal_tryWalk $here - 4, sprintf("Expected signature for " . Signatures::nameAndHex(ZIP64_END_CENTRAL_LOC_HDR_SIG) . " not found, got 0x%X", $gotSig)
5940             if $gotSig != ZIP64_END_CENTRAL_LOC_HDR_SIG ;
5941              
5942 0         0 $HeaderOffsetIndex->addOffset($fh->tell() - ZIP64_END_CENTRAL_LOC_HDR_SIZE, ZIP64_END_CENTRAL_LOC_HDR_SIG) ;
5943              
5944              
5945 0         0 my $cd64 = unpack "Q<", substr($buffer, 8, 8);
5946 0         0 my $totalDisks = unpack "V", substr($buffer, 16, 4);
5947              
5948 0         0 testPossiblePrefix($cd64, ZIP64_END_CENTRAL_REC_HDR_SIG);
5949              
5950 0 0       0 if ($totalDisks > 0)
5951             {
5952 0         0 my $commonMessage = "'Offset to Zip64 End of Central Directory Record' field in '" . Signatures::name(ZIP64_END_CENTRAL_LOC_HDR_SIG) . "' is invalid";
5953 0         0 $cd64 = checkOffsetValue($cd64, $here, 0, $commonMessage, $here + 8, ZIP64_END_CENTRAL_REC_HDR_SIG, 1) ;
5954             }
5955              
5956 0         0 my $delta = $here - $cd64;
5957              
5958             #### Zip64 end of central directory record
5959              
5960 0         0 my $zip64eocd_name = "'" . Signatures::name(ZIP64_END_CENTRAL_REC_HDR_SIG) . "'";
5961 0         0 my $zip64eocd_name_value = Signatures::nameAndHex(ZIP64_END_CENTRAL_REC_HDR_SIG);
5962 0         0 my $zip64eocd_value = Signatures::hexValue(ZIP64_END_CENTRAL_REC_HDR_SIG);
5963              
5964             # check enough bytes available
5965             # fatal_tryWalk sprintf "Size of 'Zip64 End of Central Directory Record' 0x%X too small", $cd64
5966 0 0       0 fatal_tryWalk undef, sprintf "Size of $zip64eocd_name 0x%X too small", $cd64
5967             if $delta < ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE;
5968              
5969             # Seek to Zip64 End of Central Directory Record
5970 0         0 $fh->seek($cd64, SEEK_SET) ;
5971 0         0 $HeaderOffsetIndex->addOffsetNoPrefix($fh->tell(), ZIP64_END_CENTRAL_REC_HDR_SIG) ;
5972              
5973 0         0 $fh->read($buffer, ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE) ;
5974              
5975 0         0 my $sig = unpack("V", substr($buffer, 0, 4)) ;
5976 0 0       0 fatal_tryWalk undef, sprintf "Cannot find $zip64eocd_name: expected $zip64eocd_value but got 0x%X", $sig
5977             if $sig != ZIP64_END_CENTRAL_REC_HDR_SIG ;
5978              
5979             # pkzip sets the extract zip spec to 6.2 (0x3E) to signal a v2 record
5980             # See APPNOTE 6.3.10, section, 7.3.3
5981              
5982             # Version 1 header is 44 bytes (assuming no extensible data sector)
5983             # Version 2 header (see APPNOTE 6.3.7, section) is > 44 bytes
5984              
5985 0         0 my $extractSpec = unpack "C", substr($buffer, 14, 1);
5986 0         0 my $diskNumber = unpack "V", substr($buffer, 16, 4);
5987 0         0 my $cdDiskNumber = unpack "V", substr($buffer, 20, 4);
5988 0         0 my $entriesOnThisDisk = unpack "Q<", substr($buffer, 24, 8);
5989 0         0 my $totalEntries = unpack "Q<", substr($buffer, 32, 8);
5990 0         0 my $centralDirSize = unpack "Q<", substr($buffer, 40, 8);
5991 0         0 my $centralDirOffset = unpack "Q<", substr($buffer, 48, 8);
5992              
5993 0 0       0 if ($extractSpec >= 0x3E)
5994             {
5995 0         0 $opt_walk = 1;
5996 0         0 $CentralDirectory->setPkEncryptedCD();
5997             }
5998              
5999 0 0       0 if (! emptyArchive($here, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset))
6000             {
6001 0         0 my $commonMessage = "'Offset to Central Directory' field in $zip64eocd_name is invalid";
6002 0         0 $centralDirOffset = checkOffsetValue($centralDirOffset, $here, 0, $commonMessage, $here + 48, ZIP_CENTRAL_HDR_SIG, 1, $extractSpec < 0x3E) ;
6003             }
6004              
6005             # TODO - APPNOTE allows an extensible data sector here (see APPNOTE 6.3.10, section 4.3.14.2) -- need to take this into account
6006              
6007 0         0 return ($centralDirOffset, $centralDirSize) ;
6008             }
6009              
6010 6     6   54 use constant Pack_ZIP_END_CENTRAL_HDR_SIG => pack("V", ZIP_END_CENTRAL_HDR_SIG);
  6         16  
  6         19629  
6011              
6012             sub findCentralDirectoryOffset
6013             {
6014 2     2   4 my $fh = shift ;
6015              
6016             # Most common use-case is where there is no comment, so
6017             # know exactly where the end of central directory record
6018             # should be.
6019              
6020 2         5 need ZIP_EOCD_MIN_SIZE, Signatures::name(ZIP_END_CENTRAL_HDR_SIG);
6021              
6022 2         10 $fh->seek(-ZIP_EOCD_MIN_SIZE(), SEEK_END) ;
6023 2         20 my $here = $fh->tell();
6024              
6025 2         11 my $is64bit = $here > MAX32;
6026 2         4 my $over64bit = $here & (~ MAX32);
6027              
6028 2         3 my $buffer;
6029 2         9 $fh->read($buffer, ZIP_EOCD_MIN_SIZE);
6030              
6031 2         24 my $zip64 = 0;
6032 2         91 my $diskNumber ;
6033             my $cdDiskNumber ;
6034 2         0 my $entriesOnThisDisk ;
6035 2         0 my $totalEntries ;
6036 2         0 my $centralDirSize ;
6037 2         0 my $centralDirOffset ;
6038 2         4 my $commentLength = 0;
6039 2         4 my $trailingBytes = 0;
6040              
6041 2 50       17 if ( unpack("V", $buffer) == ZIP_END_CENTRAL_HDR_SIG ) {
6042              
6043 2         11 $HeaderOffsetIndex->addOffset($here + $PREFIX_DELTA, ZIP_END_CENTRAL_HDR_SIG) ;
6044              
6045 2         10 $diskNumber = unpack("v", substr($buffer, 4, 2));
6046 2         5 $cdDiskNumber = unpack("v", substr($buffer, 6, 2));
6047 2         17 $entriesOnThisDisk= unpack("v", substr($buffer, 8, 2));
6048 2         6 $totalEntries = unpack("v", substr($buffer, 10, 2));
6049 2         5 $centralDirSize = unpack("V", substr($buffer, 12, 4));
6050 2         6 $centralDirOffset = unpack("V", substr($buffer, 16, 4));
6051 2         6 $commentLength = unpack("v", substr($buffer, 20, 2));
6052             }
6053             else {
6054 0         0 $fh->seek(0, SEEK_END) ;
6055              
6056 0         0 my $fileLen = $fh->tell();
6057 0         0 my $want = 0 ;
6058              
6059 0         0 while(1) {
6060 0         0 $want += 1024 * 32;
6061 0         0 my $seekTo = $fileLen - $want;
6062 0 0       0 if ($seekTo < 0 ) {
6063 0         0 $seekTo = 0;
6064 0         0 $want = $fileLen ;
6065             }
6066 0         0 $fh->seek( $seekTo, SEEK_SET);
6067 0         0 $fh->read($buffer, $want) ;
6068 0         0 my $pos = rindex( $buffer, Pack_ZIP_END_CENTRAL_HDR_SIG);
6069              
6070 0 0 0     0 if ($pos >= 0 && $want - $pos > ZIP_EOCD_MIN_SIZE) {
6071 0         0 $here = $seekTo + $pos ;
6072 0         0 $HeaderOffsetIndex->addOffset($here + $PREFIX_DELTA, ZIP_END_CENTRAL_HDR_SIG) ;
6073              
6074 0         0 $diskNumber = unpack("v", substr($buffer, $pos + 4, 2));
6075 0         0 $cdDiskNumber = unpack("v", substr($buffer, $pos + 6, 2));
6076 0         0 $entriesOnThisDisk= unpack("v", substr($buffer, $pos + 8, 2));
6077 0         0 $totalEntries = unpack("v", substr($buffer, $pos + 10, 2));
6078 0         0 $centralDirSize = unpack("V", substr($buffer, $pos + 12, 4));
6079 0         0 $centralDirOffset = unpack("V", substr($buffer, $pos + 16, 4));
6080 0   0     0 $commentLength = unpack("v", substr($buffer, $pos + 20, 2)) // 0;
6081              
6082 0         0 my $expectedEof = $fileLen - $want + $pos + ZIP_EOCD_MIN_SIZE + $commentLength ;
6083             # check for trailing data after end of zip
6084 0 0       0 if ($expectedEof < $fileLen ) {
6085 0         0 $TRAILING = $expectedEof ;
6086 0         0 $trailingBytes = $FILELEN - $expectedEof ;
6087             }
6088 0         0 last ;
6089             }
6090              
6091             return undef
6092 0 0       0 if $want == $fileLen;
6093              
6094             }
6095             }
6096              
6097 2         4 $EOCD_Present = 1;
6098              
6099             # Empty zip file can just contain an EOCD record
6100 2 50       11 return (0, 0)
6101             if ZIP_EOCD_MIN_SIZE + $commentLength + $trailingBytes == $FILELEN ;
6102              
6103 2 50 33     11 if (needZip64EOCDLocator($diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirOffset, $centralDirSize) &&
    50          
6104             ! emptyArchive($here, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirOffset, $centralDirSize))
6105             {
6106 0         0 ($centralDirOffset, $centralDirSize) = offsetFromZip64($fh, $here, ZIP_EOCD_MIN_SIZE + $commentLength + $trailingBytes)
6107             }
6108             elsif ($is64bit)
6109             {
6110             # use-case is where a 64-bit zip file doesn't use the 64-bit
6111             # extensions.
6112             # print "EOCD not 64-bit $centralDirOffset ($here)\n" ;
6113              
6114 0         0 fatal_tryWalk $here, "Zip file > 4Gig. Expected 'Offset to Central Dir' to be 0xFFFFFFFF, got " . hexValue($centralDirOffset);
6115              
6116 0         0 $centralDirOffset += $over64bit;
6117 0         0 $is64In32 = 1;
6118             }
6119             else
6120             {
6121 2 50       8 if ($centralDirSize)
6122             {
6123 2         5 my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name(ZIP_END_CENTRAL_HDR_SIG) . "' is invalid";
6124 2         30 $centralDirOffset = checkOffsetValue($centralDirOffset, $here, $centralDirSize, $commonMessage, $here + 16, ZIP_CENTRAL_HDR_SIG, 1) ;
6125             }
6126             }
6127              
6128 2 50 33     9 return (0, 0)
6129             if $totalEntries == 0 && $entriesOnThisDisk == 0;
6130              
6131             # APK Signing Block is directly before the first CD entry
6132             # Check if it is present
6133 2         9 ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($fh, $centralDirOffset, ZIP_EOCD_MIN_SIZE + $commentLength);
6134              
6135 2         19 return ($centralDirOffset, $centralDirSize) ;
6136             }
6137              
6138             sub findID
6139             {
6140 0     0   0 my $id_want = shift ;
6141 0         0 my $data = shift;
6142              
6143 0         0 my $XLEN = length $data ;
6144              
6145 0         0 my $offset = 0 ;
6146 0         0 while ($offset < $XLEN) {
6147              
6148             return undef
6149 0 0       0 if $offset + ZIP_EXTRA_SUBFIELD_HEADER_SIZE > $XLEN ;
6150              
6151 0         0 my $id = substr($data, $offset, ZIP_EXTRA_SUBFIELD_ID_SIZE);
6152 0         0 $id = unpack("v", $id);
6153 0         0 $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE;
6154              
6155 0         0 my $subLen = unpack("v", substr($data, $offset,
6156             ZIP_EXTRA_SUBFIELD_LEN_SIZE));
6157 0         0 $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE ;
6158              
6159             return undef
6160 0 0       0 if $offset + $subLen > $XLEN ;
6161              
6162 0 0       0 return substr($data, $offset, $subLen)
6163             if $id eq $id_want ;
6164              
6165 0         0 $offset += $subLen ;
6166             }
6167              
6168 0         0 return undef ;
6169             }
6170              
6171              
6172             sub nibbles
6173             {
6174 6     6   86 my @nibbles = (
6175             [ 16 => 0x1000000000000000 ],
6176             [ 15 => 0x100000000000000 ],
6177             [ 14 => 0x10000000000000 ],
6178             [ 13 => 0x1000000000000 ],
6179             [ 12 => 0x100000000000 ],
6180             [ 11 => 0x10000000000 ],
6181             [ 10 => 0x1000000000 ],
6182             [ 9 => 0x100000000 ],
6183             [ 8 => 0x10000000 ],
6184             [ 7 => 0x1000000 ],
6185             [ 6 => 0x100000 ],
6186             [ 5 => 0x10000 ],
6187             [ 4 => 0x1000 ],
6188             [ 4 => 0x100 ],
6189             [ 4 => 0x10 ],
6190             [ 4 => 0x1 ],
6191             );
6192 6         15 my $value = shift ;
6193              
6194 6         16 for my $pair (@nibbles)
6195             {
6196 90         115 my ($count, $limit) = @{ $pair };
  90         149  
6197              
6198 90 100       193 return $count
6199             if $value >= $limit ;
6200             }
6201             }
6202              
6203             {
6204 0         0 package HeaderOffsetEntry;
6205              
6206             sub new
6207             {
6208 10     10   19 my $class = shift ;
6209 10         15 my $offset = shift ;
6210 10         17 my $signature = shift;
6211              
6212 10         23 bless [ $offset, $signature, Signatures::name($signature)] , $class;
6213              
6214             }
6215              
6216             sub offset
6217             {
6218 10     10   18 my $self = shift;
6219 10         29 return $self->[0];
6220             }
6221              
6222             sub signature
6223             {
6224 10     10   16 my $self = shift;
6225 10         19 return $self->[1];
6226             }
6227              
6228             sub name
6229             {
6230 0     0   0 my $self = shift;
6231 0         0 return $self->[2];
6232             }
6233              
6234             }
6235              
6236             {
6237 0         0 package HeaderOffsetIndex;
  0         0  
6238              
6239             # Store a list of header offsets recorded when scannning the central directory
6240              
6241             sub new
6242             {
6243 6     6   13 my $class = shift ;
6244              
6245 6         33 my %object = (
6246             'offsetIndex' => [],
6247             'offset2Index' => {},
6248             'offset2Signature' => {},
6249             'currentIndex' => -1,
6250             'currentSignature' => 0,
6251             # 'sigNames' => $sigNames,
6252             ) ;
6253              
6254 6         19 bless \%object, $class;
6255             }
6256              
6257             sub sortOffsets
6258             {
6259 2     2   4 my $self = shift ;
6260              
6261 2         7 @{ $self->{offsetIndex} } = sort { $a->[0] <=> $b->[0] }
  14         35  
6262 2         15 @{ $self->{offsetIndex} };
  2         8  
6263 2         4 my $ix = 0;
6264             $self->{offset2Index}{$_} = $ix++
6265 2         4 for @{ $self->{offsetIndex} } ;
  2         68  
6266             }
6267              
6268             sub addOffset
6269             {
6270 6     6   9 my $self = shift ;
6271 6         10 my $offset = shift ;
6272 6         13 my $signature = shift ;
6273              
6274 6         12 $offset += $PREFIX_DELTA ;
6275 6         19 $self->addOffsetNoPrefix($offset, $signature);
6276             }
6277              
6278             sub addOffsetNoPrefix
6279             {
6280 10     10   17 my $self = shift ;
6281 10         15 my $offset = shift ;
6282 10         19 my $signature = shift ;
6283              
6284 10         28 my $name = Signatures::name($signature);
6285              
6286 10 50       51 if (! defined $self->{offset2Signature}{$offset})
6287             {
6288 10         16 push @{ $self->{offsetIndex} }, HeaderOffsetEntry->new($offset, $signature) ;
  10         132  
6289 10         35 $self->{offset2Signature}{$offset} = $signature;
6290             }
6291             }
6292              
6293             sub getNextIndex
6294             {
6295 10     10   16 my $self = shift ;
6296 10         13 my $offset = shift ;
6297              
6298 10         18 $self->{currentIndex} ++;
6299              
6300 10   50     13 return ${ $self->{offsetIndex} }[$self->{currentIndex}] // undef
  10         65  
6301             }
6302              
6303             sub rewindIndex
6304             {
6305 0     0   0 my $self = shift ;
6306 0         0 my $offset = shift ;
6307              
6308 0         0 $self->{currentIndex} --;
6309             }
6310              
6311             sub dump
6312             {
6313 0     0   0 my $self = shift;
6314              
6315 0         0 say "### HeaderOffsetIndex";
6316 0         0 say "### Offset\tSignature";
6317 0         0 for my $x ( @{ $self->{offsetIndex} } )
  0         0  
6318             {
6319 0         0 my ($offset, $sig) = @$x;
6320 0         0 printf "### %X %d\t\t" . $x->name() . "\n", $x->offset(), $x->offset();
6321             }
6322             }
6323              
6324             sub checkForOverlap
6325             {
6326 0     0   0 my $self = shift ;
6327 0         0 my $need = shift;
6328              
6329 0         0 my $needOffset = $FH->tell() + $need;
6330              
6331 0         0 for my $hdrOffset (@{ $self->{offsetIndex} })
  0         0  
6332             {
6333 0         0 my $delta = $hdrOffset - $needOffset;
6334 0 0       0 return [$self->{offsetIndex}{$hdrOffset}, $needOffset - $hdrOffset]
6335             if $delta <= 0 ;
6336             }
6337              
6338 0         0 return [undef, undef];
6339             }
6340              
6341             }
6342              
6343             {
6344 0         0 package FieldsAndAccessors;
  0         0  
6345              
6346             sub Add
6347             {
6348 6     6   4636 use Data::Dumper ;
  6         60587  
  6         1317  
6349              
6350 72     72   127 my $classname = shift;
6351 72         102 my $object = shift;
6352 72         103 my $fields = shift ;
6353 72   50     261 my $no_handler = shift // {};
6354              
6355 72         114 state $done = {};
6356              
6357              
6358 72         274 while (my ($name, $value) = each %$fields)
6359             {
6360 948         1397 my $method = "${classname}::$name";
6361              
6362 948         2986 $object->{$name} = $value;
6363              
6364             # don't auto-create a handler
6365             next
6366 948 50       1840 if $no_handler->{$name};
6367              
6368 6     6   56 no strict 'refs';
  6         15  
  6         2859  
6369              
6370             # Don't use lvalue sub for now - vscode debugger breaks with it enabled.
6371             # https://github.com/richterger/Perl-LanguageServer/issues/194
6372             # *$method = sub : lvalue {
6373             # $_[0]->{$name} ;
6374             # }
6375             # unless defined $done->{$method};
6376              
6377             # Auto-generate getter/setter
6378             *$method = sub {
6379 1190 100   1190   2874 $_[0]->{$name} = $_[1]
6380             if @_ == 2;
6381 1190         2842 return $_[0]->{$name} ;
6382             }
6383 948 100       4485 unless defined $done->{$method};
6384              
6385 948         3331 ++ $done->{$method};
6386              
6387              
6388             }
6389             }
6390             }
6391              
6392             {
6393 0         0 package BaseEntry ;
  0         0  
6394              
6395             sub new
6396             {
6397 24     24   43 my $class = shift ;
6398              
6399 24         43 state $index = 0;
6400              
6401 24         208 my %fields = (
6402             'index' => $index ++,
6403             'zip64' => 0,
6404             'offsetStart' => 0,
6405             'offsetEnd' => 0,
6406             'inCentralDir' => 0,
6407             'encapsulated' => 0, # enclosed in outer zip
6408             'childrenCount' => 0, # this entry is a zip with enclosed children
6409             'streamed' => 0,
6410             'languageEncodingFlag' => 0,
6411             'entryType' => 0,
6412             ) ;
6413              
6414 24         69 my $self = bless {}, $class;
6415              
6416 24         101 FieldsAndAccessors::Add($class, $self, \%fields) ;
6417              
6418 24         90 return $self;
6419             }
6420              
6421             sub increment_childrenCount
6422             {
6423 0     0   0 my $self = shift;
6424 0         0 $self->{childrenCount} ++;
6425             }
6426             }
6427              
6428             {
6429 0         0 package LocalCentralEntryBase ;
  0         0  
6430              
6431 6     6   55 use parent -norequire , 'BaseEntry' ;
  6         14  
  6         85  
6432              
6433             sub new
6434             {
6435 24     24   46 my $class = shift ;
6436              
6437 24         186 my $self = $class->SUPER::new();
6438              
6439              
6440 24         462 my %fields = (
6441             # fields from the header
6442             'centralHeaderOffset' => 0,
6443             'localHeaderOffset' => 0,
6444              
6445             'extractVersion' => 0,
6446             'generalPurposeFlags' => 0,
6447             'compressedMethod' => 0,
6448             'lastModDateTime' => 0,
6449             'crc32' => 0,
6450             'compressedSize' => 0,
6451             'uncompressedSize' => 0,
6452             'filename' => '',
6453             'outputFilename' => '',
6454             # inferred data
6455             # 'InCentralDir' => 0,
6456             # 'zip64' => 0,
6457              
6458             'zip64ExtraPresent' => 0,
6459             'zip64SizesPresent' => 0,
6460             'payloadOffset' => 0,
6461              
6462             # zip64 extra
6463             'zip64_compressedSize' => undef,
6464             'zip64_uncompressedSize' => undef,
6465             'zip64_localHeaderOffset' => undef,
6466             'zip64_diskNumber' => undef,
6467             'zip64_diskNumberPresent' => 0,
6468              
6469             # Values direct from the header before merging any Zip64 values
6470             'std_compressedSize' => undef,
6471             'std_uncompressedSize' => undef,
6472             'std_localHeaderOffset' => undef,
6473             'std_diskNumber' => undef,
6474              
6475             # AES
6476             'aesStrength' => 0,
6477             'aesValid' => 0,
6478              
6479             # Minizip CD encryption
6480             'minizip_secure' => 0,
6481              
6482             ) ;
6483              
6484 24         97 FieldsAndAccessors::Add($class, $self, \%fields) ;
6485              
6486 24         135 return $self;
6487             }
6488             }
6489              
6490             {
6491 0         0 package Zip64EndCentralHeaderEntry ;
  0         0  
6492              
6493 6     6   2240 use parent -norequire , 'LocalCentralEntryBase' ;
  6         16  
  6         95  
6494              
6495             sub new
6496             {
6497 0     0   0 my $class = shift ;
6498              
6499 0         0 my $self = $class->SUPER::new();
6500              
6501              
6502 0         0 my %fields = (
6503             'inCentralDir' => 1,
6504             ) ;
6505              
6506 0         0 FieldsAndAccessors::Add($class, $self, \%fields) ;
6507              
6508 0         0 return $self;
6509             }
6510              
6511             }
6512              
6513             {
6514 0         0 package CentralDirectoryEntry;
  0         0  
6515              
6516 6     6   1494 use parent -norequire , 'LocalCentralEntryBase' ;
  6         24  
  6         66  
6517              
6518 6     6   488 use constant Offset_VersionMadeBy => 4;
  6         30  
  6         612  
6519 6     6   60 use constant Offset_VersionNeededToExtract => 6;
  6         13  
  6         574  
6520 6     6   38 use constant Offset_GeneralPurposeFlags => 8;
  6         14  
  6         515  
6521 6     6   37 use constant Offset_CompressionMethod => 10;
  6         13  
  6         311  
6522 6     6   31 use constant Offset_ModificationTime => 12;
  6         25  
  6         371  
6523 6     6   33 use constant Offset_ModificationDate => 14;
  6         12  
  6         364  
6524 6     6   35 use constant Offset_CRC32 => 16;
  6         13  
  6         10100  
6525 6     6   2344 use constant Offset_CompressedSize => 20;
  6         14  
  6         2395  
6526 6     6   39 use constant Offset_UncompressedSize => 24;
  6         14  
  6         415  
6527 6     6   33 use constant Offset_FilenameLength => 28;
  6         10  
  6         275  
6528 6     6   27 use constant Offset_ExtraFieldLength => 30;
  6         12  
  6         291  
6529 6     6   31 use constant Offset_FileCommentLength => 32;
  6         12  
  6         257  
6530 6     6   30 use constant Offset_DiskNumber => 34;
  6         12  
  6         243  
6531 6     6   28 use constant Offset_InternalAttributes => 36;
  6         11  
  6         284  
6532 6     6   31 use constant Offset_ExternalAttributes => 38;
  6         10  
  6         344  
6533 6     6   31 use constant Offset_RelativeOffsetToLocal => 42;
  6         10  
  6         356  
6534 6     6   29 use constant Offset_Filename => 46;
  6         11  
  6         11592  
6535              
6536             sub new
6537             {
6538 16     16   33 my $class = shift ;
6539 16         27 my $offset = shift;
6540              
6541             # check for existing entry
6542             return $CentralDirectory->{byCentralOffset}{$offset}
6543 16 100 100     133 if defined $offset && defined $CentralDirectory->{byCentralOffset}{$offset} ;
6544              
6545 12         79 my $self = $class->SUPER::new();
6546              
6547 12         58 my %fields = (
6548             'diskNumber' => 0,
6549             'comment' => "",
6550             'ldEntry' => undef,
6551             ) ;
6552              
6553 12         47 FieldsAndAccessors::Add($class, $self, \%fields) ;
6554              
6555 12         44 $self->inCentralDir(1) ;
6556 12         50 $self->entryType(::ZIP_CENTRAL_HDR_SIG) ;
6557              
6558 12         38 return $self;
6559             }
6560             }
6561              
6562             {
6563 0         0 package CentralDirectory;
  0         0  
6564              
6565             sub new
6566             {
6567 6     6   16 my $class = shift ;
6568              
6569 6         74 my %object = (
6570             'entries' => [],
6571             'count' => 0,
6572             'byLocalOffset' => {},
6573             'byCentralOffset' => {},
6574             'byName' => {},
6575             'offset2Index' => {},
6576             'normalized_filenames' => {},
6577             'CentralDirectoryOffset' => 0,
6578             'CentralDirectorySize' => 0,
6579             'zip64' => 0,
6580             'encryptedCD' => 0,
6581             'minizip_secure' => 0,
6582             'alreadyScanned' => 0,
6583             ) ;
6584              
6585 6         23 bless \%object, $class;
6586             }
6587              
6588             sub addEntry
6589             {
6590 16     16   29 my $self = shift ;
6591 16         29 my $entry = shift ;
6592              
6593 16         39 my $localHeaderOffset = $entry->localHeaderOffset ;
6594 16         45 my $CentralDirectoryOffset = $entry->centralHeaderOffset ;
6595 16         37 my $filename = $entry->filename ;
6596              
6597 16         105 Nesting::add($entry);
6598              
6599             # Create a reference from Central to Local header entries
6600 16         117 my $ldEntry = Nesting::getLdEntryByOffset($localHeaderOffset);
6601 16 100       63 if ($ldEntry)
6602             {
6603 12         43 $entry->ldEntry($ldEntry) ;
6604              
6605             # LD -> CD
6606             # can have multiple LD entries point to same CD
6607             # so need to keep a list
6608 12         57 $ldEntry->addCdEntry($entry);
6609             }
6610              
6611             # only check for duplicate in real CD scan
6612 16 100 66     93 if ($self->{alreadyScanned} && ! $entry->encapsulated )
6613             {
6614 12         29 my $existing = $self->{byName}{$filename} ;
6615 12 50 66     100 if ($existing && $existing->centralHeaderOffset != $entry->centralHeaderOffset)
6616             {
6617             ::error $CentralDirectoryOffset,
6618             "Duplicate Central Directory entries for filename '$filename'",
6619             "Current Central Directory entry at offset " . ::decimalHex0x($CentralDirectoryOffset),
6620 0         0 "Duplicate Central Directory entry at offset " . ::decimalHex0x($self->{byName}{$filename}{centralHeaderOffset});
6621              
6622             # not strictly illegal to have duplicate filename, so save this one
6623             }
6624             else
6625             {
6626 12         51 my $existingNormalizedEntry = $self->normalize_filename($entry, $filename);
6627 12 50       40 if ($existingNormalizedEntry)
6628             {
6629 0         0 ::warning $CentralDirectoryOffset,
6630             "Portability Issue: Found case-insensitive duplicate for filename '$filename'",
6631             "Current Central Directory entry at offset " . ::decimalHex0x($CentralDirectoryOffset),
6632             "Duplicate Central Directory entry for filename '" . $existingNormalizedEntry->outputFilename . "' at offset " . ::decimalHex0x($existingNormalizedEntry->centralHeaderOffset);
6633             }
6634             }
6635             }
6636              
6637             # CD can get processed twice, so return if already processed
6638             return
6639 16 100       84 if $self->{byCentralOffset}{$CentralDirectoryOffset} ;
6640              
6641 12 50       29 if (! $entry->encapsulated )
6642             {
6643 12         17 push @{ $self->{entries} }, $entry;
  12         35  
6644              
6645 12         32 $self->{byLocalOffset}{$localHeaderOffset} = $entry;
6646 12         37 $self->{byCentralOffset}{$CentralDirectoryOffset} = $entry;
6647 12         34 $self->{byName}{ $filename } = $entry;
6648 12         47 $self->{offset2Index} = $self->{count} ++;
6649             }
6650              
6651             }
6652              
6653             sub exists
6654             {
6655 8     8   17 my $self = shift ;
6656              
6657 8         15 return scalar @{ $self->{entries} };
  8         73  
6658             }
6659              
6660             sub sortByLocalOffset
6661             {
6662 2     2   4 my $self = shift ;
6663              
6664 2         7 @{ $self->{entries} } = sort { $a->localHeaderOffset() <=> $b->localHeaderOffset() }
  2         7  
6665 2         4 @{ $self->{entries} };
  2         12  
6666             }
6667              
6668             sub getByLocalOffset
6669             {
6670 4     4   25 my $self = shift ;
6671 4         7 my $offset = shift ;
6672              
6673             # TODO - what happens if none exists?
6674 4         14 my $entry = $self->{byLocalOffset}{$offset - $PREFIX_DELTA} ;
6675 4         9 return $entry ;
6676             }
6677              
6678             sub localOffset
6679             {
6680 12     12   23 my $self = shift ;
6681 12         21 my $offset = shift ;
6682              
6683             # TODO - what happens if none exists?
6684 12         51 return $self->{byLocalOffset}{$offset - $PREFIX_DELTA} ;
6685             }
6686              
6687             sub getNextLocalOffset
6688             {
6689 0     0   0 my $self = shift ;
6690 0         0 my $offset = shift ;
6691              
6692 0         0 my $index = $self->{offset2Index} ;
6693              
6694 0 0       0 if ($index + 1 >= $self->{count})
6695             {
6696 0         0 return 0;
6697             }
6698              
6699 0         0 return ${ $self->{entries} }[$index+1]->localHeaderOffset() ;
  0         0  
6700             }
6701              
6702             sub inCD
6703             {
6704 0     0   0 my $self = shift ;
6705 0         0 $FH->tell() >= $self->{CentralDirectoryOffset};
6706             }
6707              
6708             sub setPkEncryptedCD
6709             {
6710 0     0   0 my $self = shift ;
6711              
6712 0         0 $self->{encryptedCD} = 1 ;
6713              
6714             }
6715              
6716             sub setMiniZipEncrypted
6717             {
6718 0     0   0 my $self = shift ;
6719              
6720 0         0 $self->{minizip_secure} = 1 ;
6721             }
6722              
6723             sub isMiniZipEncrypted
6724             {
6725 6     6   16 my $self = shift ;
6726 6         41 return $self->{minizip_secure};
6727             }
6728              
6729             sub isEncryptedCD
6730             {
6731 0     0   0 my $self = shift ;
6732 0   0     0 return $self->{encryptedCD} && ! $self->{minizip_secure};
6733             }
6734              
6735             sub normalize_filename
6736             {
6737             # check if there is a filename that already exists
6738             # with the same name when normalized to lower case
6739              
6740 12     12   18 my $self = shift ;
6741 12         23 my $entry = shift;
6742 12         20 my $filename = shift;
6743              
6744 12         29 my $nFilename = lc $filename;
6745              
6746 12         26 my $lookup = $self->{normalized_filenames}{$nFilename};
6747             # if ($lookup && $lookup ne $filename)
6748 12 50       33 if ($lookup)
6749             {
6750 0         0 return $lookup,
6751             }
6752              
6753 12         40 $self->{normalized_filenames}{$nFilename} = $entry;
6754              
6755 12         26 return undef;
6756             }
6757             }
6758              
6759             {
6760 0         0 package LocalDirectoryEntry;
  0         0  
6761              
6762 6     6   52 use parent -norequire , 'LocalCentralEntryBase' ;
  6         11  
  6         38  
6763              
6764 6     6   428 use constant Offset_VersionNeededToExtract => 4;
  6         12  
  6         419  
6765 6     6   36 use constant Offset_GeneralPurposeFlags => 6;
  6         11  
  6         477  
6766 6     6   37 use constant Offset_CompressionMethod => 8;
  6         10  
  6         366  
6767 6     6   35 use constant Offset_ModificationTime => 10;
  6         10  
  6         325  
6768 6     6   29 use constant Offset_ModificationDate => 12;
  6         11  
  6         334  
6769 6     6   33 use constant Offset_CRC32 => 14;
  6         23  
  6         291  
6770 6     6   30 use constant Offset_CompressedSize => 18;
  6         122  
  6         575  
6771 6     6   82 use constant Offset_UncompressedSize => 22;
  6         11  
  6         395  
6772 6     6   130 use constant Offset_FilenameLength => 26;
  6         13  
  6         275  
6773 6     6   29 use constant Offset_ExtraFieldLength => 27;
  6         8  
  6         399  
6774 6     6   105 use constant Offset_Filename => 30;
  6         13  
  6         26779  
6775              
6776             sub new
6777             {
6778 12     12   26 my $class = shift ;
6779              
6780 12         76 my $self = $class->SUPER::new();
6781              
6782 12         77 my %fields = (
6783             'streamedMatch' => 0,
6784             'readDataDescriptor' => 0,
6785             'cdEntryIndex' => {},
6786             'cdEntryList' => [],
6787             ) ;
6788              
6789 12         55 FieldsAndAccessors::Add($class, $self, \%fields) ;
6790              
6791 12         43 $self->inCentralDir(0) ;
6792 12         35 $self->entryType(::ZIP_LOCAL_HDR_SIG) ;
6793              
6794 12         33 return $self;
6795             }
6796              
6797             sub addCdEntry
6798             {
6799 16     16   29 my $self = shift ;
6800 16         27 my $entry = shift;
6801              
6802             # don't want encapsulated entries
6803             # and protect against duplicates
6804             return
6805             if $entry->encapsulated ||
6806 16 100 66     83 $self->{cdEntryIndex}{$entry->index} ++ >= 1;
6807              
6808 12         29 push @{ $self->{cdEntryList} }, $entry ;
  12         45  
6809             }
6810              
6811             sub getCdEntry
6812             {
6813 0     0   0 my $self = shift ;
6814              
6815             return []
6816 0 0       0 if ! $self->{cdEntryList} ;
6817              
6818 0         0 return $self->{cdEntryList}[0] ;
6819             }
6820              
6821             sub getCdEntries
6822             {
6823 24     24   34 my $self = shift ;
6824 24         90 return $self->{cdEntryList} ;
6825             }
6826             }
6827              
6828             {
6829 0         0 package LocalDirectory;
  0         0  
6830              
6831             sub new
6832             {
6833 6     6   13 my $class = shift ;
6834              
6835 6         47 my %object = (
6836             'entries' => [],
6837             'count' => 0,
6838             'byLocalOffset' => {},
6839             'byName' => {},
6840             'offset2Index' => {},
6841             'normalized_filenames' => {},
6842             'CentralDirectoryOffset' => 0,
6843             'CentralDirectorySize' => 0,
6844             'zip64' => 0,
6845             'encryptedCD' => 0,
6846             'streamedPresent' => 0,
6847             ) ;
6848              
6849 6         18 bless \%object, $class;
6850             }
6851              
6852             sub isLocalEntryNested
6853             {
6854 0     0   0 my $self = shift ;
6855 0         0 my $localEntry = shift;
6856              
6857 0         0 return Nesting::getFirstEncapsulation($localEntry);
6858              
6859             }
6860              
6861             sub addEntry
6862             {
6863 12     12   22 my $self = shift ;
6864 12         18 my $localEntry = shift ;
6865              
6866 12         35 my $filename = $localEntry->filename ;
6867 12         30 my $localHeaderOffset = $localEntry->localHeaderOffset;
6868 12         27 my $payloadOffset = $localEntry->payloadOffset ;
6869              
6870 12         65 my $existingEntry = $self->{byName}{$filename} ;
6871              
6872 12   50     31 my $endSurfaceArea = $payloadOffset + ($localEntry->compressedSize // 0) ;
6873              
6874 12 50       76 if ($existingEntry)
6875             {
6876 0         0 ::error $localHeaderOffset,
6877             "Duplicate Local Directory entry for filename '$filename'",
6878             "Current Local Directory entry at offset " . ::decimalHex0x($localHeaderOffset),
6879             "Duplicate Local Directory entry at offset " . ::decimalHex0x($existingEntry->localHeaderOffset),
6880             }
6881             else
6882             {
6883              
6884 12         45 my ($existing_filename, $offset) = $self->normalize_filename($filename);
6885 12 50       50 if ($existing_filename)
6886             {
6887 0         0 ::warning $localHeaderOffset,
6888             "Portability Issue: Found case-insensitive duplicate for filename '$filename'",
6889             "Current Local Directory entry at offset " . ::decimalHex0x($localHeaderOffset),
6890             "Duplicate Local Directory entry for filename '$existing_filename' at offset " . ::decimalHex0x($offset);
6891             }
6892             }
6893              
6894             # keep nested local entries for zipbomb deteection
6895 12         18 push @{ $self->{entries} }, $localEntry;
  12         36  
6896              
6897 12         35 $self->{byLocalOffset}{$localHeaderOffset} = $localEntry;
6898 12         52 $self->{byName}{ $filename } = $localEntry;
6899              
6900 12 50       37 $self->{streamedPresent} ++
6901             if $localEntry->streamed;
6902              
6903 12         35 Nesting::add($localEntry);
6904             }
6905              
6906             sub exists
6907             {
6908 6     6   11 my $self = shift ;
6909              
6910 6         13 return scalar @{ $self->{entries} };
  6         63  
6911             }
6912              
6913             sub sortByLocalOffset
6914             {
6915 6     6   13 my $self = shift ;
6916              
6917 6         23 @{ $self->{entries} } = sort { $a->localHeaderOffset() <=> $b->localHeaderOffset() }
  6         38  
6918 6         13 @{ $self->{entries} };
  6         71  
6919             }
6920              
6921             sub localOffset
6922             {
6923 0     0   0 my $self = shift ;
6924 0         0 my $offset = shift ;
6925              
6926 0         0 return $self->{byLocalOffset}{$offset} ;
6927             }
6928              
6929             sub getByLocalOffset
6930             {
6931 12     12   38 my $self = shift ;
6932 12         22 my $offset = shift ;
6933              
6934             # TODO - what happens if none exists?
6935 12         53 my $entry = $self->{byLocalOffset}{$offset} ;
6936 12         24 return $entry ;
6937             }
6938              
6939             sub getNextLocalOffset
6940             {
6941 0     0   0 my $self = shift ;
6942 0         0 my $offset = shift ;
6943              
6944 0         0 my $index = $self->{offset2Index} ;
6945              
6946 0 0       0 if ($index + 1 >= $self->{count})
6947             {
6948 0         0 return 0;
6949             }
6950              
6951 0         0 return ${ $self->{entries} }[$index+1]->localHeaderOffset ;
  0         0  
6952             }
6953              
6954             sub lastStreamedEntryAdded
6955             {
6956 0     0   0 my $self = shift ;
6957 0         0 my $offset = shift ;
6958              
6959 0         0 for my $entry ( reverse @{ $self->{entries} } )
  0         0  
6960             {
6961 0 0       0 if ($entry->streamed)# && ! $entry->streamedMatch)
6962             {
6963 0         0 $entry->streamedMatch($entry->streamedMatch + 1) ;
6964 0         0 return $entry;
6965             }
6966             }
6967              
6968 0         0 return undef;
6969             }
6970              
6971             sub inCD
6972             {
6973 0     0   0 my $self = shift ;
6974 0         0 $FH->tell() >= $self->{CentralDirectoryOffset};
6975             }
6976              
6977             sub setPkEncryptedCD
6978             {
6979 0     0   0 my $self = shift ;
6980              
6981 0         0 $self->{encryptedCD} = 1 ;
6982              
6983             }
6984              
6985             sub isEncryptedCD
6986             {
6987 0     0   0 my $self = shift ;
6988 0         0 return $self->{encryptedCD} ;
6989             }
6990              
6991             sub anyStreamedEntries
6992             {
6993 6     6   11 my $self = shift ;
6994 6         26 return $self->{streamedPresent} ;
6995             }
6996              
6997             sub normalize_filename
6998             {
6999             # check if there is a filename that already exists
7000             # with the same name when normalized to lower case
7001              
7002 12     12   37 my $self = shift ;
7003 12         21 my $filename = shift;
7004              
7005 12         27 my $nFilename = lc $filename;
7006              
7007 12         27 my $lookup = $self->{normalized_filenames}{$nFilename};
7008 12 50 33     59 if ($lookup && $lookup ne $filename)
7009             {
7010             return $self->{byName}{$lookup}{outputFilename},
7011             $self->{byName}{$lookup}{localHeaderOffset}
7012 0         0 }
7013              
7014 12         48 $self->{normalized_filenames}{$nFilename} = $filename;
7015              
7016 12         46 return undef, undef;
7017             }
7018             }
7019              
7020             {
7021 0         0 package Eocd ;
  0         0  
7022              
7023             sub new
7024             {
7025 0     0   0 my $class = shift ;
7026              
7027 0         0 my %object = (
7028             'zip64' => 0,
7029             ) ;
7030              
7031 0         0 bless \%object, $class;
7032             }
7033             }
7034              
7035             sub displayFileInfo
7036 0         0 {
7037 6     6   11 return;
7038              
7039 0         0 my $filename = shift;
7040              
7041 0         0 info undef,
7042             "Filename : '$filename'",
7043             "Size : " . (-s $filename) . " (" . decimalHex0x(-s $filename) . ")",
7044             # "Native Encoding: '" . TextEncoding::getNativeLocaleName() . "'",
7045             }
7046              
7047             {
7048 0         0 package TextEncoding;
7049              
7050 0         0 my $nativeLocaleEncoding = getNativeLocale();
7051 0         0 my $opt_EncodingFrom = $nativeLocaleEncoding;
7052 0         0 my $opt_EncodingTo = $nativeLocaleEncoding ;
7053 0         0 my $opt_Encoding_Enabled;
7054             my $opt_Debug_Encoding;
7055 0         0 my $opt_use_LanguageEncodingFlag;
7056              
7057             sub setDefaults
7058             {
7059 6     6   21 $nativeLocaleEncoding = getNativeLocale();
7060 6         15 $opt_EncodingFrom = $nativeLocaleEncoding;
7061 6         13 $opt_EncodingTo = $nativeLocaleEncoding ;
7062 6         15 $opt_Encoding_Enabled = 1;
7063 6         15 $opt_Debug_Encoding = 0;
7064 6         13 $opt_use_LanguageEncodingFlag = 1;
7065             }
7066              
7067             sub getNativeLocale
7068             {
7069 6     6   11 state $enc;
7070              
7071 6 50       25 if (! defined $enc)
7072             {
7073             eval
7074 6         15 {
7075 6         3728 require encoding ;
7076 6         22209 my $encoding = encoding::_get_locale_encoding() ;
7077 6 50       15844 if (! $encoding)
7078             {
7079             # CP437 is the legacy default for zip files
7080 0         0 $encoding = 'cp437';
7081             # ::warning undef, "Cannot determine system charset: defaulting to '$encoding'"
7082             }
7083 6         24 $enc = Encode::find_encoding($encoding) ;
7084             } ;
7085             }
7086              
7087 6         150 return $enc;
7088             }
7089              
7090             sub getNativeLocaleName
7091             {
7092 0     0   0 state $name;
7093              
7094 0 0       0 return $name
7095             if defined $name ;
7096              
7097 0 0       0 if (! defined $name)
7098             {
7099 0         0 my $enc = getNativeLocale();
7100 0 0       0 if ($enc)
7101             {
7102 0         0 $name = $enc->name()
7103             }
7104             else
7105             {
7106 0         0 $name = 'unknown'
7107             }
7108             }
7109              
7110 0         0 return $name ;
7111             }
7112              
7113             sub parseEncodingOption
7114             {
7115 12     12   13470 my $opt_name = shift;
7116 12         26 my $opt_value = shift;
7117              
7118 12         61 my $enc = Encode::find_encoding($opt_value) ;
7119 12 50       223 die "Encoding '$opt_value' not found for option '$opt_name'\n"
7120             unless ref $enc;
7121              
7122 12 100       211 if ($opt_name eq 'encoding')
    50          
7123             {
7124 6         104 $opt_EncodingFrom = $enc;
7125             }
7126             elsif ($opt_name eq 'output-encoding')
7127             {
7128 6         94 $opt_EncodingTo = $enc;
7129             }
7130             else
7131             {
7132 0         0 die "Unknown option $opt_name\n"
7133             }
7134             }
7135              
7136             sub NoEncoding
7137             {
7138 0     0   0 my $opt_name = shift;
7139 0         0 my $opt_value = shift;
7140              
7141 0         0 $opt_Encoding_Enabled = 0 ;
7142             }
7143              
7144             sub LanguageEncodingFlag
7145             {
7146 0     0   0 my $opt_name = shift;
7147 0         0 my $opt_value = shift;
7148              
7149 0         0 $opt_use_LanguageEncodingFlag = $opt_value ;
7150             }
7151              
7152             sub debugEncoding
7153             {
7154 24 50   24   64 if (@_)
7155             {
7156 0         0 $opt_Debug_Encoding = 1 ;
7157             }
7158              
7159 24         112 return $opt_Debug_Encoding ;
7160             }
7161              
7162             sub encodingInfo
7163             {
7164             return
7165 6 50 33 6   44 unless $opt_Encoding_Enabled && $opt_Debug_Encoding ;
7166              
7167 0         0 my $enc = TextEncoding::getNativeLocaleName();
7168 0         0 my $from = $opt_EncodingFrom->name();
7169 0         0 my $to = $opt_EncodingTo->name();
7170              
7171 0         0 ::debug undef, "Debug Encoding Enabled",
7172             "System Default Encoding: '$enc'",
7173             "Encoding used when reading from zip file: '$from'",
7174             "Encoding used for display output: '$to'";
7175             }
7176              
7177             sub cleanEval
7178             {
7179 0     0   0 chomp $_[0] ;
7180 0         0 $_[0] =~ s/ at .+ line \d+\.$// ;
7181 0         0 return $_[0];
7182             }
7183              
7184             sub decode
7185             {
7186 24     24   42 my $name = shift ;
7187 24         36 my $type = shift ;
7188 24         35 my $LanguageEncodingFlag = shift ;
7189              
7190 24 50       59 return $name
7191             if ! $opt_Encoding_Enabled ;
7192              
7193             # TODO - check for badly formed content
7194 24 50 33     88 if ($LanguageEncodingFlag && $opt_use_LanguageEncodingFlag)
7195             {
7196             # use "utf-8-strict" to catch invalid codepoints
7197 0         0 eval { $name = Encode::decode('utf-8-strict', $name, Encode::FB_CROAK ) } ;
  0         0  
7198 0 0       0 ::warning $FH->tell() - length $name, "Could not decode 'UTF-8' $type: " . cleanEval $@
7199             if $@ ;
7200             }
7201             else
7202             {
7203 24         48 eval { $name = $opt_EncodingFrom->decode($name, Encode::FB_CROAK ) } ;
  24         259  
7204 24 50       278 ::warning $FH->tell() - length $name, "Could not decode '" . $opt_EncodingFrom->name() . "' $type: " . cleanEval $@
7205             if $@;
7206             }
7207              
7208             # remove any BOM
7209 24         65 $name =~ s/^\x{FEFF}//;
7210              
7211 24         56 return $name ;
7212             }
7213              
7214             sub encode
7215             {
7216 24     24   39 my $name = shift ;
7217 24         55 my $type = shift ;
7218 24         38 my $LanguageEncodingFlag = shift ;
7219              
7220 24 50       60 return $name
7221             if ! $opt_Encoding_Enabled;
7222              
7223 24 50 33     72 if ($LanguageEncodingFlag && $opt_use_LanguageEncodingFlag)
7224             {
7225 0         0 eval { $name = Encode::encode('utf8', $name, Encode::FB_CROAK ) } ;
  0         0  
7226 0 0       0 ::warning $FH->tell() - length $name, "Could not encode 'utf8' $type: " . cleanEval $@
7227             if $@ ;
7228             }
7229             else
7230             {
7231 24         37 eval { $name = $opt_EncodingTo->encode($name, Encode::FB_CROAK ) } ;
  24         127  
7232 24 50       56 ::warning $FH->tell() - length $name, "Could not encode '" . $opt_EncodingTo->name() . "' $type: " . cleanEval $@
7233             if $@;
7234             }
7235              
7236 24         51 return $name;
7237             }
7238             }
7239              
7240             {
7241 0         0 package Nesting;
  0         0  
7242              
7243 6     6   69 use Data::Dumper;
  6         30  
  6         11707  
7244              
7245 0         0 my @nestingStack = ();
7246 0         0 my %encapsulations;
7247             my %inner2outer;
7248 0         0 my $encapsulationCount = 0;
7249 0         0 my %index2entry ;
7250 0         0 my %offset2entry ;
7251              
7252             # my %localOffset2cdEntry;
7253              
7254             sub clearStack
7255             {
7256 2     2   5 @nestingStack = ();
7257 2         6 %encapsulations = ();
7258 2         15 %inner2outer = ();
7259 2         6 %index2entry = ();
7260 2         5 %offset2entry = ();
7261 2         6 $encapsulationCount = 0;
7262             }
7263              
7264             sub dump
7265             {
7266 0   0 0   0 my $indent = shift // 0;
7267              
7268 0         0 for my $offset (sort {$a <=> $b} keys %offset2entry)
  0         0  
7269             {
7270 0         0 my $leading = " " x $indent ;
7271 0         0 say $leading . "\nOffset $offset" ;
7272 0         0 say Dumper($offset2entry{$offset})
7273             }
7274             }
7275              
7276             sub add
7277             {
7278 28     28   48 my $entry = shift;
7279              
7280 28         77 getEnclosingEntry($entry);
7281 28         47 push @nestingStack, $entry;
7282 28         73 $index2entry{ $entry->index } = $entry;
7283 28         82 $offset2entry{ $entry->offsetStart } = $entry;
7284             }
7285              
7286             sub getEnclosingEntry
7287             {
7288 28     28   48 my $entry = shift;
7289              
7290 28         59 my $filename = $entry->filename;
7291              
7292             pop @nestingStack
7293 28   66     152 while @nestingStack && $entry->offsetStart > $nestingStack[-1]->offsetEnd ;
7294              
7295 28         62 my $match = undef;
7296              
7297 28 0 33     90 if (@nestingStack &&
      33        
      0        
7298             $entry->offsetStart >= $nestingStack[-1]->offsetStart &&
7299             $entry->offsetEnd <= $nestingStack[-1]->offsetEnd &&
7300             $entry->index != $nestingStack[-1]->index)
7301             {
7302             # Nested entry found
7303 0         0 $match = $nestingStack[-1];
7304 0         0 push @{ $encapsulations{ $match->index } }, $entry;
  0         0  
7305 0         0 $inner2outer{ $entry->index} = $match->index;
7306 0         0 ++ $encapsulationCount;
7307              
7308 0         0 $entry->encapsulated(1) ;
7309 0         0 $match->increment_childrenCount();
7310              
7311 0 0       0 if ($NESTING_DEBUG)
7312             {
7313 0         0 say "#### nesting " . (caller(1))[3] . " index #" . $entry->index . ' "' .
7314             $entry->outputFilename . '" [' . $entry->offsetStart . "->" . $entry->offsetEnd . "]" .
7315             " in #" . $match->index . ' "' .
7316             $match->outputFilename . '" [' . $match->offsetStart . "->" . $match->offsetEnd . "]" ;
7317             }
7318             }
7319              
7320 28         54 return $match;
7321             }
7322              
7323             sub isNested
7324             {
7325 6     6   90 my $offsetStart = shift;
7326 6         16 my $offsetEnd = shift;
7327              
7328 6 50       20 if ($NESTING_DEBUG)
7329             {
7330 0         0 say "### Want: offsetStart " . ::decimalHex0x($offsetStart) . " offsetEnd " . ::decimalHex0x($offsetEnd);
7331 0         0 for my $entry (@nestingStack)
7332             {
7333 0         0 say "### Have: offsetStart " . ::decimalHex0x($entry->offsetStart) . " offsetEnd " . ::decimalHex0x($entry->offsetEnd);
7334             }
7335             }
7336              
7337 6 50       22 return 0
7338             unless @nestingStack ;
7339              
7340 6         183 my @copy = @nestingStack ;
7341              
7342             pop @copy
7343 6   66     146 while @copy && $offsetStart > $copy[-1]->offsetEnd ;
7344              
7345 6   33     55 return @copy &&
7346             $offsetStart >= $copy[-1]->offsetStart &&
7347             $offsetEnd <= $copy[-1]->offsetEnd ;
7348             }
7349              
7350             sub getOuterEncapsulation
7351             {
7352 0     0   0 my $entry = shift;
7353              
7354 0         0 my $outerIndex = $inner2outer{ $entry->index } ;
7355              
7356             return undef
7357 0 0       0 if ! defined $outerIndex ;
7358              
7359 0   0     0 return $index2entry{$outerIndex} // undef;
7360             }
7361              
7362             sub getEncapsulations
7363             {
7364 0     0   0 my $entry = shift;
7365              
7366 0         0 return $encapsulations{ $entry->index } ;
7367             }
7368              
7369             sub getFirstEncapsulation
7370             {
7371 0     0   0 my $entry = shift;
7372              
7373 0         0 my $got = $encapsulations{ $entry->index } ;
7374              
7375 0 0       0 return defined $got ? $$got[0] : undef;
7376             }
7377              
7378             sub encapsulations
7379             {
7380 0     0   0 return \%encapsulations;
7381             }
7382              
7383             sub encapsulationCount
7384             {
7385 12     12   48 return $encapsulationCount;
7386             }
7387              
7388             sub childrenInCentralDir
7389             {
7390             # find local header entries that have children that are not referenced in the CD
7391             # tis means it is likely a benign nextd zip file
7392 0     0   0 my $entry = shift;
7393              
7394 0         0 for my $child (@{ $encapsulations{$entry->index} } )
  0         0  
7395             {
7396             next
7397 0 0       0 unless $child->entryType == ::ZIP_LOCAL_HDR_SIG ;
7398              
7399             return 1
7400 0 0       0 if @{ $child->cdEntryList };
  0         0  
7401             }
7402              
7403 0         0 return 0;
7404             }
7405              
7406             sub entryByIndex
7407             {
7408 0     0   0 my $index = shift;
7409 0         0 return $index2entry{$index};
7410             }
7411              
7412             sub getEntryByOffset
7413             {
7414 0     0   0 my $offset = shift;
7415 0         0 return $offset2entry{$offset};
7416             }
7417              
7418             sub getLdEntryByOffset
7419             {
7420 16     16   35 my $offset = shift;
7421 16         69 my $entry = $offset2entry{$offset};
7422              
7423 16 100 66     101 return $entry
7424             if $entry && $entry->entryType == ::ZIP_LOCAL_HDR_SIG;
7425              
7426 4         10 return undef;
7427             }
7428              
7429             sub getEntriesByOffset
7430             {
7431 18     18   139 return \%offset2entry ;
7432             }
7433             }
7434              
7435             {
7436 0         0 package SimpleTable ;
  0         0  
7437              
7438 6     6   78 use List::Util qw(max sum);
  6         12  
  6         24432  
7439              
7440             sub new
7441             {
7442 12     12   26 my $class = shift;
7443              
7444 12         70 my %object = (
7445             header => [],
7446             data => [],
7447             columns => 0,
7448             prefix => '# ',
7449             );
7450 12         42 bless \%object, $class;
7451             }
7452              
7453             sub addHeaderRow
7454             {
7455 12     12   71 my $self = shift;
7456 12         21 push @{ $self->{header} }, [ @_ ] ;
  12         90  
7457 12         69 $self->{columns} = max($self->{columns}, scalar @_ ) ;
7458             }
7459              
7460             sub addDataRow
7461             {
7462 0     0   0 my $self = shift;
7463              
7464 0         0 push @{ $self->{data} }, [ @_ ] ;
  0         0  
7465 0         0 $self->{columns} = max($self->{columns}, scalar @_ ) ;
7466             }
7467              
7468             sub hasData
7469             {
7470 12     12   36 my $self = shift;
7471              
7472 12         41 return scalar @{ $self->{data} } ;
  12         31  
7473             }
7474              
7475             sub display
7476             {
7477 0     0     my $self = shift;
7478              
7479             # work out the column widths
7480 0           my @colW = (0) x $self->{columns} ;
7481 0           for my $row (@{ $self->{data} }, @{ $self->{header} })
  0            
  0            
7482             {
7483 0           my @r = @$row;
7484 0           for my $ix (0 .. $self->{columns} -1)
7485             {
7486 0           $colW[$ix] = max($colW[$ix],
7487             3 + length( $r[$ix] )
7488             );
7489             }
7490             }
7491              
7492 0           my $width = sum(@colW) ; #+ @colW ;
7493 0           my @template ;
7494 0           for my $w (@colW)
7495             {
7496 0           push @template, ' ' x ($w - 3);
7497             }
7498              
7499 0           print $self->{prefix} . '-' x ($width + 1) . "\n";
7500              
7501 0           for my $row (@{ $self->{header} })
  0            
7502             {
7503 0           my @outputRow = @template;
7504              
7505 0           print $self->{prefix} . '| ';
7506 0           for my $ix (0 .. $self->{columns} -1)
7507             {
7508 0           my $field = $template[$ix] ;
7509 0           substr($field, 0, length($row->[$ix]), $row->[$ix]);
7510 0           print $field . ' | ';
7511             }
7512 0           print "\n";
7513              
7514             }
7515              
7516 0           print $self->{prefix} . '-' x ($width + 1) . "\n";
7517              
7518 0           for my $row (@{ $self->{data} })
  0            
7519             {
7520 0           my @outputRow = @template;
7521              
7522 0           print $self->{prefix} . '| ';
7523 0           for my $ix (0 .. $self->{columns} -1)
7524             {
7525 0           my $field = $template[$ix] ;
7526 0           substr($field, 0, length($row->[$ix]), $row->[$ix]);
7527 0           print $field . ' | ';
7528             }
7529 0           print "\n";
7530             }
7531              
7532 0           print $self->{prefix} . '-' x ($width + 1) . "\n";
7533 0           print "#\n";
7534             }
7535             }
7536              
7537             sub Usage
7538 0         0 {
7539 0     0     my $enc = TextEncoding::getNativeLocaleName();
7540              
7541 0           my $message = <
7542             zipdetails [OPTIONS] file
7543              
7544             Display details about the internal structure of a Zip file.
7545              
7546             OPTIONS
7547              
7548             General Options
7549             -h, --help
7550             Display help
7551             --redact
7552             Hide filename and payload data in the output
7553             --scan
7554             Enable pessimistic scanning mode.
7555             Blindly scan the file looking for zip headers
7556             Expect false-positives.
7557             --utc
7558             Display date/time fields in UTC. Default is local time
7559             -v
7560             Enable verbose mode -- output more stuff
7561             --version
7562             Print zipdetails version number
7563             This is version $VERSION
7564             --walk
7565             Enable optimistic scanning mode.
7566             Blindly scan the file looking for zip headers
7567             Expect false-positives.
7568              
7569             Filename/Comment Encoding
7570             --encoding e
7571             Use encoding "e" when reading filename/comments from the zip file
7572             Uses system encoding ('$enc') by default
7573             --no-encoding
7574             Disable filename & comment encoding. Default disabled.
7575             --output-encoding e
7576             Use encoding "e" when writing filename/comments to the display
7577             Uses system encoding ('$enc') by default
7578             --debug-encoding
7579             Display eatra info when a filename/comment encoding has changed
7580             --language-encoding, --no-language-encoding
7581             Enable/disable support for the zip file "Language Encoding" flag.
7582             When this flag is set in a zip file the filename/comment is assumed
7583             to be encoded in UTF8.
7584             Default is enabled
7585              
7586             Message Control
7587             --messages, --no-messages
7588             Enable/disable all info/warning/error messages. Default enabled.
7589             --exit-bitmask, --no-exit-bitmask
7590             Enable/disable exit status bitmask for messages. Default disabled.
7591             Bitmask values are
7592             Info 1
7593             Warning 2
7594             Error 4
7595              
7596             Copyright (c) 2011-2026 Paul Marquess. All rights reserved.
7597              
7598             This program is free software; you can redistribute it and/or
7599             modify it under the same terms as Perl itself.
7600             EOM
7601              
7602 0 0         if (@_)
7603             {
7604             warn "$_\n"
7605 0           for @_ ;
7606 0           warn "\n";
7607              
7608 0           die $message ;
7609             }
7610              
7611 0           print $message ;
7612 0           exit 0;
7613              
7614             }
7615              
7616             1;
7617              
7618             __END__