diff --git a/Changes b/Changes index 3931324..b9e0774 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,18 @@ CHANGES ------- + 2.207 18 February 2024 + + * Add bin/zipdetails 4.000 + + * Merge pull request #51 from rwp0/rwp0/fix-indirect-syntax + Thu Nov 23 12:17:30 2023 +0000 + f1c70d0ef8ed7a91daae6a3ea6b10dc486db4463 + + * Fix indirect syntax + Mon Nov 6 13:23:07 2023 +0100 + 7bd8408ef428024985e39f1275875a5f0812fbbf + 2.206 25 July 2023 * 2.206 diff --git a/META.json b/META.json index 893f3c2..d0f1759 100644 --- a/META.json +++ b/META.json @@ -34,8 +34,8 @@ }, "runtime" : { "requires" : { - "Compress::Raw::Bzip2" : "2.206", - "Compress::Raw::Zlib" : "2.206", + "Compress::Raw::Bzip2" : "2.207", + "Compress::Raw::Zlib" : "2.207", "Scalar::Util" : "0", "Encode" : "0", "Time::Local" : "0" @@ -54,6 +54,6 @@ "web" : "https://github.com/pmqs/IO-Compress" } }, - "version" : "2.206", + "version" : "2.207", "x_serialization_backend" : "JSON::PP version 2.97001" } diff --git a/META.yml b/META.yml index e893573..f1e89c4 100644 --- a/META.yml +++ b/META.yml @@ -20,8 +20,8 @@ no_index: - t - private requires: - Compress::Raw::Bzip2: '2.206' - Compress::Raw::Zlib: '2.206' + Compress::Raw::Bzip2: '2.207' + Compress::Raw::Zlib: '2.207' Scalar::Util: '0' Encode: '0' Time::Local: '0' @@ -29,5 +29,5 @@ resources: bugtracker: https://github.com/pmqs/IO-Compress/issues homepage: https://github.com/pmqs/IO-Compress repository: git://github.com/pmqs/IO-Compress.git -version: '2.206' +version: '2.207' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' diff --git a/Makefile.PL b/Makefile.PL index 86154c2..cff08dd 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -3,8 +3,8 @@ use strict ; require 5.006 ; -$::VERSION = '2.206' ; -$::DEP_VERSION = '2.206'; +$::VERSION = '2.207' ; +$::DEP_VERSION = '2.207'; use lib '.'; use private::MakeUtil; diff --git a/README b/README index 9188150..1ad1d6e 100644 --- a/README +++ b/README @@ -1,11 +1,11 @@ IO-Compress - Version 2.206 + Version 2.207 - 25 July 2023 + 18 February 2024 - Copyright (c) 1995-2023 Paul Marquess. All rights reserved. + Copyright (c) 1995-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. @@ -111,7 +111,7 @@ To help me help you, I need all of the following information: If you haven't installed IO-Compress then search IO::Compress::Gzip.pm for a line like this: - $VERSION = "2.206" ; + $VERSION = "2.207" ; 2. If you are having problems building IO-Compress, send me a complete log of what happened. Start by unpacking the IO-Compress diff --git a/bin/streamzip b/bin/streamzip index 199599e..98f0372 100755 --- a/bin/streamzip +++ b/bin/streamzip @@ -136,7 +136,7 @@ OPTIONS zstd Use LZMA compression [needs IO::Compress::Zstd] -version Display version number [$VERSION] -Copyright (c) 2019-2022 Paul Marquess. All rights reserved. +Copyright (c) 2019-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. @@ -292,7 +292,7 @@ Paul Marquess F. =head1 COPYRIGHT -Copyright (c) 2019-2022 Paul Marquess. All rights reserved. +Copyright (c) 2019-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/bin/zipdetails b/bin/zipdetails index 3f89b83..6b0aecd 100755 --- a/bin/zipdetails +++ b/bin/zipdetails @@ -7,6 +7,8 @@ use 5.010; # for unpack "Q<" +my $NESTING_DEBUG = 0 ; + BEGIN { # Check for a 32-bit Perl if (!eval { pack "Q", 1 }) { @@ -20,15 +22,30 @@ BEGIN { pop @INC if $INC[-1] eq '.' } use strict; use warnings ; no warnings 'portable'; # for unpacking > 2^32 -use feature 'state'; +use feature qw(state say); use IO::File; use Encode; use Getopt::Long; - -my $VERSION = "2.104" ; - +use List::Util qw(min max); + +my $VERSION = '4.000' ; + +sub fatal_tryWalk; +sub fatal_truncated ; +sub info ; +sub warning ; +sub error ; +sub debug ; +sub fatal ; +sub topLevelFatal ; +sub internalFatal; +sub need ; +sub decimalHex; + +use constant MAX64 => 0xFFFFFFFFFFFFFFFF ; use constant MAX32 => 0xFFFFFFFF ; +use constant MAX16 => 0xFFFF ; # Compression types use constant ZIP_CM_STORE => 0 ; @@ -45,6 +62,11 @@ use constant ZIP_GP_FLAG_PATCHED_MASK => (1 << 5) ; use constant ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK => (1 << 6) ; use constant ZIP_GP_FLAG_LZMA_EOS_PRESENT => (1 << 1) ; use constant ZIP_GP_FLAG_LANGUAGE_ENCODING => (1 << 11) ; +use constant ZIP_GP_FLAG_PKWARE_ENHANCED_COMP => (1 << 12) ; +use constant ZIP_GP_FLAG_ENCRYPTED_CD => (1 << 13) ; + +# All the encryption flags +use constant ZIP_GP_FLAG_ALL_ENCRYPT => (ZIP_GP_FLAG_ENCRYPTED_MASK | ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK | ZIP_GP_FLAG_ENCRYPTED_CD ); # Internal File Attributes use constant ZIP_IFA_TEXT_MASK => 1; @@ -56,10 +78,9 @@ use constant ZIP_CENTRAL_HDR_SIG => 0x02014b50; use constant ZIP_END_CENTRAL_HDR_SIG => 0x06054b50; use constant ZIP64_END_CENTRAL_REC_HDR_SIG => 0x06064b50; use constant ZIP64_END_CENTRAL_LOC_HDR_SIG => 0x07064b50; -use constant ZIP_ARCHIVE_EXTRA_DATA_SIG => 0x08064b50; use constant ZIP_DIGITAL_SIGNATURE_SIG => 0x05054b50; - use constant ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG => 0x08064b50; +use constant ZIP_SINGLE_SEGMENT_MARKER => 0x30304b50; # APPNOTE 6.3.10, sec 8.5.4 # Extra sizes use constant ZIP_EXTRA_HEADER_SIZE => 2 ; @@ -71,6 +92,12 @@ use constant ZIP_EXTRA_SUBFIELD_HEADER_SIZE => ZIP_EXTRA_SUBFIELD_ID_SIZE + use constant ZIP_EXTRA_SUBFIELD_MAX_SIZE => ZIP_EXTRA_MAX_SIZE - ZIP_EXTRA_SUBFIELD_HEADER_SIZE; +use constant ZIP_EOCD_MIN_SIZE => 22 ; + + +use constant ZIP_LD_FILENAME_OFFSET => 30; +use constant ZIP_CD_FILENAME_OFFSET => 46; + my %ZIP_CompressionMethods = ( 0 => 'Stored', @@ -82,7 +109,7 @@ my %ZIP_CompressionMethods = 6 => 'Imploded', 7 => 'Reserved for Tokenizing compression algorithm', 8 => 'Deflated', - 9 => 'Enhanced Deflating using Deflate64(tm)', + 9 => 'Deflate64', 10 => 'PKWARE Data Compression Library Imploding', 11 => 'Reserved by PKWARE', 12 => 'BZIP2', @@ -91,15 +118,17 @@ my %ZIP_CompressionMethods = 15 => 'Reserved by PKWARE', 16 => 'IBM z/OS CMPSC Compression', 17 => 'Reserved by PKWARE', - 18 => 'File is compressed using IBM TERSE (new)', + 18 => 'IBM/TERSE or Xceed BWT', # APPNOTE has IBM/TERSE. Xceed reuses it unofficially 19 => 'IBM LZ77 z Architecture (PFS)', + 20 => 'Ipaq8', # see https://encode.su/threads/1048-info-zip-lpaq8 + 92 => 'Reference', # Winzip Only from version 25 93 => 'Zstandard', 94 => 'MP3', 95 => 'XZ', 96 => 'WinZip JPEG Compression', 97 => 'WavPack compressed data', 98 => 'PPMd version I, Rev 1', - 99 => 'AES Encryption', + 99 => 'AES Encryption', # Apple also use this code for LZFSE compression in IPA files ); my %OS_Lookup = ( @@ -126,95 +155,264 @@ my %OS_Lookup = ( 30 => "AtheOS/Syllable", ); +{ + package Signatures ; + + my %Lookup = ( + # Map unpacked signature to + # decoder + # name + # central flag + + # Core Signatures + ::ZIP_LOCAL_HDR_SIG, [ \&::LocalHeader, "Local File Header", 0 ], + ::ZIP_DATA_HDR_SIG, [ \&::DataDescriptor, "Data Descriptor", 0 ], + ::ZIP_CENTRAL_HDR_SIG, [ \&::CentralHeader, "Central Directory Header", 1 ], + ::ZIP_END_CENTRAL_HDR_SIG, [ \&::EndCentralHeader, "End Central Directory Record", 1 ], + ::ZIP_SINGLE_SEGMENT_MARKER, [ \&::SingleSegmentMarker, "Split Archive Single Segment Marker", 0], + + # Zip64 + ::ZIP64_END_CENTRAL_REC_HDR_SIG, [ \&::Zip64EndCentralHeader, "Zip64 End of Central Directory Record", 1 ], + ::ZIP64_END_CENTRAL_LOC_HDR_SIG, [ \&::Zip64EndCentralLocator, "Zip64 End of Central Directory Locator", 1 ], + + # Digital signature (pkzip) + ::ZIP_DIGITAL_SIGNATURE_SIG, [ \&::DigitalSignature, "Digital Signature", 1 ], + + # Archive Encryption Headers (pkzip) - never seen this one + ::ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG, [ \&::ArchiveExtraDataRecord, "Archive Extra Record", 1 ], + ); + + sub decoder + { + my $signature = shift ; + + return undef + unless exists $Lookup{$signature}; + + return $Lookup{$signature}[0]; + } + + sub name + { + my $signature = shift ; + + return 'UNKNOWN' + unless exists $Lookup{$signature}; + + return $Lookup{$signature}[1]; + } + + sub titleName + { + my $signature = shift ; + + uc name($signature); + } + + sub hexValue + { + my $signature = shift ; + sprintf "0x%X", $signature ; + } + + sub hexValue32 + { + my $signature = shift ; + sprintf "0x%08X", $signature ; + } + + sub hexValue16 + { + my $signature = shift ; + sprintf "0x%04X", $signature ; + } + + sub nameAndHex + { + my $signature = shift ; + + return "'" . name($signature) . "' (" . hexValue32($signature) . ")" + } + + sub isCentralHeader + { + my $signature = shift ; -my %Lookup = ( - ZIP_LOCAL_HDR_SIG, \&LocalHeader, - ZIP_DATA_HDR_SIG, \&DataHeader, - ZIP_CENTRAL_HDR_SIG, \&CentralHeader, - ZIP_END_CENTRAL_HDR_SIG, \&EndCentralHeader, - ZIP64_END_CENTRAL_REC_HDR_SIG, \&Zip64EndCentralHeader, - ZIP64_END_CENTRAL_LOC_HDR_SIG, \&Zip64EndCentralLocator, + return undef + unless exists $Lookup{$signature}; + + return $Lookup{$signature}[2]; + } + #sub isValidSignature + #{ + # my $signature = shift ; + # return exists $Lookup{$signature}} + #} + + sub getSigsForScan + { + my %sigs = + # map { $_ => 1 } + # map { substr($_->[0], 2, 2) => $_->[1] } # don't want the initial "PK" + map { substr(pack("V", $_), 2, 2) => $_ } + keys %Lookup ; + + return %sigs; + } - # TODO - Archive Encryption Headers & digital signature - #ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG - #ZIP_DIGITAL_SIGNATURE_SIG - #ZIP_ARCHIVE_EXTRA_DATA_SIG -); +} my %Extras = ( - 0x0001, ['ZIP64', \&decode_Zip64], - 0x0007, ['AV Info', undef], - 0x0008, ['Extended Language Encoding', undef], - 0x0009, ['OS/2 extended attributes', undef], - 0x000a, ['NTFS FileTimes', \&decode_NTFS_Filetimes], - 0x000c, ['OpenVMS', undef], - 0x000d, ['Unix', undef], - 0x000e, ['Stream & Fork Descriptors', undef], - 0x000f, ['Patch Descriptor', undef], - 0x0014, ['PKCS#7 Store for X.509 Certificates', undef], - 0x0015, ['X.509 Certificate ID and Signature for individual file', undef], - 0x0016, ['X.509 Certificate ID for Central Directory', undef], - 0x0017, ['Strong Encryption Header', undef], - 0x0018, ['Record Management Controls', undef], - 0x0019, ['PKCS#7 Encryption Recipient Certificate List', undef], - 0x0020, ['Reserved for Timestamp record', undef], - 0x0021, ['Policy Decryption Key Record', undef], - 0x0022, ['Smartcrypt Key Provider Record', undef], - 0x0023, ['Smartcrypt Policy Key Data Record', undef], - # The Header ID mappings defined by Info-ZIP and third parties are: + # Local Central + # ID Name Handler min size max size min size max size + 0x0001, ['ZIP64', \&decode_Zip64, 0, 28, 0, 28], + 0x0007, ['AV Info', undef], # TODO + 0x0008, ['Extended Language Encoding', undef], # TODO + 0x0009, ['OS/2 extended attributes', undef], # TODO + 0x000a, ['NTFS FileTimes', \&decode_NTFS_Filetimes, 32, 32, 32, 32], + 0x000c, ['OpenVMS', \&decode_OpenVMS, 4, undef, 4, undef], + 0x000d, ['Unix', undef], + 0x000e, ['Stream & Fork Descriptors', undef], # TODO + 0x000f, ['Patch Descriptor', undef], + 0x0014, ['PKCS#7 Store for X.509 Certificates', undef], + 0x0015, ['X.509 Certificate ID and Signature for individual file', undef], + 0x0016, ['X.509 Certificate ID for Central Directory', undef], + 0x0017, ['Strong Encryption Header', \&decode_strong_encryption, 12, undef, 12, undef], + 0x0018, ['Record Management Controls', undef], + 0x0019, ['PKCS#7 Encryption Recipient Certificate List', undef], + 0x0020, ['Reserved for Timestamp record', undef], + 0x0021, ['Policy Decryption Key Record', undef], + 0x0022, ['Smartcrypt Key Provider Record', undef], + 0x0023, ['Smartcrypt Policy Key Data Record', undef], - 0x0065, ['IBM S/390 attributes - uncompressed', \&decodeMVS], - 0x0066, ['IBM S/390 attributes - compressed', undef], - 0x07c8, ['Info-ZIP Macintosh (old, J. Lee)', undef], - 0x2605, ['ZipIt Macintosh (first version)', undef], - 0x2705, ['ZipIt Macintosh v 1.3.5 and newer (w/o full filename)', undef], - 0x2805, ['ZipIt Macintosh v 1.3.5 and newer', undef], - 0x334d, ["Info-ZIP Macintosh (new, D. Haase's 'Mac3' field)", undef], - 0x4154, ['Tandem NSK', undef], - 0x4341, ['Acorn/SparkFS (David Pilling)', undef], - 0x4453, ['Windows NT security descriptor', \&decode_NT_security], - 0x4690, ['POSZIP 4690', undef], - 0x4704, ['VM/CMS', undef], - 0x470f, ['MVS', undef], - 0x4854, ['Theos, old inofficial port', undef], - 0x4b46, ['FWKCS MD5 (see below)', undef], - 0x4c41, ['OS/2 access control list (text ACL)', undef], - 0x4d49, ['Info-ZIP OpenVMS (obsolete)', undef], - 0x4d63, ['Macintosh SmartZIP, by Macro Bambini', undef], - 0x4f4c, ['Xceed original location extra field', undef], - 0x5356, ['AOS/VS (binary ACL)', undef], - 0x5455, ['Extended Timestamp', \&decode_UT], - 0x554e, ['Xceed unicode extra field', \&decode_Xceed_unicode], - 0x5855, ['Info-ZIP Unix (original; also OS/2, NT, etc.)', \&decode_UX], - 0x5a4c, ['ZipArchive Unicode Filename', undef], - 0x5a4d, ['ZipArchive Offsets Array', undef], - 0x6375, ['Info-ZIP Unicode Comment', \&decode_up ], - 0x6542, ['BeOS (BeBox, PowerMac, etc.)', undef], - 0x6854, ['Theos', undef], - 0x7075, ['Info-ZIP Unicode Path', \&decode_up ], - 0x756e, ['ASi Unix', undef], - 0x7441, ['AtheOS (AtheOS/Syllable attributes)', undef], - 0x7855, ['Unix Extra type 2', \&decode_Ux], - 0x7875, ['Unix Extra Type 3', \&decode_ux], - 0x9901, ['AES Encryption', \&decode_AES], - 0xa11e, ['Data Stream Alignment', \&decode_DataStreamAlignment], - 0xA220, ['Open Packaging Growth Hint', \&decode_GrowthHint ], - 0xCAFE, ['Java Executable', \&decode_Java_exe], - 0xfb4a, ['SMS/QDOS', undef], + # The Header ID mappings defined by Info-ZIP and third parties are: + 0x0065, ['IBM S/390 attributes - uncompressed', \&decode_MVS, 4, undef, 4, undef], + 0x0066, ['IBM S/390 attributes - compressed', undef], + 0x07c8, ['Info-ZIP Macintosh (old, J. Lee)', undef], + 0x10c5, ['Minizip CMS Signature', \&decode_Minizip_Signature, undef, undef, undef, undef], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md + 0x1986, ['Pixar USD', undef], # TODO + 0x1a51, ['Minizip Hash', \&decode_Minizip_Hash, 4, undef, 4, undef], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md + 0x2605, ['ZipIt Macintosh (first version)', undef], + 0x2705, ['ZipIt Macintosh v 1.3.5 and newer (w/o full filename)', undef], + 0x2805, ['ZipIt Macintosh v 1.3.5 and newer', undef], + 0x334d, ["Info-ZIP Macintosh (new, D. Haase's 'Mac3' field)", undef], # TODO + 0x4154, ['Tandem NSK [TA]', undef], # TODO + 0x4341, ['Acorn/SparkFS [AC]', undef], # TODO + 0x4453, ['Windows NT security descriptor [SD]', \&decode_NT_security, 11, undef, 4, 4], # TODO + 0x4690, ['POSZIP 4690', undef], + 0x4704, ['VM/CMS', undef], + 0x470f, ['MVS', undef], + 0x4854, ['Theos [TH]', undef], + 0x4b46, ['FWKCS MD5 [FK]', undef], + 0x4c41, ['OS/2 access control list [AL]', undef], + 0x4d49, ['Info-ZIP OpenVMS (obsolete) [IM]', undef], + 0x4d63, ['Macintosh SmartZIP [cM]', undef], # TODO + 0x4f4c, ['Xceed original location [LO]', undef], + 0x5356, ['AOS/VS (binary ACL) [VS]', undef], + 0x5455, ['Extended Timestamp [UT]', \&decode_UT, 1, 13, 1, 13], + 0x554e, ['Xceed unicode extra field [UN]', \&decode_Xceed_unicode, 6, undef, 8, undef], + 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 + 0x5855, ['Unix Extra type 1 [UX]', \&decode_UX, 12, 12, 8, 8], + 0x5a4c, ['ZipArchive Unicode Filename [LZ]', undef], # https://www.artpol-software.com/ZipArchive + 0x5a4d, ['ZipArchive Offsets Array [MZ]', undef], # https://www.artpol-software.com/ZipArchive + 0x6375, ['Unicode Comment [uc]', \&decode_uc, 5, undef, 5, undef], + 0x6542, ['BeOS/Haiku [Be]', undef], # TODO + 0x6854, ['Theos [Th]', undef], + 0x7075, ['Unicode Path [up]', \&decode_up, 5, undef, 5, undef], + 0x756e, ['ASi Unix [un]', \&decode_ASi_Unix], # TODO + 0x7441, ['AtheOS [At]', undef], + 0x7855, ['Unix Extra type 2 [Ux]', \&decode_Ux, 4,4, 0, 0 ], + 0x7875, ['Unix Extra type 3 [ux]', \&decode_ux, 3, undef, 3, undef], + 0x9901, ['AES Encryption', \&decode_AES, 7, 7, 7, 7], + 0x9903, ['Reference', \&decode_Reference, 20, 20, 20, 20], # Added in WinZip ver 25 + 0xa11e, ['Data Stream Alignment', \&decode_DataStreamAlignment, 2, undef, 2, undef ], + 0xA220, ['Open Packaging Growth Hint', \&decode_GrowthHint, 4, undef, 4, undef ], + 0xCAFE, ['Java Executable', \&decode_Java_exe, 0, 0, 0, 0], + 0xCDCD, ['Minizip Central Directory', \&decode_Minizip_CD, 8, 8, 8, 8], # https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md + 0xd935, ['Android APK Alignment', undef], # TODO + 0xE57a, ['ALZip Codepage', undef], # TODO + 0xfb4a, ['SMS/QDOS', undef], # TODO ); + # Dummy entry only used in test harness, so only enable when ZIPDETAILS_TESTHARNESS is set + $Extras{0xFFFF} = + ['DUMMY', \&decode_DUMMY, undef, undef, undef, undef] + if $ENV{ZIPDETAILS_TESTHARNESS} ; + +sub extraFieldIdentifier +{ + my $id = shift ; + + my $name = $Extras{$id}[0] // "Unknown"; + + return "Extra Field '$name' (ID " . hexValue16($id) .")"; +} + +# Zip64EndCentralHeader version 2 +my %HashIDLookup = ( + 0x0000 => 'none', + 0x0001 => 'CRC32', + 0x8003 => 'MD5', + 0x8004 => 'SHA1', + 0x8007 => 'RIPEMD160', + 0x800C => 'SHA256', + 0x800D => 'SHA384', + 0x800E => 'SHA512', + ); + + +# Zip64EndCentralHeader version 2, Strong Encryption Header & DecryptionHeader +my %AlgIdLookup = ( + 0x6601 => "DES", + 0x6602 => "RC2 (version needed to extract < 5.2)", + 0x6603 => "3DES 168", + 0x6609 => "3DES 112", + 0x660E => "AES 128", + 0x660F => "AES 192", + 0x6610 => "AES 256", + 0x6702 => "RC2 (version needed to extract >= 5.2)", + 0x6720 => "Blowfish", + 0x6721 => "Twofish", + 0x6801 => "RC4", + 0xFFFF => "Unknown algorithm", + ); + +# Zip64EndCentralHeader version 2, Strong Encryption Header & DecryptionHeader +my %FlagsLookup = ( + 0x0001 => "Password required to decrypt", + 0x0002 => "Certificates only", + 0x0003 => "Password or certificate required to decrypt", + + # Values > 0x0003 reserved for certificate processing + ); + +# Strong Encryption Header & DecryptionHeader +my %HashAlgLookup = ( + 0x8004 => 'SHA1', + ); my $FH; my $ZIP64 = 0 ; my $NIBBLES = 8; + my $LocalHeaderCount = 0; my $CentralHeaderCount = 0; +my $InfoCount = 0; +my $WarningCount = 0; +my $ErrorCount = 0; +my $lastWasMessage = 0; + +my $fatalDisabled = 0; -my $START; my $OFFSET = 0 ; + +# Prefix data +my $POSSIBLE_PREFIX_DELTA = 0; +my $PREFIX_DELTA = 0; + my $TRAILING = 0 ; my $PAYLOADLIMIT = 256; my $ZERO = 0 ; @@ -222,7 +420,10 @@ my $APK = 0 ; my $START_APK = 0; my $APK_LEN = 0; -my $SEEN = Seen->new(); +my $CentralDirectory = CentralDirectory->new(); +my $LocalDirectory = LocalDirectory->new(); +my $HeaderOffsetIndex = HeaderOffsetIndex->new(); +my $EOCD_Present = 0; sub prOff { @@ -239,10 +440,11 @@ sub offset sprintf("%0${NIBBLES}X", $v); } -my ($OFF, $LENGTH, $CONTENT, $TEXT, $VALUE) ; +# Format variables +my ($OFF, $ENDS_AT, $LENGTH, $CONTENT, $TEXT, $VALUE) ; -my $FMT1 ; -my $FMT2 ; +my $FMT1 = 'STDOUT1'; +my $FMT2 = 'STDOUT2'; sub setupFormat { @@ -252,51 +454,58 @@ sub setupFormat my $width = '@' . ('>' x ($nibbles -1)); my $space = " " x length($width); + # See https://github.com/Perl/perl5/issues/14255 for issue with "^*" in perl < 5.22 + # my $rightColumn = "^*" ; + my $rightColumn = "^" . ("<" x 132); + + # Fill mode can split on space or newline chars + # Spliting on hyphen works differently from Perl 5.20 onwards + $: = " \n"; + my $fmt ; if ($wantVerbose) { - $FMT1 = " - format STDOUT = -$width $width ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< -\$OFF, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE -$space $space ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ - \$CONTENT, \$TEXT, \$VALUE + eval "format $FMT1 = +$width $width $width ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< $rightColumn +\$OFF, \$ENDS_AT, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE +$space $space $space ^<<<<<<<<<<<^<<<<<<<<<<<<<<<<<<<< $rightColumn~~ + \$CONTENT, \$TEXT, \$VALUE . "; - $FMT2 = " - format STDOUT = -$width $width ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< -\$OFF, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE -$space $space ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ + eval "format $FMT2 = +$width $width $width ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< $rightColumn +\$OFF, \$ENDS_AT, \$LENGTH, \$CONTENT, \$TEXT, \$VALUE +$space $space $space ^<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<< $rightColumn~~ \$CONTENT, \$TEXT, \$VALUE -. " ; +. +"; } else { - - $FMT1 = " - format STDOUT = -$width ^<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + eval "format $FMT1 = +$width ^<<<<<<<<<<<<<<<<<<<< $rightColumn \$OFF, \$TEXT, \$VALUE -$space ^<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ +$space ^<<<<<<<<<<<<<<<<<<<< $rightColumn~~ \$TEXT, \$VALUE . "; - $FMT2 = " - format STDOUT = -$width ^<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + eval "format $FMT2 = +$width ^<<<<<<<<<<<<<<<<<< $rightColumn \$OFF, \$TEXT, \$VALUE -$space ^<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ +$space ^<<<<<<<<<<<<<<<<<< $rightColumn~~ \$TEXT, \$VALUE . -" ; +" } - eval "$FMT1"; + no strict 'refs'; + open($FMT1, ">&", \*STDOUT); select $FMT1; $| = 1 ; + open($FMT2, ">&", \*STDOUT); select $FMT2; $| = 1 ; + select 'STDOUT'; $| = 1; } @@ -310,23 +519,6 @@ sub mySpr return sprintf $format, @_ ; } -sub out0 -{ - my $size = shift; - my $text = shift; - my $format = shift; - - $OFF = prOff($size); - $LENGTH = offset($size) ; - $CONTENT = '...'; - $TEXT = $text; - $VALUE = mySpr $format, @_; - - write; - - skip($FH, $size); -} - sub xDump { my $input = shift; @@ -337,19 +529,81 @@ sub xDump sub hexDump { - my $input = shift; + return uc join ' ', unpack('(H2)*', $_[0]); +} + +sub hexDump16 +{ + return uc + join "\r", + map { join ' ', unpack('(H2)*', $_ ) } + unpack('(a16)*', $_[0]) ; +} - my $out = unpack('H*', $input) ; - $out =~ s#(..)# $1#g ; - $out =~ s/^ //; - $out = uc $out; +sub charDump2 +{ + sprintf "%v02X", $_[0]; +} - return $out; +sub charDump +{ + sprintf "%vX", $_[0]; } sub hexValue { - return sprintf("%X", $_[0]); + return sprintf("0x%X", $_[0]); +} + +sub hexValue32 +{ + return sprintf("0x%08X", $_[0]); +} + +sub hexValue16 +{ + return sprintf("0x%04X", $_[0]); +} + +sub outHexdump +{ + my $size = shift; + my $text = shift; + my $limit = shift ; + + return 0 + if $size == 0; + + # TODO - add a limit to data output + # if ($limit) + # { + # outSomeData($size, $text); + # } + # else + { + myRead(my $payload, $size); + out($payload, $text, hexDump16($payload)); + } + + return $size; +} + +sub decimalHex +{ + sprintf("%0*X (%u)", $_[1] // 0, $_[0], $_[0]) +} + +sub decimalHex0x +{ + sprintf("0x%0*X (%u)", $_[1] // 0, $_[0], $_[0]) +} + +sub decimalHex0xUndef +{ + return 'Unknown' + if ! defined $_[0]; + + return decimalHex0x @_; } sub out @@ -360,6 +614,7 @@ sub out my $size = length($data) ; + $ENDS_AT = offset($OFFSET + $size - 1) ; $OFF = prOff($size); $LENGTH = offset($size) ; $CONTENT = hexDump($data); @@ -368,7 +623,29 @@ sub out no warnings; - write; + write $FMT1 ; + + $lastWasMessage = 0; +} + +sub out0 +{ + my $size = shift; + my $text = shift; + my $format = shift; + + $ENDS_AT = offset($OFFSET + $size - 1) ; + $OFF = prOff($size); + $LENGTH = offset($size) ; + $CONTENT = '...'; + $TEXT = $text; + $VALUE = mySpr $format, @_; + + write $FMT1; + + skip($FH, $size); + + $lastWasMessage = 0; } sub out1 @@ -376,13 +653,16 @@ sub out1 my $text = shift; my $format = shift; + $ENDS_AT = '' ; $OFF = ''; $LENGTH = '' ; $CONTENT = ''; $TEXT = $text; $VALUE = mySpr $format, @_; - write; + write $FMT1; + + $lastWasMessage = 0; } sub out2 @@ -392,6 +672,7 @@ sub out2 my $format = shift; my $size = length($data) ; + $ENDS_AT = offset($OFFSET + $size - 1) ; $OFF = prOff($size); $LENGTH = offset($size); $CONTENT = hexDump($data); @@ -399,26 +680,26 @@ sub out2 $VALUE = mySpr $format, @_; no warnings; - eval "$FMT2"; - write ; - eval "$FMT1"; + write $FMT2; + + $lastWasMessage = 0; } + sub Value { my $letter = shift; - my @value = @_; if ($letter eq 'C') - { return Value_C(@value) } + { return decimalHex($_[0], 2) } elsif ($letter eq 'v') - { return Value_v(@value) } + { return decimalHex($_[0], 4) } elsif ($letter eq 'V') - { return Value_V(@value) } + { return decimalHex($_[0], 8) } elsif ($letter eq 'Q<') - { return Value_Q(@value) } + { return decimalHex($_[0], 16) } else - { die "here letter $letter"} + { internalFatal undef, "here letter $letter"} } sub outer @@ -498,6 +779,8 @@ sub outSomeData my $message = shift; my $redact = shift ; + # return if $size == 0; + if ($size > 0) { if ($size > $PAYLOADLIMIT) { my $before = $FH->tell(); @@ -511,6 +794,19 @@ sub outSomeData } } +sub outSomeDataParagraph +{ + my $size = shift; + my $message = shift; + my $redact = shift ; + + return if $size == 0; + + print "\n"; + outSomeData($size, $message, $redact); + +} + sub unpackValue_C { Value_v(unpack "C", $_[0]); @@ -518,7 +814,7 @@ sub unpackValue_C sub Value_C { - sprintf "%02X", $_[0]; + return decimalHex($_[0], 2); } @@ -529,7 +825,7 @@ sub unpackValue_v sub Value_v { - sprintf "%04X", $_[0]; + return decimalHex($_[0], 4); } sub unpackValue_V @@ -539,8 +835,7 @@ sub unpackValue_V sub Value_V { - my $v = defined $_[0] ? $_[0] : 0; - sprintf "%08X", $v; + return decimalHex($_[0] // 0, 8); } sub unpackValue_Q @@ -551,8 +846,7 @@ sub unpackValue_Q sub Value_Q { - my $v = shift ; - sprintf "%016X", $v; + return decimalHex($_[0], 16); } sub read_Q @@ -596,63 +890,95 @@ sub seekTo $OFFSET = $FH->tell(); } -sub scanForSignature +sub rewindRelative { - my %sigs = - map { $_ => 1 } - map { substr $_, 2, 2 } # don't want the initial "PK" - map { pack "V", $_ } - ( - ZIP_LOCAL_HDR_SIG , - ZIP_DATA_HDR_SIG , - ZIP_CENTRAL_HDR_SIG , - ZIP_END_CENTRAL_HDR_SIG , - ZIP64_END_CENTRAL_REC_HDR_SIG , - ZIP64_END_CENTRAL_LOC_HDR_SIG , - # ZIP_ARCHIVE_EXTRA_DATA_SIG , - # ZIP_DIGITAL_SIGNATURE_SIG , - # ZIP_ARCHIVE_EXTRA_DATA_RECORD_SIG , - ); + my $offset = shift ; + + $FH->seek(-$offset, SEEK_CUR); + # $OFFSET -= $offset; + $OFFSET = $FH->tell(); +} +sub deltaToNextSignature +{ my $start = $FH->tell(); - my $last = ''; - my $offset = 0; - my $buffer ; - BUFFER: - while ($FH->read($buffer, 1024 * 1000)) + my $got = scanForSignature(1); + + my $delta = $FH->tell() - $start ; + seekTo($start); + + if ($got) { - my $combine = $last . $buffer ; + return $delta ; + } - my $ix = 0; - while (1) + return 0 ; +} + +sub scanForSignature +{ + my $walk = shift // 0; + + # $count is only used to when 'walk' is enabled. + # Want to scan for a PK header at the start of the file. + # All other PK headers are should be directly after the previous PK record. + state $count = 0; + $count += $walk; + + my %sigs = Signatures::getSigsForScan(); + + my $start = $FH->tell(); + + # TODO -- Fix this? + if (1 || $count <= 1) { + + my $last = ''; + my $offset = 0; + my $buffer ; + + BUFFER: + while ($FH->read($buffer, 1024 * 1000)) { - $ix = index($combine, "PK", $ix) ; + my $combine = $last . $buffer ; - if ($ix == -1) + my $ix = 0; + while (1) { - $last = ''; - next BUFFER; - } + $ix = index($combine, "PK", $ix) ; - my $rest = substr($combine, $ix + 2, 2); + if ($ix == -1) + { + $last = ''; + next BUFFER; + } - if (! $sigs{$rest}) - { - $ix += 2; - next; - } + my $rest = substr($combine, $ix + 2, 2); - # possible match - my $here = $FH->tell(); - seekTo($here - length($combine) + $ix); + if (! $sigs{$rest}) + { + $ix += 2; + next; + } - return 1; - } + # possible match + my $here = $FH->tell(); + seekTo($here - length($combine) + $ix); + + my $name = Signatures::name($sigs{$rest}); + return $sigs{$rest}; + } - $last = substr($combine, $ix+4); + $last = substr($combine, $ix+4); + } + } + else { + die "FIX THIS"; + return ! $FH->eof(); } + # printf("scanForSignature %X\t%X (%X)\t%s\n", $start, $FH->tell(), $FH->tell() - $start, 'NO MATCH') ; + return 0; } @@ -660,733 +986,4960 @@ my $is64In32 = 0; my $opt_verbose = 0; my $opt_scan = 0; +my $opt_walk = 0; my $opt_Redact = 0; my $opt_utc = 0; - +my $opt_want_info_mesages = 1; +my $opt_want_warning_mesages = 1; +my $opt_want_error_mesages = 1; +my $opt_want_message_exit_status = 0; +my $exit_status_code = 0; +my $opt_help =0; $Getopt::Long::bundling = 1 ; -GetOptions("h" => \&Usage, - "v" => \$opt_verbose, - "scan" => \$opt_scan, - "redact" => \$opt_Redact, - "utc" => \$opt_utc, - "version" => sub { print "$VERSION\n"; exit }, +TextEncoding::setDefaults(); + +GetOptions("h|help" => \$opt_help, + "v" => \$opt_verbose, + "scan" => \$opt_scan, + "walk" => \$opt_walk, + "redact" => \$opt_Redact, + "utc" => \$opt_utc, + "version" => sub { print "$VERSION\n"; exit }, + + # Filename/comment encoding + "encoding=s" => \&TextEncoding::parseEncodingOption, + "no-encoding" => \&TextEncoding::NoEncoding, + "debug-encoding" => \&TextEncoding::debugEncoding, + "output-encoding=s" => \&TextEncoding::parseEncodingOption, + "language-encoding!" => \&TextEncoding::LanguageEncodingFlag, + + # Message control + "exit-bitmask!" => \$opt_want_message_exit_status, + "messages!" => sub { + my ($opt_name, $opt_value) = @_; + $opt_want_info_mesages = + $opt_want_warning_mesages = + $opt_want_error_mesages = $opt_value; + }, ) - or Usage("Invalid command line option\n"); + or exit 255 ; +Usage() + if $opt_help; -Usage("No zipfile") unless @ARGV == 1; +die("No zipfile\n") + unless @ARGV == 1; + +die("Cannot specify both '--walk' and '--scan'\n") + if $opt_walk && $opt_scan ; my $filename = shift @ARGV; -die "$filename does not exist\n" +topLevelFatal "No such file" unless -e $filename ; -die "$filename not a standard file\n" +topLevelFatal "'$filename' is a directory" + if -d $filename ; + +topLevelFatal "'$filename' is not a standard file" unless -f $filename ; $FH = IO::File->new( "<$filename" ) - or die "Cannot open $filename: $!\n"; + or topLevelFatal "Cannot open '$filename': $!"; +binmode($FH); +displayFileInfo($filename); +TextEncoding::encodingInfo(); my $FILELEN = -s $filename ; $TRAILING = -s $filename ; $NIBBLES = nibbles(-s $filename) ; -die "$filename too short to be a zip file\n" - if $FILELEN < 22 ; - -setupFormat($opt_verbose, $NIBBLES); +topLevelFatal "'$filename' is empty" + if $FILELEN == 0 ; -if(0) -{ - # Sanity check that this is a Zip file - my ($buffer, $signature) = read_V(); +topLevelFatal "file is too short to be a zip file" + if $FILELEN < ZIP_EOCD_MIN_SIZE ; - warn "$filename doesn't look like a zip file\n" - if $signature != ZIP_LOCAL_HDR_SIG ; - $FH->seek(0, SEEK_SET) ; -} +setupFormat($opt_verbose, $NIBBLES); my @Messages = (); -if ($opt_scan) +if ($opt_scan || $opt_walk) { + # Main loop for walk/scan processing + + my $foundZipRecords = 0; my $foundCentralHeader = 0; my $lastEndsAt = 0; + my $lastSignature = 0; + my $lastHeader = {}; + + $CentralDirectory->{alreadyScanned} = 1 ; - while(scanForSignature()) + my $output_encryptedCD = 0; + + reportPrefixData(); + while(my $s = scanForSignature($opt_walk)) { my $here = $FH->tell(); + my $delta = $here - $lastEndsAt ; - my ($buffer, $signature) = read_V(); - - # check for an APK header directly before the first central header - if ($signature == ZIP_CENTRAL_HDR_SIG && $foundCentralHeader == 0) + # delta can only be negative when '--scan' is used + if ($delta < 0 ) + { + # nested or overlap + # check if nested + # remember & check if matching entry in CD + # printf("### WARNING: OVERLAP/NESTED Record found 0x%X 0x%X $delta\n", $here, $lastEndsAt) ; + } + elsif ($here != $lastEndsAt) { - $foundCentralHeader = 1; + # scanForSignature had to skip bytes to find the next signature + + # some special cases that don't have signatures need to be checked first - ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here) ; + seekTo($lastEndsAt); - if ($START_APK) + if (! $output_encryptedCD && $CentralDirectory->isEncryptedCD()) { - seekTo($lastEndsAt+4); + displayEncryptedCD(); + $output_encryptedCD = 1; + $lastEndsAt = $FH->tell(); + next; + } + elsif ($lastSignature == ZIP_LOCAL_HDR_SIG && $lastHeader->{'streamed'} ) + { + # Check for size of possibe malformed Data Descriptor before outputting payload + if (! $lastHeader->{'gotDataDescriptorSize'}) + { + my $hdrSize = checkForBadlyFormedDataDescriptor($lastHeader, $delta) ; + + if ($hdrSize) + { + # remove size of Data Descriptor from payload + $delta -= $hdrSize; + $lastHeader->{'gotDataDescriptorSize'} = $hdrSize; + } + } - scanApkBlock(); - seekTo($here); + if(defined($lastHeader->{'payloadOutput'}) && ($lastEndsAt = BadlyFormedDataDescriptor($lastHeader, $delta))) + { + $HeaderOffsetIndex->rewindIndex(); + $lastHeader->{entry}->readDataDescriptor(1) ; + next; + } + + # Assume we have the payload when streaming is enabled + outSomeData($delta, "PAYLOAD", $opt_Redact) ; + $lastHeader->{'payloadOutput'} = 1; + $lastEndsAt = $FH->tell(); + + next; + } + elsif (Signatures::isCentralHeader($s) && $foundCentralHeader == 0) + { + # check for an APK header directly before the first central header + $foundCentralHeader = 1; + + ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here, 0) ; + + if ($START_APK) + { + seekTo($lastEndsAt+4); + + scanApkBlock(); + $lastEndsAt = $FH->tell(); + next; + } + + seekTo($lastEndsAt); + } + + # Not a special case, so output generic padding message + if ($delta > 0) + { + reportPrefixData($delta) + if $lastEndsAt == 0 ; + outSomeDataParagraph($delta, "UNEXPECTED PADDING"); + info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes" + if $FH->tell() - $delta ; + $POSSIBLE_PREFIX_DELTA = $delta + if $lastEndsAt == 0; + $lastEndsAt = $FH->tell(); next; } + else + { + seekTo($here); + } + + } + + my ($buffer, $signature) = read_V(); + + $lastSignature = $signature; - seekTo($here + 4); + my $handler = Signatures::decoder($signature); + if (!defined $handler) { + internalFatal undef, "xxx"; } - my $handler = $Lookup{$signature}; - $handler->($signature, $buffer); + $foundZipRecords = 1; + $lastHeader = $handler->($signature, $buffer, $FH->tell() - 4) // {'streamed' => 0}; $lastEndsAt = $FH->tell(); - seekTo($here + 4) ; + seekTo($here + 4) + if $opt_scan; } - dislayMessages(); - exit; + topLevelFatal "'$filename' is not a zip file" + unless $foundZipRecords ; } +else +{ + # Main loop for non-walk/scan processing -our ($CdExists, $CdOffset, @CentralDirectory) = scanCentralDirectory($FH); + # check for prefix data + my $s = scanForSignature(); + if ($s && $FH->tell() != 0) + { + $POSSIBLE_PREFIX_DELTA = $FH->tell(); + } -die "No Central Directory records found\n" - if ! $CdExists ; + seekTo(0); -$OFFSET = 0 ; -$FH->seek(0, SEEK_SET) ; + scanCentralDirectory($FH); -outSomeData($START, "PREFIX DATA") - if defined $START && $START > 0 ; + fatal_tryWalk undef, "No Zip metadata found at end of file" + if ! $CentralDirectory->exists() && ! $EOCD_Present ; -my $skippedFrom = 0 ; -my $skippedContent = 0 ; + $CentralDirectory->{alreadyScanned} = 1 ; -while (1) -{ - last if $FH->eof(); + Nesting::clearStack(); - my $here = $FH->tell(); - if ($here >= $TRAILING) { - print "\n" ; - outSomeData($FILELEN - $TRAILING, "TRAILING DATA"); - last; + # $HeaderOffsetIndex->dump(); - } + $OFFSET = 0 ; + $FH->seek(0, SEEK_SET) ; - my ($buffer, $signature) = read_V(); + my $expectedOffset = 0; + my $expectedSignature = 0; + my $expectedBuffer = 0; + my $foundCentralHeader = 0; + my $processedAPK = 0; + my $processedECD = 0; + my $lastHeader ; - my $handler = $Lookup{$signature}; + # my $lastWasLocalHeader = 0; + # my $inCentralHeader = 0; - if (!defined $handler) + while (1) { - if (@CentralDirectory) { - # Should be at offset that central directory says - my $locOffset = $CentralDirectory[0][0]; - my $delta = $locOffset - $here ; + last if $FH->eof(); - if ($here + 4 == $locOffset ) { - for (0 .. 3) { - $FH->ungetc(ord(substr($buffer, $_, 1))) - } - outSomeData($delta, "UNEXPECTED PADDING"); - next; - } + my $here = $FH->tell(); + + if ($here >= $TRAILING) { + my $delta = $FILELEN - $TRAILING; + outSomeDataParagraph($delta, "TRAILING DATA"); + info $FH->tell(), "Unexpected Trailing Data: " . decimalHex0x($delta) . " bytes"; + + last; } + my ($buffer, $signature) = read_V(); + + $expectedOffset = undef; + $expectedSignature = undef; - if ($here < $CdOffset) + # Check for split archive marker at start of file + if ($here == 0 && $signature == ZIP_SINGLE_SEGMENT_MARKER) + { + # let it drop through + $expectedSignature = ZIP_SINGLE_SEGMENT_MARKER; + $expectedOffset = 0; + } + else { - if ($APK) + my $expectedEntry = $HeaderOffsetIndex->getNextIndex() ; + if ($expectedEntry) { - scanApkBlock(); - next; + $expectedOffset = $expectedEntry->offset(); + $expectedSignature = $expectedEntry->signature(); + $expectedBuffer = pack "V", $expectedSignature ; } + } + + my $delta = $expectedOffset - $here ; - # next - # if scanForSignature() ; + # if ($here != $expectedOffset && $signature != ZIP_DATA_HDR_SIG) + # { + # rewindRelative(4); + # my $delta = $expectedOffset - $here ; + # outSomeDataParagraph($delta, "UNEXPECTED PADDING"); + # $HeaderOffsetIndex->rewindIndex(); + # next; + # } - $skippedFrom = $FH->tell() ; - $skippedContent = $CdOffset - $skippedFrom ; + # Need to check for use-case where + # * there is a ZIP_DATA_HDR_SIG directly after a ZIP_LOCAL_HDR_SIG. + # The HeaderOffsetIndex object doesn't have visibility of it. + # * APK header directly before the CD + # * zipbomb - printf "\nWARNING!\nZip local header not found at offset 0x%X\n", $skippedFrom; - printf "Skipping 0x%x bytes to Central Directory...\n", $skippedContent; + if (defined $expectedOffset && $here != $expectedOffset && ( $CentralDirectory->exists() || $EOCD_Present) ) + { + if ($here > $expectedOffset) + { + # Probable zipbomb - push @Messages, - sprintf("Expected Zip header not found at offset 0x%X, ", $skippedFrom) . - sprintf("skipped 0x%X bytes\n", $skippedContent); + # Cursor $OFFSET need to rewind + $OFFSET = $expectedOffset; + $FH->seek($OFFSET + 4, SEEK_SET) ; - seekTo($CdOffset); + $signature = $expectedSignature; + $buffer = $expectedBuffer ; + } - next; - } - else - { - printf "\n\nUnexpected END at offset %08X, value %s\n", $here, Value_V($signature); + # If get here then $here is less than $expectedOffset - last; - } - } - $ZIP64 = 0 if $signature != ZIP_DATA_HDR_SIG ; - $handler->($signature, $buffer); -} + # check for an APK header directly before the first central header + if (Signatures::isCentralHeader($expectedSignature) && $START_APK && ! $processedAPK ) + { + # my ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($FH, $here, 0) ; + seekTo($here+4); + # rewindRelative(4); + scanApkBlock(); + $HeaderOffsetIndex->rewindIndex(); + $processedAPK = 1; + next; + } -dislayMessages(); + # Check Encrypted Central Directory + # if ($CentralHeaderSignatures{$expectedSignature} && $CentralDirectory->isEncryptedCD() && ! $processedECD) + # { + # # rewind the invalid signature + # seekTo($here); + # # rewindRelative(4); + # displayEncryptedCD(); + # $processedECD = 1; + # next; + # } + + if ($signature != ZIP_DATA_HDR_SIG && $delta >= 0) + { + rewindRelative(4); + if($lastHeader->{'streamed'} && BadlyFormedDataDescriptor($lastHeader, $delta)) + { + $lastHeader->{entry}->readDataDescriptor(1) ; + $HeaderOffsetIndex->rewindIndex(); + next; + } -exit ; + reportPrefixData($delta) + if $here == 0; + outSomeDataParagraph($delta, "UNEXPECTED PADDING"); + info $FH->tell() - $delta, decimalHex0x($delta) . " Unexpected Padding bytes" + if $FH->tell() - $delta ; + $HeaderOffsetIndex->rewindIndex(); + next; + } -sub dislayMessages -{ - $SEEN->short_summary(); + # ZIP_DATA_HDR_SIG drops through + } - $SEEN->summary(); + my $handler = Signatures::decoder($signature); + if (!defined $handler) + { + # if ($CentralDirectory->exists()) { + + # # Should be at offset that central directory says + # my $locOffset = $CentralDirectory->getNextLocalOffset(); + # my $delta = $locOffset - $here ; + + # if ($here + 4 == $locOffset ) { + # for (0 .. 3) { + # $FH->ungetc(ord(substr($buffer, $_, 1))) + # } + # outSomeData($delta, "UNEXPECTED PADDING"); + # next; + # } + # } + + + # if ($here == $CentralDirectory->{CentralDirectoryOffset} && $EOCD_Present && $CentralDirectory->isEncryptedCD()) + # { + # # rewind the invalid signature + # rewindRelative(4); + # displayEncryptedCD(); + # next; + # } + # elsif ($here < $CentralDirectory->{CentralDirectoryOffset}) + # { + # # next + # # if scanForSignature() ; + + # my $skippedFrom = $FH->tell() ; + # my $skippedContent = $CentralDirectory->{CentralDirectoryOffset} - $skippedFrom ; + + # printf "\nWARNING!\nExpected Zip header not found at offset 0x%X\n", $here; + # printf "Skipping 0x%X bytes to Central Directory...\n", $skippedContent; + + # push @Messages, + # sprintf("Expected Zip header not found at offset 0x%X, ", $skippedFrom) . + # sprintf("skipped 0x%X bytes\n", $skippedContent); + + # seekTo($CentralDirectory->{CentralDirectoryOffset}); + + # next; + # } + # else + { + fatal $here, sprintf "Unexpected Zip Signature '%s' at offset %s", Value_V($signature), decimalHex0x($here) ; + last; + } + } - if (@Messages) - { - my $count = scalar @Messages ; - print "\nWARNINGS\n\n"; - print "* $_\n" for @Messages ; + $ZIP64 = 0 if $signature != ZIP_DATA_HDR_SIG ; + $lastHeader = $handler->($signature, $buffer, $FH->tell() - 4); + # $lastWasLocalHeader = $signature == ZIP_LOCAL_HDR_SIG ; + $HeaderOffsetIndex->rewindIndex() + if $signature == ZIP_DATA_HDR_SIG ; } - - print "Done\n"; } -sub compressionMethod -{ - my $id = shift ; - Value_v($id) . " '" . ($ZIP_CompressionMethods{$id} || "Unknown Method") . "'" ; -} -sub LocalHeader -{ - my $signature = shift ; - my $data = shift ; +dislayMessages() + if $opt_want_error_mesages ; - my $from_offset = $FH->tell() - 4; +exit $exit_status_code ; - print "\n"; - ++ $LocalHeaderCount; - my $hexHdrCount = sprintf("%X", $LocalHeaderCount) ; - out $data, "LOCAL HEADER #$hexHdrCount" , Value_V($signature); +sub dislayMessages +{ - my $buffer; + # Compare Central & LOcal for discrepencies - my ($loc, $CDcompressedLength, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) ; - if (! $opt_scan) + if ($CentralDirectory->isMiniZipEncrypted) { - ($loc, $CDcompressedLength, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) = @{ shift @CentralDirectory } ; - # my $cdi = sprintf("%X", $cdIndex) ; - # out1 "CENTRAL HEADER #$cdi", sprintf "Offset %s\n", Value_Q($cdEntryOffset); + # don't compare local & central entries when minizip-ng encryption is in play + info undef, "Zip file uses minizip-ng central directory encryption" } - out_C "Extract Zip Spec", \&decodeZipVer; - out_C "Extract OS", \&decodeOS; + elsif ($CentralDirectory->exists() && $LocalDirectory->exists()) + { + # TODO check number of entries matches eocd + # TODO check header length matches reality - my ($bgp, $gpFlag) = read_v(); - my ($bcm, $compressedMethod) = read_v(); + # Nesting::dump(); - out $bgp, "General Purpose Flag", Value_v($gpFlag) ; - GeneralPurposeBits($compressedMethod, $gpFlag); + $LocalDirectory->sortByLocalOffset(); + my %cleanCentralEntries = %{ $CentralDirectory->{byCentralOffset} }; - out $bcm, "Compression Method", compressionMethod($compressedMethod) ; + if ($NESTING_DEBUG) + { + if (Nesting::encapsulationCount()) + { + say "# ENCAPSULATIONS"; - out_V "Last Mod Time", sub { getTime(_dosToUnixTime($_[0])) }; + for my $index (sort { $a <=> $b } keys %{ Nesting::encapsulations() }) + { + my $outer = Nesting::entryByIndex($index) ; - my $crc = out_V "CRC"; - my $compressedLength = out_V "Compressed Length"; - my $uncompressedLength = out_V "Uncompressed Length"; - my $filenameLength = out_v "Filename Length"; - my $extraLength = out_v "Extra Length"; + say "# Nesting " . $outer->outputFilename . " " . $outer->offsetStart . " " . $outer->offsetEnd ; - my $filename ; - myRead($filename, $filenameLength); - outputFilename($filename); + for my $inner (sort { $a <=> $b } @{ Nesting::encapsulations()->{$index} } ) + { + say "# " . $inner->outputFilename . " " . $inner->offsetStart . " " . $inner->offsetEnd ;; + } + } + } + } - my $cl64 = $compressedLength; - my %ExtraContext = (); - if ($extraLength) - { - my @z64 = ($uncompressedLength, $compressedLength, 1, 1); - $ExtraContext{Zip64} = \@z64 ; - $ExtraContext{InCentralDir} = 0; - walkExtra($extraLength, \%ExtraContext); - } + { + # check for Local Directory orphans + my %orphans = map { $_->localHeaderOffset => $_->outputFilename } + grep { $_->entryType == ZIP_LOCAL_HDR_SIG && # Want Local Headers + ! $_->encapsulated && + @{ $_->getCdEntries } == 0 + } + values %{ Nesting::getEntriesByOffset() }; - my @msg ; - # if ($cdZip64 && ! $ZIP64) - # { - # # Central directory said this was Zip64 - # # some zip files don't have the Zip64 field in the local header - # # seems to be a streaming issue. - # push @msg, "Missing Zip64 extra field in Local Header #$hexHdrCount\n"; - # if (! $zip64Sizes) - # { - # # Central has a ZIP64 entry that doesn't have sizes - # # Local doesn't have a Zip 64 at all - # push @msg, "Unzip may complain about 'overlapped components' #$hexHdrCount\n"; - # } - # else - # { - # $ZIP64 = 1 - # } - # } + if (keys %orphans) + { + error undef, "Orphan Local Headers found: " . scalar(keys %orphans) ; + my $table = new SimpleTable; + $table->addHeaderRow('Offset', 'Filename'); + $table->addDataRow(decimalHex0x($_), $orphans{$_}) + for sort { $a <=> $b } keys %orphans ; + $table->display(); + } + } - my $size = 0; - $size = printAes(\%ExtraContext) - if $compressedMethod == 99 ; + { + # check for Central Directory orphans + # probably only an issue with --walk & a zipbomb - $size += printLzmaProperties() - if $compressedMethod == ZIP_CM_LZMA ; + my %orphans = map { $_->centralHeaderOffset => $_ } + grep { $_->entryType == ZIP_CENTRAL_HDR_SIG # Want Central Headers + && ! $_->ldEntry # Filter out orphans + && ! $_->encapsulated # Not encapsulated + } + values %{ Nesting::getEntriesByOffset() }; - $CDcompressedLength = $compressedLength - if $opt_scan ; + if (keys %orphans) + { + error undef, "Possible zipbomb -- Orphan Central Headers found: " . scalar(keys %orphans) ; - $CDcompressedLength -= $size; + my $table = new SimpleTable; + $table->addHeaderRow('Offset', 'Filename'); + for (sort { $a <=> $b } keys %orphans ) + { + $table->addDataRow(decimalHex0x($_), $orphans{$_}{filename}); + delete $cleanCentralEntries{ $_ }; + } - if ($CDcompressedLength) { - outSomeData($CDcompressedLength, "PAYLOAD", $opt_Redact) ; - } + $table->display(); + } + } - if ($compressedMethod == 99) { - my $auth ; - myRead($auth, 10); - out $auth, "AES Auth", hexDump($auth); - } + if (Nesting::encapsulationCount()) + { + # Benign Nested zips + # This is the use-case where a zip file is "stored" in another zip file. + # NOT a zipbomb -- want the benign nested entries + + # Note: this is only active when scan is used + + my %outerEntries = map { $_->localHeaderOffset => $_->outputFilename } + grep { + $_->entryType == ZIP_CENTRAL_HDR_SIG && + ! $_->encapsulated && # not encapsulated + $_->ldEntry && # central header has a local sibling + $_->ldEntry->childrenCount && # local entry has embedded entries + ! Nesting::childrenInCentralDir($_->ldEntry) + } + values %{ Nesting::getEntriesByOffset() }; + + if (keys %outerEntries) + { + my $count = scalar keys %outerEntries; + info undef, "Nested Zip files found: $count"; - print "WARNING: $_" - for @msg; + my $table = new SimpleTable; + $table->addHeaderRow('Offset', 'Filename'); + $table->addDataRow(decimalHex0x($_), $outerEntries{$_}) + for sort { $a <=> $b } keys %outerEntries ; - push @Messages, @msg ; + $table->display(); + } + } + if ($LocalDirectory->anyStreamedEntries) + { + # Check for a missing Data Descriptors - # $SEEN->save("LOCAL HEADER #" . sprintf("%X", $LocalHeaderCount), $filename, $from_offset, $from_offset + $CDcompressedLength); -} + my %missingDataDescriptor = map { $_->localHeaderOffset => $_->outputFilename } + grep { $_->entryType == ZIP_LOCAL_HDR_SIG && + $_->streamed && + ! $_->readDataDescriptor + } + values %{ Nesting::getEntriesByOffset() }; -sub redactFilename -{ - my $filename = shift; - # Redact everything apart from directory seperators - $filename =~ s([^/])(X)g - if $opt_Redact; + for my $offset (sort keys %missingDataDescriptor) + { + my $filename = $missingDataDescriptor{$offset}; + error $offset, "Filename '$filename': Missing 'Data Descriptor'" ; + } + } - return $filename; -} + { + # compare local & central for duplicate entries (CD entries point to same local header) -sub outputFilename -{ - my $filename = shift; + my %ByLocalOffset = map { $_->localHeaderOffset => $_ } + grep { + $_->entryType == ZIP_LOCAL_HDR_SIG # Want Local Headers + && ! $_->encapsulated # Not encapsulated + && @{ $_->getCdEntries } > 1 + } + values %{ Nesting::getEntriesByOffset() }; - $filename = redactFilename($filename) ; + for my $offset (sort keys %ByLocalOffset) + { + my @entries = @{ $ByLocalOffset{$offset}->getCdEntries }; + if (@entries > 1) + { + # found duplicates + my $localEntry = $LocalDirectory->getByLocalOffset($offset) ; + if ($localEntry) + { + error undef, "Possible zipbomb -- Duplicate Central Headers referring to one Local header for '" . $localEntry->outputFilename . "' at offset " . decimalHex0x($offset); + } + else + { + error undef, "Possible zipbomb -- Duplicate Central Headers referring to one Local header at offset " . decimalHex0x($offset); + } + + my $table = new SimpleTable; + $table->addHeaderRow('Offset', 'Filename'); + for (sort { $a->centralHeaderOffset <=> $b->centralHeaderOffset } @entries) + { + $table->addDataRow(decimalHex0x($_->centralHeaderOffset), $_->outputFilename); + delete $cleanCentralEntries{ $_->centralHeaderOffset }; + } + + $table->display(); + } + } + } - if (length $filename > 256) - { - my $f = substr($filename, 0, 256) ; - out $f, "Filename", "'". $f . "' ..."; - } - else - { - out $filename, "Filename", "'". $filename . "'"; - } -} + if (Nesting::encapsulationCount()) + { + # compare local & central for nested entries -sub CentralHeader -{ - my $signature = shift ; - my $data = shift ; + # get the local offsets referenced in the CD + # this deliberately ignores any valid nested local entries + my @localOffsets = sort { $a <=> $b } keys %{ $CentralDirectory->{byLocalOffset} }; - my $from_offset = $FH->tell() - 4; + # now check for nesting - ++ $CentralHeaderCount; - print "\n"; - out $data, "CENTRAL HEADER #" . sprintf("%X", $CentralHeaderCount) . "", Value_V($signature); - my $buffer; + my %nested ; + my %bomb; - out_C "Created Zip Spec", \&decodeZipVer; - out_C "Created OS", \&decodeOS; - out_C "Extract Zip Spec", \&decodeZipVer; - out_C "Extract OS", \&decodeOS; + for my $offset (@localOffsets) + { + my $innerEntry = $LocalDirectory->{byLocalOffset}{$offset}; + if ($innerEntry) + { + my $outerLocalEntry = Nesting::getOuterEncapsulation($innerEntry); + if (defined $outerLocalEntry) + { + my $outerOffset = $outerLocalEntry->localHeaderOffset(); + if ($CentralDirectory->{byLocalOffset}{ $offset }) + { + push @{ $bomb{ $outerOffset } }, $offset ; + } + else + { + push @{ $nested{ $outerOffset } }, $offset ; + } + } + } + } - my ($bgp, $gpFlag) = read_v(); - my ($bcm, $compressedMethod) = read_v(); + if (keys %nested) + { + # The real central directory at eof does not know about these. + # likely to be a zip file stored in another zip file + warning undef, "Nested Local Entries found"; + for my $loc (sort keys %nested) + { + my $count = scalar @{ $nested{$loc} }; + my $outerEntry = $LocalDirectory->getByLocalOffset($loc); + say "Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) . " has $count nested Local Headers"; + for my $n ( @{ $nested{$loc} } ) + { + my $innerEntry = $LocalDirectory->getByLocalOffset($n); + + say "# Nested Local Header for filename '" . $innerEntry->outputFilename . "' is at Offset " . decimalHex0x($n) ; + } + } + } - out $bgp, "General Purpose Flag", Value_v($gpFlag) ; - GeneralPurposeBits($compressedMethod, $gpFlag); + if (keys %bomb) + { + # Central Directory knows about these, so this is a zipbomb - out $bcm, "Compression Method", compressionMethod($compressedMethod) ; + error undef, "Possible zipbomb -- Nested Local Entries found"; + for my $loc (sort keys %bomb) + { + my $count = scalar @{ $bomb{$loc} }; + my $outerEntry = $LocalDirectory->getByLocalOffset($loc); + say "# Local Header for '" . $outerEntry->outputFilename . "' at offset " . decimalHex0x($loc) . " has $count nested Local Headers"; - out_V "Last Mod Time", sub { getTime(_dosToUnixTime($_[0])) }; + my $table = new SimpleTable; + $table->addHeaderRow('Offset', 'Filename'); + $table->addDataRow(decimalHex0x($_), $LocalDirectory->getByLocalOffset($_)->outputFilename) + for sort @{ $bomb{$loc} } ; - my $crc = out_V "CRC"; - my $compressedLength = out_V "Compressed Length"; - my $uncompressedLength = out_V "Uncompressed Length"; - my $filenameLength = out_v "Filename Length"; - my $extraLength = out_v "Extra Length"; - my $comment_length = out_v "Comment Length"; - my $disk_start = out_v "Disk Start"; - my $int_file_attrib = out_v "Int File Attributes"; + $table->display(); - out1 "[Bit 0]", $int_file_attrib & 1 ? "1 Text Data" : "0 'Binary Data'"; + delete $cleanCentralEntries{ $_ } + for grep { defined $_ } + map { $CentralDirectory->{byLocalOffset}{$_}{centralHeaderOffset} } + @{ $bomb{$loc} } ; + } + } + } - my $ext_file_attrib = out_V "Ext File Attributes"; - out1 "[Bit 0]", "Read-Only" - if $ext_file_attrib & 0x01 ; - out1 "[Bit 1]", "Hidden" - if $ext_file_attrib & 0x02 ; - out1 "[Bit 2]", "System" - if $ext_file_attrib & 0x04 ; - out1 "[Bit 3]", "Label" - if $ext_file_attrib & 0x08 ; - out1 "[Bit 4]", "Directory" - if $ext_file_attrib & 0x10 ; - out1 "[Bit 5]", "Archive" - if $ext_file_attrib & 0x20 ; - - my $lcl_hdr_offset = out_V "Local Header Offset"; + # Check if contents of local headers match with central headers + # + # When central header encryption is used the local header values are masked (see APPNOTE 6.3.10, sec 4) + # In this usecase the central header will appear to be absent + # + # key fields + # filename, compressed/uncompessed lengths, crc, compression method + { + for my $centralEntry ( sort { $a->centralHeaderOffset() <=> $b->centralHeaderOffset() } values %cleanCentralEntries ) + { + my $localOffset = $centralEntry->localHeaderOffset; + my $localEntry = $LocalDirectory->getByLocalOffset($localOffset); + + next + unless $localEntry; + + state $fields = [ + # field name offset display name stringify + ['filename', ZIP_CD_FILENAME_OFFSET, + 'Filename', undef, ], + ['extractVersion', 7, 'Extract Zip Spec', sub { decimalHex0xUndef($_[0]) . " " . decodeZipVer($_[0]) }, ], + ['generalPurposeFlags', 8, 'General Purpose Flag', \&decimalHex0xUndef, ], + ['compressedMethod', 10, 'Compression Method', sub { decimalHex0xUndef($_[0]) . " " . getcompressionMethodName($_[0]) }, ], + ['lastModDateTime', 12, 'Last Mod Date/Time', sub { decimalHex0xUndef($_[0]) . " " . LastModTime($_[0]) }, ], + ['crc32', 16, 'CRC32', \&decimalHex0xUndef, ], + ['compressedSize', 20, 'Compressed Size', \&decimalHex0xUndef, ], + ['uncompressedSize', 24, 'Uncompressed Size', \&decimalHex0xUndef, ], + + ] ; + + my $table = new SimpleTable; + $table->addHeaderRow('Field Name', 'Central Offset', 'Central Value', 'Local Offset', 'Local Value'); + + for my $data (@$fields) + { + my ($field, $offset, $name, $stringify) = @$data; + # if the local header uses streaming and we are running a scan/walk, the compressed/uncompressed sizes will not be known + my $localValue = $localEntry->{$field} ; + my $centralValue = $centralEntry->{$field}; + + if (($localValue // '-1') ne $centralValue) + { + if ($stringify) + { + $localValue = $stringify->($localValue); + $centralValue = $stringify->($centralValue); + } + + $table->addDataRow($name, + decimalHex0xUndef($centralEntry->centralHeaderOffset() + $offset), + $centralValue, + decimalHex0xUndef($localOffset+$offset), + $localValue); + } + } - my $filename ; - myRead($filename, $filenameLength); - outputFilename($filename); + my $badFields = $table->hasData; + if ($badFields) + { + error undef, "Found $badFields Field Mismatch for Filename '". $centralEntry->outputFilename . "'"; + $table->display(); + } + } + } + } + elsif ($CentralDirectory->exists()) + { + my @messages = "Central Directory exists, but Local Directory not found" ; + push @messages , "Try running with --walk' or '--scan' options" + unless $opt_scan || $opt_walk ; + error undef, @messages; + } + elsif ($LocalDirectory->exists()) + { + if ($CentralDirectory->isEncryptedCD()) + { + warning undef, "Local Directory exists, but Central Directory is encrypted" + } + else + { + error undef, "Local Directory exists, but Central Directory not found" + } - my %ExtraContext = ( CRC => $crc, - LocalHdrOffset => $lcl_hdr_offset, - CompressedLength => $compressedLength); + } - if ($extraLength) + if ($ErrorCount ||$WarningCount || $InfoCount ) { - my @z64 = ($uncompressedLength, $compressedLength, $lcl_hdr_offset, $disk_start); - $ExtraContext{Zip64} = \@z64 ; - $ExtraContext{InCentralDir} = 1; - walkExtra($extraLength, \%ExtraContext); + say "#" + unless $lastWasMessage ; + + say "# Error Count: $ErrorCount" + if $ErrorCount; + say "# Warning Count: $WarningCount" + if $WarningCount; + say "# Info Count: $InfoCount" + if $InfoCount; } - if ($comment_length) + if (@Messages) { - my $comment ; - myRead($comment, $comment_length); - out $comment, "Comment", "'". $comment . "'"; + my $count = scalar @Messages ; + say "#\nWARNINGS"; + say "# * $_\n" for @Messages ; } - - $SEEN->save("CENTRAL HEADER ref Local #" . sprintf("%X", $CentralHeaderCount), $filename, \%ExtraContext) ; + say "#\n# Done"; } -sub decodeZipVer +sub checkForBadlyFormedDataDescriptor { - my $ver = shift ; - - my $sHi = int($ver /10) ; - my $sLo = $ver % 10 ; + my $lastHeader = shift; + my $delta = shift // 0; - #out1 "Zip Spec", "$sHi.$sLo"; - "$sHi.$sLo"; -} + # check size of delta - a DATA HDR without a signature can only be + # 12 bytes for 32-bit + # 20 bytes for 64-bit -sub decodeOS -{ - my $ver = shift ; + my $here = $FH->tell(); - $OS_Lookup{$ver} || "Unknown" ; -} + my $localEntry = $lastHeader->{entry}; -sub Zip64EndCentralHeader -{ - my $signature = shift ; - my $data = shift ; + return 0 + unless $opt_scan || $opt_walk ; - print "\n"; - out $data, "ZIP64 END CENTRAL DIR RECORD", Value_V($signature); + # delta can be the actual payload + a data descriptor without a sig - my $buff; - myRead($buff, 8); + my $signature = unpack "V", peekAtOffset($here + $delta, 4); - out $buff, "Size of record", unpackValue_Q($buff); + if ($signature == ZIP_DATA_HDR_SIG) + { + return 0; + } - my $size = unpack "Q<", $buff; + my $cl32 = unpack "V", peekAtOffset($here + $delta - 8, 4); + my $cl64 = unpack "Q<", peekAtOffset($here + $delta - 16, 8); - out_C "Created Zip Spec", \&decodeZipVer; - out_C "Created OS", \&decodeOS; - out_C "Extract Zip Spec", \&decodeZipVer; - out_C "Extract OS", \&decodeOS; - out_V "Number of this disk"; - out_V "Central Dir Disk no"; - out_Q "Entries in this disk"; - out_Q "Total Entries"; - out_Q "Size of Central Dir"; - out_Q "Offset to Central dir"; + if ($cl32 == $delta - 12) + { + return 12; + } - # TODO - - if ($size != 44) + if ($cl64 == $delta - 20) { - push @Messages, "Unsupported Size field in Zip64EndCentralHeader: should be 44, got $size\n" + return 20 ; } + + return 0; } -sub Zip64EndCentralLocator +sub BadlyFormedDataDescriptor { - my $signature = shift ; - my $data = shift ; - - print "\n"; - out $data, "ZIP64 END CENTRAL DIR LOCATOR", Value_V($signature); + my $lastHeader= shift; + my $delta = shift; - out_V "Central Dir Disk no"; - out_Q "Offset to Central dir"; - out_V "Total no of Disks"; -} + # check size of delta - a DATA HDR without a signature can only be + # 12 bytes for 32-bit + # 20 bytes for 64-bit -sub EndCentralHeader -{ - my $signature = shift ; - my $data = shift ; + my $here = $FH->tell(); - print "\n"; - out $data, "END CENTRAL HEADER", Value_V($signature); + my $localEntry = $lastHeader->{entry}; + my $compressedSize = $lastHeader->{payloadLength} ; - out_v "Number of this disk"; - out_v "Central Dir Disk no"; - out_v "Entries in this disk"; - out_v "Total Entries"; - out_V "Size of Central Dir"; - out_V "Offset to Central Dir"; - my $comment_length = out_v "Comment Length"; + my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG); - if ($comment_length) + if ($opt_scan || $opt_walk) { - my $comment ; - myRead($comment, $comment_length); - out $comment, "Comment", "'$comment'"; - } -} + # delta can be the actual payload + a data descriptor without a sig -sub DataHeader -{ - my $signature = shift ; - my $data = shift ; + if ($lastHeader->{'gotDataDescriptorSize'} == 12) + { + # seekTo($FH->tell() + $delta - 12) ; - print "\n"; - out $data, "STREAMING DATA HEADER", Value_V($signature); + # outSomeData($delta - 12, "PAYLOAD", $opt_Redact) ; + + print "\n"; + out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG); + + error $FH->tell(), "Missimg $sigName Signature"; + $localEntry->crc32( out_V "CRC"); + $localEntry->compressedSize( out_V "Compressed Size"); + $localEntry->uncompressedSize( out_V "Uncompressed Size"); + + if ($localEntry->zip64) + { + error $here, "'$sigName': expected 64-bit values, got 32-bit"; + } + + return $FH->tell(); + } + + if ($lastHeader->{'gotDataDescriptorSize'} == 20) + { + # seekTo($FH->tell() + $delta - 20) ; + + # outSomeData($delta - 20, "PAYLOAD", $opt_Redact) ; + + print "\n"; + out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG); + + error $FH->tell(), "Missimg $sigName Signature"; + $localEntry->crc32( out_V "CRC"); + $localEntry->compressedSize( out_Q "Compressed Size"); + $localEntry->uncompressedSize( out_Q "Uncompressed Size"); + + if (! $localEntry->zip64) + { + error $here, "'$sigName': expected 32-bit values, got 64-bit"; + } + + return $FH->tell(); + } + + error 0, "MISSING $sigName"; - out_V "CRC"; + seekTo($here); + return 0; + } - if ($ZIP64) + my $cdEntry = $localEntry->getCdEntry; + + if ($delta == 12) { - out_Q "Compressed Length" ; - out_Q "Uncompressed Length" ; + $FH->seek($lastHeader->{payloadOffset} + $lastHeader->{payloadLength}, SEEK_SET) ; + + my $cl = unpack "V", peekAtOffset($FH->tell() + 4, 4); + if ($cl == $compressedSize) + { + print "\n"; + out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG); + + error $FH->tell(), "Missimg $sigName Signature"; + $localEntry->crc32( out_V "CRC"); + $localEntry->compressedSize( out_V "Compressed Size"); + $localEntry->uncompressedSize( out_V "Uncompressed Size"); + + if ($localEntry->zip64) + { + error $here, "'$sigName': expected 64-bit values, got 32-bit"; + } + + return $FH->tell(); + } } - else + + if ($delta == 20) { - out_V "Compressed Length" ; - out_V "Uncompressed Length" ; + $FH->seek($lastHeader->{payloadOffset} + $lastHeader->{payloadLength}, SEEK_SET) ; + + my $cl = unpack "Q<", peekAtOffset($FH->tell() + 4, 8); + + if ($cl == $compressedSize) + { + print "\n"; + out1 "Missing $sigName Signature", Value_V(ZIP_DATA_HDR_SIG); + + error $FH->tell(), "Missimg $sigName Signature"; + $localEntry->crc32( out_V "CRC"); + $localEntry->compressedSize( out_Q "Compressed Size"); + $localEntry->uncompressedSize( out_Q "Uncompressed Size"); + + if (! $localEntry->zip64 && ( $cdEntry && ! $cdEntry->zip64)) + { + error $here, "'$sigName': expected 32-bit values, got 64-bit"; + } + + return $FH->tell(); + } } + + seekTo($here); + + error $here, "Missing $sigName"; + return 0; } +sub getcompressionMethodName +{ + my $id = shift ; + " '" . ($ZIP_CompressionMethods{$id} || "Unknown Method") . "'" ; +} -sub GeneralPurposeBits +sub compressionMethod { - my $method = shift; - my $gp = shift; + my $id = shift ; + Value_v($id) . getcompressionMethodName($id); +} - out1 "[Bit 0]", "1 'Encryption'" if $gp & ZIP_GP_FLAG_ENCRYPTED_MASK; +sub LocalHeader +{ + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; - my %lookup = ( - 0 => "Normal Compression", - 1 => "Maximum Compression", - 2 => "Fast Compression", - 3 => "Super Fast Compression"); + my $locHeaderOffset = $FH->tell() -4 ; + ++ $LocalHeaderCount; + print "\n"; + out $data, "LOCAL HEADER #$LocalHeaderCount" , Value_V($signature); - if ($method == ZIP_CM_DEFLATE) + need 26, Signatures::name($signature); + + my $buffer; + my $orphan = 0; + + my ($loc, $CDcompressedSize, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset) ; + my $CentralEntryExists = $CentralDirectory->localOffset($startRecordOffset); + my $localEntry = LocalDirectoryEntry->new(); + + my $cdEntry; + + if (! $opt_scan && ! $opt_walk && $CentralEntryExists) { - my $mid = ($gp >> 1) & 0x03 ; + $cdEntry = $CentralDirectory->getByLocalOffset($startRecordOffset); - out1 "[Bits 1-2]", "$mid '$lookup{$mid}'"; + if (! $cdEntry) + { + out1 "Orphan Entry: No matching central directory" ; + $orphan = 1 ; + } + + $cdZip64 = $cdEntry->zip64ExtraPresent; + $zip64Sizes = $cdEntry->zip64SizesPresent; + $cdEntryOffset = $cdEntry->centralHeaderOffset ; + $localEntry->addCdEntry($cdEntry) ; + + if ($cdIndex && $cdIndex != $LocalHeaderCount) + { + # fatal undef, "$cdIndex != $LocalHeaderCount" + } } - if ($method == ZIP_CM_LZMA) + my $extractVer = out_C "Extract Zip Spec", \&decodeZipVer; + out_C "Extract OS", \&decodeOS; + + my ($bgp, $gpFlag) = read_v(); + my ($bcm, $compressedMethod) = read_v(); + + out $bgp, "General Purpose Flag", Value_v($gpFlag) ; + GeneralPurposeBits($compressedMethod, $gpFlag); + my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ; + my $streaming = $gpFlag & ZIP_GP_FLAG_STREAMING_MASK ; + $localEntry->languageEncodingFlag($LanguageEncodingFlag) ; + + out $bcm, "Compression Method", compressionMethod($compressedMethod) ; + info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2) + if ! defined $ZIP_CompressionMethods{$compressedMethod} ; + + my $lastMod = out_V "Last Mod Date/Time", sub { LastModTime($_[0]) }; + + my $crc = out_V "CRC"; + warning $FH->tell() - 4, "CRC field should be zero when streaming is enabled" + if $streaming && $crc != 0 ; + + my $compressedSize = out_V "Compressed Size"; + # warning $FH->tell(), "Compressed Size should be zero when streaming is enabled"; + + my $uncompressedSize = out_V "Uncompressed Size"; + # warning $FH->tell(), "Uncompressed Size should be zero when streaming is enabled"; + + my $filenameLength = out_v "Filename Length"; + + if ($filenameLength == 0) { - if ($gp & ZIP_GP_FLAG_LZMA_EOS_PRESENT) { - out1 "[Bit 1]", "1 'LZMA EOS Marker Present'" ; - } - else { - out1 "[Bit 1]", "0 'LZMA EOS Marker Not Present'" ; + info $FH->tell()- 2, "Zero Length filename"; + } + + my $extraLength = out_v "Extra Length"; + + my $filename = ''; + if ($filenameLength) + { + need $filenameLength, Signatures::name($signature), 'Filename'; + + myRead(my $raw_filename, $filenameLength); + $localEntry->filename($raw_filename) ; + $filename = outputFilename($raw_filename, $LanguageEncodingFlag); + $localEntry->outputFilename($filename); + + # APPNOTE 6.3.10, sec 4.3.8 + warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload" + if ! $streaming && $filename =~ m#/$# && $uncompressedSize ; + } + + $localEntry->localHeaderOffset($locHeaderOffset) ; + $localEntry->offsetStart($locHeaderOffset) ; + $localEntry->compressedSize($compressedSize) ; + $localEntry->uncompressedSize($uncompressedSize) ; + $localEntry->extractVersion($extractVer); + $localEntry->generalPurposeFlags($gpFlag); + $localEntry->lastModDateTime($lastMod); + $localEntry->crc32($crc) ; + $localEntry->zip64ExtraPresent($cdZip64) ; + $localEntry->zip64SizesPresent($zip64Sizes) ; + + $localEntry->compressedMethod($compressedMethod) ; + $localEntry->streamed($gpFlag & ZIP_GP_FLAG_STREAMING_MASK) ; + + $localEntry->std_localHeaderOffset($locHeaderOffset + $PREFIX_DELTA) ; + $localEntry->std_compressedSize($compressedSize) ; + $localEntry->std_uncompressedSize($uncompressedSize) ; + $localEntry->std_diskNumber(0) ; + + if ($extraLength) + { + need $extraLength, Signatures::name($signature), 'Extra'; + walkExtra($extraLength, $localEntry); + } + + my @msg ; + # if ($cdZip64 && ! $ZIP64) + # { + # # Central directory said this was Zip64 + # # some zip files don't have the Zip64 field in the local header + # # seems to be a streaming issue. + # push @msg, "Missing Zip64 extra field in Local Header #$hexHdrCount\n"; + + # if (! $zip64Sizes) + # { + # # Central has a ZIP64 entry that doesn't have sizes + # # Local doesn't have a Zip 64 at all + # push @msg, "Unzip may complain about 'overlapped components' #$hexHdrCount\n"; + # } + # else + # { + # $ZIP64 = 1 + # } + # } + + + my $minizip_encrypted = $localEntry->minizip_secure; + my $pk_encrypted = ($gpFlag & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK) && $compressedMethod != 99 && ! $minizip_encrypted; + + # Detecting PK strong encryption from a local header is a bit convoluted. + # Cannot just use ZIP_GP_FLAG_ENCRYPTED_CD because minizip also uses this bit. + # so jump through some hoops + # extract ver is >= 5.0' + # all the encryption flags are set in gpflags + # TODO - add zero lengths for crc, compresssed & uncompressed + + if (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == ZIP_GP_FLAG_ALL_ENCRYPT && $extractVer >= 0x32 ) + { + $CentralDirectory->setPkEncryptedCD() + } + + my $size = 0; + + # If no CD scanned, get compressed Size from local header. + # Zip64 extra field takes priority + my $cdl = defined $cdEntry + ? $cdEntry->compressedSize() + : undef; + + $CDcompressedSize = $localEntry->compressedSize ; + $CDcompressedSize = $cdl + if defined $cdl && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK; + + my $cdu = defined $CentralDirectory->{byLocalOffset}{$locHeaderOffset} + ? $CentralDirectory->{byLocalOffset}{$locHeaderOffset}{uncompressedSize} + : undef; + my $CDuncompressedSize = $localEntry->uncompressedSize ; + + $CDuncompressedSize = $cdu + if defined $cdu && $gpFlag & ZIP_GP_FLAG_STREAMING_MASK; + + my $fullCompressedSize = $CDcompressedSize; + + my $payloadOffset = $FH->tell(); + $localEntry->payloadOffset($payloadOffset) ; + $localEntry->offsetEnd($payloadOffset + $fullCompressedSize -1) ; + + if ($CDcompressedSize) + { + # check if enough left in file for the payload + my $available = $FILELEN - $FH->tell; + if ($available < $CDcompressedSize ) + { + error $FH->tell, + "file truncated while reading 'PAYLOAD'", + "Expected " . decimalHex0x($CDcompressedSize) . " bytes, but only " . decimalHex0x($available) . " available" ; + + $CDcompressedSize = $available; } } - if ($method == ZIP_CM_IMPLODE) # Imploding + if ($compressedMethod == 99 && $localEntry->aesValid) # AES Encryption { - out1 "[Bit 1]", ($gp & (1 << 1) ? "1 '8k" : "0 '4k") . " Sliding Dictionary'" ; - out1 "[Bit 2]", ($gp & (2 << 1) ? "1 '3" : "0 '2" ) . " Shannon-Fano Trees'" ; + $CDcompressedSize -= printAes($localEntry) } + elsif (($gpFlag & ZIP_GP_FLAG_ALL_ENCRYPT) == 0) + { + if ($compressedMethod == ZIP_CM_LZMA) + { - out1 "[Bit 3]", "1 'Streamed'" if $gp & ZIP_GP_FLAG_STREAMING_MASK; - out1 "[Bit 4]", "1 'Enhanced Deflating'" if $gp & 1 << 4; - out1 "[Bit 5]", "1 'Compressed Patched'" if $gp & 1 << 5 ; - out1 "[Bit 6]", "1 'Strong Encryption'" if $gp & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK; - out1 "[Bit 11]", "1 'Language Encoding'" if $gp & ZIP_GP_FLAG_LANGUAGE_ENCODING; - out1 "[Bit 12]", "1 'Pkware Enhanced Compression'" if $gp & 1 <<12 ; - out1 "[Bit 13]", "1 'Encrypted Central Dir'" if $gp & 1 <<13 ; + $size = printLzmaProperties() + } - return (); -} + $CDcompressedSize -= $size; + } + elsif ($pk_encrypted) + { + $CDcompressedSize -= DecryptionHeader(); + } + if ($CDcompressedSize) { -sub seekSet -{ - my $fh = $_[0] ; - my $size = $_[1]; + if ($compressedMethod == 92 && $CDcompressedSize == 20) { + # Payload for a Reference is the SHA-1 hash of the uncompressed content + myRead(my $sha1, 20); + out $sha1, "PAYLOAD", "SHA-1 Hash: " . hexDump($sha1); + } + elsif ($compressedMethod == 99 && $localEntry->aesValid ) { + outSomeData($CDcompressedSize, "PAYLOAD", $opt_Redact) ; + my $auth ; + myRead($auth, 10); + out $auth, "AES Auth", hexDump16($auth); + } + else { + outSomeData($CDcompressedSize, "PAYLOAD", $opt_Redact) ; + } + } - use Fcntl qw(SEEK_SET); - seek($fh, $size, SEEK_SET); + print "WARNING: $_" + for @msg; + push @Messages, @msg ; + + $LocalDirectory->addEntry($localEntry); + + return { + 'localHeader' => 1, + 'streamed' => $gpFlag & ZIP_GP_FLAG_STREAMING_MASK, + 'offset' => $startRecordOffset, + 'length' => $FH->tell() - $startRecordOffset, + 'payloadLength' => $fullCompressedSize, + 'payloadOffset' => $payloadOffset, + 'entry' => $localEntry, + } ; } -sub skip +use constant Pack_ZIP_DIGITAL_SIGNATURE_SIG => pack("V", ZIP_DIGITAL_SIGNATURE_SIG); + +sub findDigitalSignature { - my $fh = $_[0] ; - my $size = $_[1]; + my $cdSize = shift; - use Fcntl qw(SEEK_CUR); - seek($fh, $size, SEEK_CUR); + my $here = $FH->tell(); -} + my $data ; + myRead($data, $cdSize); + seekTo($here); -sub myRead + # find SIG + my $ix = index($data, Pack_ZIP_DIGITAL_SIGNATURE_SIG); + if ($ix > -1) + { + # check size of signature meaans it is directly after the encrypted CD + my $sigSize = unpack "v", substr($data, $ix+4, 2); + if ($ix + 4 + 2 + $sigSize == $cdSize) + { + # return size of digital signature record + return 4 + 2 + $sigSize ; + } + } + + return 0; +} + +sub displayEncryptedCD { - my $got = \$_[0] ; - my $size = $_[1]; + # First thing in the encrypted CD is the Decryption Header + my $decryptHeaderSize = DecryptionHeader(1); - my $wantSize = $size; - $$got = ''; + # Check for digital signature record in the CD + # It needs to be the very last thing in the CD - if ($size == 0) + my $delta = deltaToNextSignature(); + print "\n"; + outSomeData($delta, "ENCRYPTED CENTRAL DIRECTORY") + if $delta; +} + +sub DecryptionHeader +{ + # APPNOTE 6.3.10, sec 7.2.4 + + # -Decryption Header: + # Value Size Description + # ----- ---- ----------- + # IVSize 2 bytes Size of initialization vector (IV) + # IVData IVSize Initialization vector for this file + # Size 4 bytes Size of remaining decryption header data + # Format 2 bytes Format definition for this record + # AlgID 2 bytes Encryption algorithm identifier + # Bitlen 2 bytes Bit length of encryption key + # Flags 2 bytes Processing flags + # ErdSize 2 bytes Size of Encrypted Random Data + # ErdData ErdSize Encrypted Random Data + # Reserved1 4 bytes Reserved certificate processing data + # Reserved2 (var) Reserved for certificate processing data + # VSize 2 bytes Size of password validation data + # VData VSize-4 Password validation data + # VCRC32 4 bytes Standard ZIP CRC32 of password validation data + + my $central = shift ; + + if ($central) { - return ; - } + print "\n"; + out "", "CENTRAL HEADER DECRYPTION RECORD"; - if ($size > 0) + } + else { - my $buff ; - my $status = $FH->read($buff, $size); - return $status - if $status < 0; - $$got .= $buff ; + print "\n"; + out "", "DECRYPTION HEADER RECORD"; } - my $len = length $$got; - die "Truncated file (got $len, wanted $wantSize): $!\n" - if length $$got != $wantSize; -} + my $bytecount = 2; + my $IVSize = out_v "IVSize"; + outHexdump($IVSize, "IVData"); + $bytecount += $IVSize; + my $Size = out_V "Size"; + $bytecount += $Size + 4; + out_v "Format"; + out_v "AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ; + out_v "BitLen"; + out_v "Flags", sub { $FlagsLookup{ $_[0] } // "Reserved for certificate processing" } ; -sub walkExtra + my $ErdSize = out_v "ErdSize"; + outHexdump($ErdSize, "ErdData"); + + my $Reserved1_RCount = out_V "RCount"; + Reserved2($Reserved1_RCount); + + my $VSize = out_v "VSize"; + outHexdump($VSize-4, "VData"); + + out_V "VCRC32"; + + return $bytecount ; +} + +sub Reserved2 { - my $XLEN = shift; - my $context = shift; + # APPNOTE 6.3.10, sec 7.4.3 & 7.4.4 - my $buff ; - my $offset = 0 ; + my $recipients = shift; - my $id; - my $subLen; - my $payload ; + return 0 + if $recipients == 0; - my $count = 0 ; + out_v "HashAlg", sub { $HashAlgLookup{ $_[0] } // "Unknown algorithm" } ; + my $HSize = out_v "HSize" ; - while ($offset < $XLEN) { + my $ix = 1; + for (0 .. $recipients-1) + { + my $hex = sprintf("Key #%X", $ix) ; + my $RESize = out_v "RESize $hex"; - ++ $count; + outHexdump($HSize, "REHData $hex"); + outHexdump($RESize - $HSize, "REKData $hex"); - # Detect if there is not enough data for an extra ID and length. - # Android zipalign and zipflinger are prime candidates for these - # non-standard extra sub-fields. - my $remaining = $XLEN - $offset; - if ($remaining < ZIP_EXTRA_SUBFIELD_HEADER_SIZE) { - # There is not enough left. - # Consume whatever is there and return so parsing - # can continue. - myRead($payload, $remaining); - my $data = hexDump($payload); + ++ $ix; + } +} - if ($payload =~ /^\x00+$/) +sub redactData +{ + my $data = shift; + + # Redact everything apart from directory seperators + $data =~ s(.)(X)g + if $opt_Redact; + + return $data; +} + +sub redactFilename +{ + my $filename = shift; + + # Redact everything apart from directory seperators + $filename =~ s(.)(X)g + if $opt_Redact; + + return $filename; +} + +sub validateFilename +{ + my $filename = shift ; + + return "Zero length filename" + if $filename eq '' ; + + # TODO + # - check length of filename + # getconf NAME_MAX . and getconf PATH_MAX . on Linux + + # Start with APPNOTE restrictions + + # APPNOTE 6.3.10, sec 4.4.17.1 + # + # No absolute path + # No backslash delimeters + # No drive letters + + return "Filename must not be an absolute path" + if $filename =~ m#^/#; + + return ["Backslash detected in filename", "Possible Windows path."] + if $filename =~ m#\\#; + + return "Windows Drive Letter '$1' not allowed in filename" + if $filename =~ /^([a-z]:)/i ; + + # Slip Vulnerability with use of ".." in a relative path + # https://security.snyk.io/research/zip-slip-vulnerability + return ["Use of '..' in filename is a Zip Slip Vulnerability", + "See https://security.snyk.io/research/zip-slip-vulnerability" ] + if $filename =~ m#^\.\./# || $filename =~ m#/\.\./# || $filename =~ m#/\.\.# ; + + # Cannot have "." or ".." as the full filename + return "Use of current-directory filename '.' may not unzip correctly" + if $filename eq '.' ; + + return "Use of parent-directory filename '..' may not unzip correctly" + if $filename eq '..' ; + + # Portability (mostly with Windows) + + # see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + state $badDosFilename = join '|', map { quotemeta } + qw(CON PRN AUX NUL + COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 + LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9 + ) ; + return "Portability Issue: '$1' is a reserved Windows device name" + if $filename =~ /^($badDosFilename)$/io ; + + # Can't have the device name with an extension either + return "Portability Issue: '$1' is a reserved Windows device name" + if $filename =~ /^($badDosFilename)\./io ; + + state $illegal_windows_chars = join '|', map { quotemeta } qw( < > : " | ? * ); + return "Portability Issue: Windows filename cannot contain '$1'" + if $filename =~ /($illegal_windows_chars)/o ; + + return "Portability Issue: Null character '\\x00' is not allowed in a Windows or Linux filename" + if $filename =~ /\x00/ ; + + return sprintf "Portability Issue: Control character '\\x%02X' is not allowed in a Windows filename", ord($1) + if $filename =~ /([\x00-\x1F])/ ; + + return undef; +} + +sub getOutputFilename +{ + my $raw_filename = shift; + my $LanguageEncodingFlag = shift; + my $message = shift // "Filename"; + + my $filename ; + my $decoded_filename; + + if ($raw_filename eq '') + { + if ($message eq 'Filename') + { + warning $FH->tell() , + "Filename ''", + "Zero Length Filename" ; + } + + return '', '', 0; + } + elsif ($opt_Redact) + { + return redactFilename($raw_filename), '', 0 ; + } + else + { + $decoded_filename = TextEncoding::decode($raw_filename, $message, $LanguageEncodingFlag) ; + $filename = TextEncoding::encode($decoded_filename, $message, $LanguageEncodingFlag) ; + } + + return $filename, $decoded_filename, $filename ne $raw_filename ; +} + +sub outputFilename +{ + my $raw_filename = shift; + my $LanguageEncodingFlag = shift; + my $message = shift // "Filename"; + + my ($filename, $decoded_filename, $modified) = getOutputFilename($raw_filename, $LanguageEncodingFlag); + + out $raw_filename, $message, "'". $filename . "'"; + + if (! $opt_Redact && TextEncoding::debugEncoding()) + { + # use Devel::Peek; + # print "READ " ; Dump($raw_filename); + # print "INTERNAL " ; Dump($decoded_filename); + # print "OUTPUT " ; Dump($filename); + + debug $FH->tell() - length($raw_filename), + "$message Encoding Change" + if $modified ; + + # use Unicode::Normalize; + # my $NormaizedForm ; + # if (defined $decoded_filename) + # { + # $NormaizedForm .= Unicode::Normalize::checkNFD $decoded_filename ? 'NFD ' : ''; + # $NormaizedForm .= Unicode::Normalize::checkNFC $decoded_filename ? 'NFC ' : ''; + # $NormaizedForm .= Unicode::Normalize::checkNFKD $decoded_filename ? 'NFKD ' : ''; + # $NormaizedForm .= Unicode::Normalize::checkNFKC $decoded_filename ? 'NFKC ' : ''; + # $NormaizedForm .= Unicode::Normalize::checkFCD $decoded_filename ? 'FCD ' : ''; + # $NormaizedForm .= Unicode::Normalize::checkFCC $decoded_filename ? 'FCC ' : ''; + # } + + debug $FH->tell() - length($raw_filename), + "Encoding Debug for $message", + "Octets Read from File [$raw_filename][" . length($raw_filename). "] [" . charDump2($raw_filename) . "]", + "Via Unicode Codepoints [$decoded_filename][" . length($decoded_filename) . "] [" . charDump($decoded_filename) . "]", + # "Unicode Normalization $NormaizedForm", + "Octets Written [$filename][" . length($filename). "] [" . charDump2($filename) . "]"; + } + + if ($message eq 'Filename' && $opt_want_warning_mesages) + { + # Check for bad, unsafe & not portable filenames + my $v = validateFilename($decoded_filename); + + if ($v) + { + my @v = ref $v eq 'ARRAY' + ? @$v + : $v; + + warning $FH->tell() - length($raw_filename), + "Filename '$filename'", + @v + } + } + + return $filename; +} + +sub CentralHeader +{ + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + my $cdEntryOffset = $FH->tell() - 4 ; + + ++ $CentralHeaderCount; + + print "\n"; + out $data, "CENTRAL HEADER #$CentralHeaderCount", Value_V($signature); + my $buffer; + + need 42, Signatures::name($signature); + + out_C "Created Zip Spec", \&decodeZipVer; + my $made_by = out_C "Created OS", \&decodeOS; + my $extractVer = out_C "Extract Zip Spec", \&decodeZipVer; + out_C "Extract OS", \&decodeOS; + + my ($bgp, $gpFlag) = read_v(); + my ($bcm, $compressedMethod) = read_v(); + + my $cdEntry = CentralDirectoryEntry->new($cdEntryOffset); + + out $bgp, "General Purpose Flag", Value_v($gpFlag) ; + GeneralPurposeBits($compressedMethod, $gpFlag); + my $LanguageEncodingFlag = $gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING ; + $cdEntry->languageEncodingFlag($LanguageEncodingFlag) ; + + out $bcm, "Compression Method", compressionMethod($compressedMethod) ; + info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2) + if ! defined $ZIP_CompressionMethods{$compressedMethod} ; + + my $lastMod = out_V "Last Mod Date/Time", sub { LastModTime($_[0]) }; + + my $crc = out_V "CRC"; + my $compressedSize = out_V "Compressed Size"; + my $std_compressedSize = $compressedSize; + my $uncompressedSize = out_V "Uncompressed Size"; + my $std_uncompressedSize = $uncompressedSize; + my $filenameLength = out_v "Filename Length"; + if ($filenameLength == 0) + { + info $FH->tell()- 2, "Zero Length filename"; + } + my $extraLength = out_v "Extra Length"; + my $comment_length = out_v "Comment Length"; + my $disk_start = out_v "Disk Start"; + my $std_disk_start = $disk_start; + + my $int_file_attrib = out_v "Int File Attributes"; + out1 "[Bit 0]", $int_file_attrib & 1 ? "1 'Text Data'" : "0 'Binary Data'"; + out1 "[Bits 1-15]", Value_v($int_file_attrib & 0xFE) . " 'Unknown'" + if $int_file_attrib & 0xFE ; + + my $ext_file_attrib = out_V "Ext File Attributes"; + + { + # MS-DOS Attributes are bottom two bytes + my $dos_attrib = $ext_file_attrib & 0xFFFF; + + # See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + # and https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/65e0c225-5925-44b0-8104-6b91339c709f + + out1 "[Bit 0]", "Read-Only" + if $dos_attrib & 0x0001 ; + out1 "[Bit 1]", "Hidden" + if $dos_attrib & 0x0002 ; + out1 "[Bit 2]", "System" + if $dos_attrib & 0x0004 ; + out1 "[Bit 3]", "Label" + if $dos_attrib & 0x0008 ; + out1 "[Bit 4]", "Directory" + if $dos_attrib & 0x0010 ; + out1 "[Bit 5]", "Archive" + if $dos_attrib & 0x0020 ; + out1 "[Bit 6]", "Device" + if $dos_attrib & 0x0040 ; + out1 "[Bit 7]", "Normal" # Exe? + if $dos_attrib & 0x0080 ; + out1 "[Bit 8]", "Offline" + if $dos_attrib & 0x0100 ; + out1 "[Bit 9]", "Not Indexed" + if $dos_attrib & 0x0200 ; + out1 "[Bit 10]", "Encrypted" + if $dos_attrib & 0x0400 ; + out1 "[Bits 11-15]", Value_v($dos_attrib & 0xf800) . " 'Unknown DOS attrib'" + if $dos_attrib & 0xf800 ; + } + + my $native_attrib = ($ext_file_attrib >> 16 ) & 0xFFFF; + + if ($made_by == 3) # Unix + { + + state $mask = { + 0 => '---', + 1 => '--x', + 2 => '-w-', + 3 => '-wx', + 4 => 'r--', + 5 => 'r-x', + 6 => 'rw-', + 7 => 'rwx', + } ; + + my $rwx = ($native_attrib & 0777); + + if ($rwx) + { + my $output = ''; + $output .= $mask->{ ($rwx >> 6) & 07 } ; + $output .= $mask->{ ($rwx >> 3) & 07 } ; + $output .= $mask->{ ($rwx >> 0) & 07 } ; + + out1 "[Bits 16-24]", Value_v($rwx) . " 'Unix attrib: $output'" ; + out1 "[Bit 25]", "1 'Sticky'" + if $rwx & 0x200 ; + out1 "[Bit 26]", "1 'Set GID'" + if $rwx & 0x400 ; + out1 "[Bit 27]", "1 'Set UID'" + if $rwx & 0x800 ; + + my $not_rwx = (($native_attrib >> 12) & 0xF); + if ($not_rwx) + { + state $masks = { + 0x0C => 'Socket', # 0x0C 0b1100 + 0x0A => 'Symbolic Link', # 0x0A 0b1010 + 0x08 => 'Regular File', # 0x08 0b1000 + 0x06 => 'Block Device', # 0x06 0b0110 + 0x04 => 'Directory', # 0x04 0b0100 + 0x02 => 'Character Device', # 0x02 0b0010 + 0x01 => 'FIFO', # 0x01 0b0001 + }; + + my $got = $masks->{$not_rwx} // 'Unknown Unix attrib' ; + out1 "[Bits 28-31]", Value_C($not_rwx) . " '$got'" + } + } + } + elsif ($native_attrib) + { + out1 "[Bits 24-31]", Value_v($native_attrib) . " 'Unknown attributes for OS ID $made_by'" + } + + my ($d, $locHeaderOffset) = read_V(); + my $out = Value_V($locHeaderOffset); + my $std_localHeaderOffset = $locHeaderOffset; + + if ($locHeaderOffset != MAX32) + { + testPossiblePrefix($locHeaderOffset, ZIP_LOCAL_HDR_SIG); + if ($PREFIX_DELTA) + { + $out .= " [Actual Offset is " . Value_V($locHeaderOffset + $PREFIX_DELTA) . "]" + } + } + + out $d, "Local Header Offset", $out; + + if ($locHeaderOffset != MAX32) + { + my $commonMessage = "'Local Header Offset' field in '" . Signatures::name($signature) . "' is invalid"; + $locHeaderOffset = checkOffsetValue($locHeaderOffset, $startRecordOffset, 0, $commonMessage, $startRecordOffset + 42, ZIP_LOCAL_HDR_SIG) ; + } + + my $filename = ''; + if ($filenameLength) + { + need $filenameLength, Signatures::name($signature), 'Filename'; + + myRead(my $raw_filename, $filenameLength); + $cdEntry->filename($raw_filename) ; + $filename = outputFilename($raw_filename, $LanguageEncodingFlag); + $cdEntry->outputFilename($filename); + + # APPNOTE 6.3.10, sec 4.3.8 + warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload" + if $filename =~ m#/$# && $uncompressedSize ; + } + + $cdEntry->centralHeaderOffset($cdEntryOffset) ; + $cdEntry->localHeaderOffset($locHeaderOffset) ; + $cdEntry->compressedSize($compressedSize) ; + $cdEntry->uncompressedSize($uncompressedSize) ; + $cdEntry->zip64ExtraPresent(undef) ; #$cdZip64; ### FIX ME + $cdEntry->zip64SizesPresent(undef) ; # $zip64Sizes; ### FIX ME + $cdEntry->extractVersion($extractVer); + $cdEntry->generalPurposeFlags($gpFlag); + $cdEntry->compressedMethod($compressedMethod) ; + $cdEntry->lastModDateTime($lastMod); + $cdEntry->crc32($crc) ; + $cdEntry->inCentralDir(1) ; + + $cdEntry->std_localHeaderOffset($std_localHeaderOffset) ; + $cdEntry->std_compressedSize($std_compressedSize) ; + $cdEntry->std_uncompressedSize($std_uncompressedSize) ; + $cdEntry->std_diskNumber($std_disk_start) ; + + if ($extraLength) + { + need $extraLength, Signatures::name($signature), 'Extra'; + + walkExtra($extraLength, $cdEntry); + } + + # $cdEntry->endCentralHeaderOffset($FH->tell() - 1); + + + if ($comment_length) + { + need $comment_length, Signatures::name($signature), 'Comment'; + + my $comment ; + myRead($comment, $comment_length); + outputFilename $comment, $LanguageEncodingFlag, "Comment"; + $cdEntry->comment($comment); + } + + $cdEntry->offsetStart($cdEntryOffset) ; + $cdEntry->offsetEnd($FH->tell() - 1) ; + + $CentralDirectory->addEntry($cdEntry); + + return { 'encapsulated' => $cdEntry ? $cdEntry->encapsulated() : 0}; +} + +sub decodeZipVer +{ + my $ver = shift ; + + my $sHi = int($ver /10) ; + my $sLo = $ver % 10 ; + + "$sHi.$sLo"; +} + +sub decodeOS +{ + my $ver = shift ; + + $OS_Lookup{$ver} || "Unknown" ; +} + +sub Zip64EndCentralHeader +{ + # Extra ID is 0x0001 + + # APPNOTE 6.3.10, section 4.3.14, 7.3.3, 7.3.4 & APPENDIX C + + # TODO - APPNOTE allows an extensible data sector at end of this record (see APPNOTE 6.3.10, section 4.3.14.4) + # The code below does NOT take this into account. + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + print "\n"; + out $data, "ZIP64 END CENTRAL DIR RECORD", Value_V($signature); + + need 8, Signatures::name($signature); + + my $size = out_Q "Size of record"; + + need $size, Signatures::name($signature); + + out_C "Created Zip Spec", \&decodeZipVer; + out_C "Created OS", \&decodeOS; + my $extractSpec = out_C "Extract Zip Spec", \&decodeZipVer; + out_C "Extract OS", \&decodeOS; + my $diskNumber = out_V "Number of this disk"; + my $cdDiskNumber = out_V "Central Dir Disk no"; + my $entriesOnThisDisk = out_Q "Entries in this disk"; + my $totalEntries = out_Q "Total Entries"; + my $centralDirSize = out_Q "Size of Central Dir"; + + my ($d, $centralDirOffset) = read_Q(); + my $out = Value_Q($centralDirOffset); + testPossiblePrefix($centralDirOffset, ZIP_CENTRAL_HDR_SIG); + + $out .= " [Actual Offset is " . Value_Q($centralDirOffset + $PREFIX_DELTA) . "]" + if $PREFIX_DELTA ; + out $d, "Offset to Central dir", $out; + + if (! emptyArchive($startRecordOffset, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset)) + { + my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name($signature) . "' is invalid"; + $centralDirOffset = checkOffsetValue($centralDirOffset, $startRecordOffset, $centralDirSize, $commonMessage, $startRecordOffset + 48, ZIP_CENTRAL_HDR_SIG, 0, $extractSpec < 0x3E) ; + } + + # Length of 44 means typical version 1 header + return + if $size == 44 ; + + my $remaining = $size - 44; + + # pkzip sets the extract zip spec to 6.2 (0x3E) to signal a v2 record + # See APPNOTE 6.3.10, section, 7.3.3 + + if ($extractSpec >= 0x3E) + { + # Version 2 header (see APPNOTE 6.3.7, section 7.3.4, ) + # Can use version 2 header to infer presence of encrypted CD + $CentralDirectory->setPkEncryptedCD(); + + + # Compression Method 2 bytes Method used to compress the + # Central Directory + # Compressed Size 8 bytes Size of the compressed data + # Original Size 8 bytes Original uncompressed size + # AlgId 2 bytes Encryption algorithm ID + # BitLen 2 bytes Encryption key length + # Flags 2 bytes Encryption flags + # HashID 2 bytes Hash algorithm identifier + # Hash Length 2 bytes Length of hash data + # Hash Data (variable) Hash data + + my ($bcm, $compressedMethod) = read_v(); + out $bcm, "Compression Method", compressionMethod($compressedMethod) ; + info $FH->tell() - 2, "Unknown 'Compression Method' ID " . decimalHex0x($compressedMethod, 2) + if ! defined $ZIP_CompressionMethods{$compressedMethod} ; + out_Q "Compressed Size"; + out_Q "Uncompressed Size"; + out_v "AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ; + out_v "BitLen"; + out_v "Flags", sub { $FlagsLookup{ $_[0] } // "reserved for certificate processing" } ; + out_v "HashID", sub { $HashIDLookup{ $_[0] } // "Unknown ID" } ; + + my $hashLen = out_v "Hash Length "; + outHexdump($hashLen, "Hash Data"); + + $remaining -= $hashLen + 28; + } + + my $entry = Zip64EndCentralHeaderEntry->new(); + + if ($remaining) + { + # Handle 'zip64 extensible data sector' here + # See APPNOTE 6.3.10, section 4.3.14.3, 4.3.14.4 & APPENDIX C + # Not seen a real example of this. Tested with hand crafted files. + walkExtra($remaining, $entry); + } + + return {}; +} + + +sub Zip64EndCentralLocator +{ + # APPNOTE 6.3.10, sec 4.3.15 + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + print "\n"; + out $data, "ZIP64 END CENTRAL DIR LOCATOR", Value_V($signature); + + need 16, Signatures::name($signature); + + # my ($nextRecord, $deltaActuallyAvailable) = $HeaderOffsetIndex->checkForOverlap(16); + + # if ($deltaActuallyAvailable) + # { + # fatal_truncated_record( + # sprintf("ZIP64 END CENTRAL DIR LOCATOR \@%X truncated", $FH->tell() - 4), + # sprintf("Need 0x%X bytes, have 0x%X available", 16, $deltaActuallyAvailable), + # sprintf("Next Record is %s \@0x%X", $nextRecord->name(), $nextRecord->offset()) + # ) + # } + + # TODO - check values for traces of multi-part + crazy offsets + out_V "Central Dir Disk no"; + + my ($d, $zip64EndCentralDirOffset) = read_Q(); + my $out = Value_Q($zip64EndCentralDirOffset); + testPossiblePrefix($zip64EndCentralDirOffset, ZIP64_END_CENTRAL_REC_HDR_SIG); + + $out .= " [Actual Offset is " . Value_Q($zip64EndCentralDirOffset + $PREFIX_DELTA) . "]" + if $PREFIX_DELTA ; + out $d, "Offset to Zip64 EOCD", $out; + + my $totalDisks = out_V "Total no of Disks"; + + if ($totalDisks > 0) + { + my $commonMessage = "'Offset to Zip64 End of Central Directory Record' field in '" . Signatures::name($signature) . "' is invalid"; + $zip64EndCentralDirOffset = checkOffsetValue($zip64EndCentralDirOffset, $startRecordOffset, 0, $commonMessage, $FH->tell() - 12, ZIP64_END_CENTRAL_REC_HDR_SIG) ; + } + + return {}; +} + +sub needZip64EOCDLocator +{ + # zip64 end of central directory field needed if any of the fields + # in the End Central Header record are maxed out + + my $diskNumber = shift ; + my $cdDiskNumber = shift ; + my $entriesOnThisDisk = shift ; + my $totalEntries = shift ; + my $centralDirSize = shift ; + my $centralDirOffset = shift ; + + return (full16($diskNumber) || # 4.4.19 + full16($cdDiskNumber) || # 4.4.20 + full16($entriesOnThisDisk) || # 4.4.21 + full16($totalEntries) || # 4.4.22 + full32($centralDirSize) || # 4.4.23 + full32($centralDirOffset) # 4.4.24 + ) ; +} + +sub emptyArchive +{ + my $offset = shift; + my $diskNumber = shift ; + my $cdDiskNumber = shift ; + my $entriesOnThisDisk = shift ; + my $totalEntries = shift ; + my $centralDirSize = shift ; + my $centralDirOffset = shift ; + + return (#$offset == 0 && + $diskNumber == 0 && + $cdDiskNumber == 0 && + $entriesOnThisDisk == 0 && + $totalEntries == 0 && + $centralDirSize == 0 && + $centralDirOffset== 0 + ) ; +} + +sub EndCentralHeader +{ + # APPNOTE 6.3.10, sec 4.3.16 + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + print "\n"; + out $data, "END CENTRAL HEADER", Value_V($signature); + + need 18, Signatures::name($signature); + + # TODO - check values for traces of multi-part + crazy values + my $diskNumber = out_v "Number of this disk"; + my $cdDiskNumber = out_v "Central Dir Disk no"; + my $entriesOnThisDisk = out_v "Entries in this disk"; + my $totalEntries = out_v "Total Entries"; + my $centralDirSize = out_V "Size of Central Dir"; + + my ($d, $centralDirOffset) = read_V(); + my $out = Value_V($centralDirOffset); + testPossiblePrefix($centralDirOffset, ZIP_CENTRAL_HDR_SIG); + + $out .= " [Actual Offset is " . Value_V($centralDirOffset + $PREFIX_DELTA) . "]" + if $PREFIX_DELTA && $centralDirOffset != MAX32 ; + out $d, "Offset to Central Dir", $out; + + my $comment_length = out_v "Comment Length"; + + if ($comment_length) + { + my $here = $FH->tell() ; + my $available = $FILELEN - $here ; + if ($available < $comment_length) + { + error $here, + "file truncated while reading 'Comment' field in '" . Signatures::name($signature) . "'", + "Expected " . decimalHex0x($comment_length) . " bytes, but only " . decimalHex0x($available) . " available" ; + $comment_length = $available; + } + + if ($comment_length) + { + my $comment ; + myRead($comment, $comment_length); + outputFilename $comment, 0, "Comment"; + } + } + + if ( ! Nesting::isNested($startRecordOffset, $FH->tell() -1)) + { + # Not nested + if (! needZip64EOCDLocator($diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset) && + ! emptyArchive($startRecordOffset, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset)) + { + my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name($signature) . "' is invalid"; + $centralDirOffset = checkOffsetValue($centralDirOffset, $startRecordOffset, $centralDirSize, $commonMessage, $startRecordOffset + 16, ZIP_CENTRAL_HDR_SIG) ; + } + } + # else do nothing + + return {}; +} + +sub DataDescriptor +{ + + # Data header record or Spanned archive marker. + # + + # ZIP_DATA_HDR_SIG at start of file flags a spanned zip file. + # If it is a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG + # See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5 + + # If not at start of file, assume a Data Header Record + # See APPNOTE 6.3.10, sec 4.3.9 & 4.3.9.3 + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + my $here = $FH->tell(); + + if ($here == 4) + { + # Spanned Archive Marker + out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature); + return; + + # my (undef, $next_sig) = read_V(); + # seekTo(0); + + # if ($next_sig == ZIP_LOCAL_HDR_SIG) + # { + # print "\n"; + # out $data, "SPLIT ARCHIVE MULTI-SEGMENT MARKER", Value_V($signature); + # seekTo($here); + # return; + # } + } + + my $sigName = Signatures::titleName(ZIP_DATA_HDR_SIG); + + print "\n"; + out $data, $sigName, Value_V($signature); + + need 24, Signatures::name($signature); + + # Ignore header payload if nested (assume 64-bit descriptor) + if (Nesting::isNested( $here - 4, $here - 4 + 24 - 1)) + { + out "", "Skipping nested payload"; + return {}; + } + + my $compressedSize; + my $uncompressedSize; + + my $localEntry = $LocalDirectory->lastStreamedEntryAdded(); + my $centralEntry = $localEntry && $localEntry->getCdEntry ; + + if (!$localEntry) + { + # found a Data Descriptor without a local header + out "", "Skipping Data Descriptor", "No matching Local header with streaming bit set"; + error $here - 4, "Orphan '$sigName' found", "No matching Local header with streaming bit set"; + return {}; + } + + my $crc = out_V "CRC"; + my $payloadLength = $here - 4 - $localEntry->payloadOffset; + + my $deltaToNext = deltaToNextSignature(); + my $cl32 = unpack "V", peekAtOffset($here + 4, 4); + my $cl64 = unpack "Q<", peekAtOffset($here + 4, 8); + + # use delta to next header & payload length + # deals with use case where the payload length < 32 bit + # will use a 32-bit value rather than the 64-bit value + + # see if delta & payload size match + if ($deltaToNext == 16 && $cl64 == $payloadLength) + { + if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64)) + { + error $here, "'$sigName': expected 32-bit values, got 64-bit"; + } + + $compressedSize = out_Q "Compressed Size" ; + $uncompressedSize = out_Q "Uncompressed Size" ; + } + elsif ($deltaToNext == 8 && $cl32 == $payloadLength) + { + if ($localEntry->zip64) + { + error $here, "'$sigName': expected 64-bit values, got 32-bit"; + } + + $compressedSize = out_V "Compressed Size" ; + $uncompressedSize = out_V "Uncompressed Size" ; + } + + # Try matching juast payload lengths + elsif ($cl32 == $payloadLength) + { + if ($localEntry->zip64) + { + error $here, "'$sigName': expected 64-bit values, got 32-bit"; + } + + $compressedSize = out_V "Compressed Size" ; + $uncompressedSize = out_V "Uncompressed Size" ; + + warning $here, "'$sigName': Zip Header not directly after Data Descriptor"; + } + elsif ($cl64 == $payloadLength) + { + if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64)) + { + error $here, "'$sigName': expected 32-bit values, got 64-bit"; + } + + $compressedSize = out_Q "Compressed Size" ; + $uncompressedSize = out_Q "Uncompressed Size" ; + + warning $here, "'$sigName': Zip Header not directly after Data Descriptor"; + } + + # payloads don't match, so try delta + elsif ($deltaToNext == 16) + { + if (! $localEntry->zip64 && ($centralEntry && ! $centralEntry->zip64)) + { + error $here, "'$sigName': expected 32-bit values, got 64-bit"; + } + + $compressedSize = out_Q "Compressed Size" ; + # compressed size is wrong + error $here, "'$sigName': Compressed size" . decimalHex0x($compressedSize) . " doesn't match with payload size " . decimalHex0x($payloadLength); + + $uncompressedSize = out_Q "Uncompressed Size" ; + } + elsif ($deltaToNext == 8 ) + { + if ($localEntry->zip64) + { + error $here, "'$sigName': expected 64-bit values, got 32-bit"; + } + + $compressedSize = out_V "Compressed Size" ; + # compressed size is wrong + error $here, "'$sigName': Compressed Size " . decimalHex0x($compressedSize) . " doesn't match with payload size " . decimalHex0x($payloadLength); + + $uncompressedSize = out_V "Uncompressed Size" ; + } + + # no payoad or delta match at all, so likely a false positive or data corruption + else + { + warning $here, "Cannot determine size of Data Descriptor record"; + } + + # TODO - neither payload size or delta to next signature match + + if ($localEntry) + { + $localEntry->readDataDescriptor(1) ; + $localEntry->crc32($crc) ; + $localEntry->compressedSize($compressedSize) ; + $localEntry->uncompressedSize($uncompressedSize) ; + } + + # APPNOTE 6.3.10, sec 4.3.8 + my $filename = $localEntry->filename; + warning undef, "Directory '$filename' must not have a payload" + if $filename =~ m#/$# && $uncompressedSize ; + + return { + crc => $crc, + compressedSize => $compressedSize, + uncompressedSize => $uncompressedSize, + }; +} + +sub SingleSegmentMarker +{ + # ZIP_SINGLE_SEGMENT_MARKER at start of file flags a spanned zip file. + # If this ia a true marker, the next four bytes MUST be a ZIP_LOCAL_HDR_SIG + # See APPNOTE 6.3.10, sec 8.5.3, 8.5.4 & 8.5.5 + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + my $here = $FH->tell(); + + if ($here == 4) + { + my (undef, $next_sig) = read_V(); + if ($next_sig == ZIP_LOCAL_HDR_SIG) + { + print "\n"; + out $data, "SPLIT ARCHIVE SINGLE-SEGMENT MARKER", Value_V($signature); + } + seekTo($here); + } + + return {}; +} + +sub ArchiveExtraDataRecord +{ + # TODO - not seen an example of this record + + # APPNOTE 6.3.10, sec 4.3.11 + + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + out $data, "ARCHIVE EXTRA DATA RECORD", Value_V($signature); + + need 2, Signatures::name($signature); + + my $size = out_v "Size of record"; + + need $size, Signatures::name($signature); + + outHexdump($size, "Field data", 1); + + return {}; +} + +sub DigitalSignature +{ + my $signature = shift ; + my $data = shift ; + my $startRecordOffset = shift ; + + print "\n"; + out $data, "DIGITAL SIGNATURE RECORD", Value_V($signature); + + need 2, Signatures::name($signature); + my $Size = out_v "Size of record"; + + need $Size, Signatures::name($signature); + + + myRead(my $payload, $Size); + out $payload, "Signature", hexDump16($payload); + + return {}; +} + +sub GeneralPurposeBits +{ + my $method = shift; + my $gp = shift; + + out1 "[Bit 0]", "1 'Encryption'" if $gp & ZIP_GP_FLAG_ENCRYPTED_MASK; + + my %lookup = ( + 0 => "Normal Compression", + 1 => "Maximum Compression", + 2 => "Fast Compression", + 3 => "Super Fast Compression"); + + + if ($method == ZIP_CM_DEFLATE) + { + my $mid = ($gp >> 1) & 0x03 ; + + out1 "[Bits 1-2]", "$mid '$lookup{$mid}'"; + } + + if ($method == ZIP_CM_LZMA) + { + if ($gp & ZIP_GP_FLAG_LZMA_EOS_PRESENT) { + out1 "[Bit 1]", "1 'LZMA EOS Marker Present'" ; + } + else { + out1 "[Bit 1]", "0 'LZMA EOS Marker Not Present'" ; + } + } + + if ($method == ZIP_CM_IMPLODE) # Imploding + { + out1 "[Bit 1]", ($gp & (1 << 1) ? "1 '8k" : "0 '4k") . " Sliding Dictionary'" ; + out1 "[Bit 2]", ($gp & (2 << 1) ? "1 '3" : "0 '2" ) . " Shannon-Fano Trees'" ; + } + + out1 "[Bit 3]", "1 'Streamed'" if $gp & ZIP_GP_FLAG_STREAMING_MASK; + out1 "[Bit 4]", "1 'Enhanced Deflating'" if $gp & 1 << 4; + out1 "[Bit 5]", "1 'Compressed Patched'" if $gp & ZIP_GP_FLAG_PATCHED_MASK ; + out1 "[Bit 6]", "1 'Strong Encryption'" if $gp & ZIP_GP_FLAG_STRONG_ENCRYPTED_MASK; + out1 "[Bit 11]", "1 'Language Encoding'" if $gp & ZIP_GP_FLAG_LANGUAGE_ENCODING; + out1 "[Bit 12]", "1 'Pkware Enhanced Compression'" if $gp & ZIP_GP_FLAG_PKWARE_ENHANCED_COMP ; + out1 "[Bit 13]", "1 'Encrypted Central Dir'" if $gp & ZIP_GP_FLAG_ENCRYPTED_CD ; + + return (); +} + + +sub seekSet +{ + my $fh = $_[0] ; + my $size = $_[1]; + + use Fcntl qw(SEEK_SET); + seek($fh, $size, SEEK_SET); + +} + +sub skip +{ + my $fh = $_[0] ; + my $size = $_[1]; + + use Fcntl qw(SEEK_CUR); + seek($fh, $size, SEEK_CUR); + +} + + +sub myRead +{ + my $got = \$_[0] ; + my $size = $_[1]; + + my $wantSize = $size; + $$got = ''; + + if ($size == 0) + { + return ; + } + + if ($size > 0) + { + my $buff ; + my $status = $FH->read($buff, $size); + return $status + if $status < 0; + $$got .= $buff ; + } + + my $len = length $$got; + fatal undef, "Truncated file (got $len, wanted $wantSize): $!" + if length $$got != $wantSize; +} + +sub need +{ + my $byteCount = shift ; + my $message = shift ; + my $field = shift // ''; + + # return $FILELEN - $FH->tell() >= $byteCount; + my $here = $FH->tell() ; + my $available = $FILELEN - $here ; + if ($available < $byteCount) + { + my @message ; + + if ($field) + { + push @message, "file truncated while reading '$field' field in '$message'"; + } + else + { + push @message, "file truncated while reading '$message'"; + } + + push @message, sprintf("Expected 0x%X bytes, but only 0x%X available", $byteCount, $available); + push @message, "Try running with --walk' or '--scan' options" + if ! $opt_scan && ! $opt_walk ; + + fatal $here, @message; + } +} + +sub testPossiblePrefix +{ + my $offset = shift; + my $expectedSignature = shift ; + + if (testPossiblePrefixNoPREFIX_DELTA($offset, $expectedSignature)) + { + $PREFIX_DELTA = $POSSIBLE_PREFIX_DELTA; + $POSSIBLE_PREFIX_DELTA = 0; + + reportPrefixData(); + + return 1 + } + + return 0 +} + +sub testPossiblePrefixNoPREFIX_DELTA +{ + my $offset = shift; + my $expectedSignature = shift ; + + return 0 + if $offset + 4 > $FILELEN || ! $POSSIBLE_PREFIX_DELTA || $PREFIX_DELTA; + + my $currentOFFSET = $OFFSET; + my $gotSig = readSignatureFromOffset($offset); + + if ($gotSig == $expectedSignature) + { + # do have possible prefix data, but the offset is correct + $POSSIBLE_PREFIX_DELTA = $PREFIX_DELTA = 0; + $OFFSET = $currentOFFSET; + + return 0; + } + + $gotSig = readSignatureFromOffset($offset + $POSSIBLE_PREFIX_DELTA); + + $OFFSET = $currentOFFSET; + + return ($gotSig == $expectedSignature) ; +} + +sub offsetIsValid +{ + my $offset = shift; + my $headerStart = shift; + my $centralDirSize = shift; + my $commonMessage = shift ; + my $expectedSignature = shift ; + my $dereferencePointer = shift; + + my $must_point_back = 1; + + my $delta = $offset - $FILELEN + 1 ; + + $offset += $PREFIX_DELTA + if $PREFIX_DELTA ; + + return sprintf("value %s is %s bytes past EOF", decimalHex0x($offset), decimalHex0x($delta)) + if $delta > 0 ; + + return sprintf "value %s must be less that %s", decimalHex0x($offset), decimalHex0x($headerStart) + if $must_point_back && $offset >= $headerStart; + + if ($dereferencePointer) + { + my $actual = $headerStart - $centralDirSize; + my $cdSizeOK = ($actual == $offset); + my $possibleDelta = $actual - $offset; + + if ($centralDirSize && ! $cdSizeOK && $possibleDelta > 0 && readSignatureFromOffset($possibleDelta) == ZIP_LOCAL_HDR_SIG) + { + # If testing end of central dir, check if the location of the first CD header + # is consistent with the central dir size. + # Common use case is a SFX zip file + + my $gotSig = readSignatureFromOffset($actual); + my $v = hexValue32($gotSig); + return 'value @ ' . hexValue($actual) . " should decode to signature for " . Signatures::nameAndHex($expectedSignature) . ". Got $v" # . hexValue32($gotSig) + if $gotSig != $expectedSignature ; + + $PREFIX_DELTA = $possibleDelta; + reportPrefixData(); + + return undef; + } + else + { + my $gotSig = readSignatureFromOffset($offset); + my $v = hexValue32($gotSig); + return 'value @ ' . hexValue($offset) . " should decode to signature for " . Signatures::nameAndHex($expectedSignature) . ". Got $v" # . hexValue32($gotSig) + if $gotSig != $expectedSignature ; + } + } + + return undef ; +} + +sub checkOffsetValue +{ + my $offset = shift; + my $headerStart = shift; + my $centralDirSize = shift; + my $commonMessage = shift ; + my $messageOffset = shift; + my $expectedSignature = shift ; + my $fatal = shift // 0; + my $dereferencePointer = shift // 1; + + my $keepOFFSET = $OFFSET ; + + my $message = offsetIsValid($offset, $headerStart, $centralDirSize, $commonMessage, $expectedSignature, $dereferencePointer); + if ($message) + { + fatal_tryWalk($messageOffset, $commonMessage, $message) + if $fatal; + + error $messageOffset, $commonMessage, $message + if ! $fatal; + } + + $OFFSET = $keepOFFSET; + + return $offset + $PREFIX_DELTA; + +} + +sub fatal_truncated +{ + my $message = shift; + fatal_tryWalk(undef, "truncated file", $message, @_); + +} + +sub fatal_tryWalk +{ + my $offset = shift ; + my $message = shift; + + fatal($offset, $message, @_, "Try running with --walk' or '--scan' options"); +} + +sub fatal +{ + my $offset = shift ; + my $message = shift; + + return if $fatalDisabled; + + if (defined $offset) + { + warn "#\n# FATAL: Offset " . hexValue($offset) . ": $message\n"; + } + else + { + warn "#\n# FATAL: $message\n"; + } + + warn "# $_ . \n" + for @_; + warn "#\n" ; + + exit 1; +} + +sub disableFatal +{ + $fatalDisabled = 1 ; +} + +sub enableFatal +{ + $fatalDisabled = 0 ; +} + +sub topLevelFatal +{ + my $message = shift ; + + no warnings 'utf8'; + + warn "FATAL: $message\n"; + + warn "$_ . \n" + for @_; + + exit 1; +} + +sub internalFatal +{ + my $offset = shift ; + my $message = shift; + + no warnings 'utf8'; + + if (defined $offset) + { + warn "# FATAL: Offset " . hexValue($offset) . ": Internal Error: $message\n"; + } + else + { + warn "# FATAL: Internal Error: $message\n"; + } + + warn "# $_ \n" + for @_; + + warn "# Please report error at https://github.com/pmqs/zipdetails/issues\n"; + exit 1; +} + +sub warning +{ + my $offset = shift ; + my $message = shift; + + no warnings 'utf8'; + + return + unless $opt_want_warning_mesages ; + + say "#" + unless $lastWasMessage ++ ; + + if (defined $offset) + { + say "# WARNING: Offset " . hexValue($offset) . ": $message"; + } + else + { + say "# WARNING: $message"; + } + + + say "# $_" for @_ ; + say "#"; + ++ $WarningCount ; + + $exit_status_code |= 2 + if $opt_want_message_exit_status ; +} + +sub error +{ + my $offset = shift ; + my $message = shift; + + no warnings 'utf8'; + + return + unless $opt_want_error_mesages ; + + say "#" + unless $lastWasMessage ++ ; + + if (defined $offset) + { + say "# ERROR: Offset " . hexValue($offset) . ": $message"; + } + else + { + say "# ERROR: $message"; + } + + + say "# $_" for @_ ; + say "#"; + + ++ $ErrorCount ; + + $exit_status_code |= 4 + if $opt_want_message_exit_status ; +} + +sub debug +{ + my $offset = shift ; + my $message = shift; + + no warnings 'utf8'; + + say "#" + unless $lastWasMessage ++ ; + + if (defined $offset) + { + say "# DEBUG: Offset " . hexValue($offset) . ": $message"; + } + else + { + say "# DEBUG: $message"; + } + + + say "# $_" for @_ ; + say "#"; +} + +sub internalError +{ + my $message = shift; + + no warnings 'utf8'; + + say "#"; + say "# ERROR: $message"; + say "# $_" for @_ ; + say "# Please report error at https://github.com/pmqs/zipdetails/issues"; + say "#"; + + ++ $ErrorCount ; +} + +sub reportPrefixData +{ + my $delta = shift // $PREFIX_DELTA ; + state $reported = 0; + return if $reported || $delta == 0; + + info 0, "found " . decimalHex0x($delta) . " bytes before beginning of zipfile" ; + $reported = 1; +} + +sub info +{ + my $offset = shift; + my $message = shift; + + no warnings 'utf8'; + + return + unless $opt_want_info_mesages ; + + say "#" + unless $lastWasMessage ++ ; + + if (defined $offset) + { + say "# INFO: Offset " . hexValue($offset) . ": $message"; + } + else + { + say "# INFO: $message"; + } + + say "# $_" for @_ ; + say "#"; + + ++ $InfoCount ; + + $exit_status_code |= 1 + if $opt_want_message_exit_status ; +} + +sub walkExtra +{ + # APPNOTE 6.3.10, sec 4.4.11, 4.4.28, 4.5 + my $XLEN = shift; + my $entry = shift; + + # Caller has determined that there are $XLEN bytes available to read + + my $buff ; + my $offset = 0 ; + + my $id; + my $subLen; + my $payload ; + + my $count = 0 ; + my $endExtraOffset = $FH->tell() + $XLEN ; + + while ($offset < $XLEN) { + + ++ $count; + + # Detect if there is not enough data for an extra ID and length. + # Android zipalign and zipflinger are prime candidates for these + # non-standard extra sub-fields. + my $remaining = $XLEN - $offset; + if ($remaining < ZIP_EXTRA_SUBFIELD_HEADER_SIZE) { + # There is not enough left. + # Consume whatever is there and return so parsing + # can continue. + + myRead($payload, $remaining); + my $data = hexDump($payload); + + if ($payload =~ /^\x00+$/) + { + # All nulls + out $payload, "Null Padding in Extra"; + info $FH->tell() - length($payload), decimalHex0x(length $payload) . " Null Padding Bytes in Extra Field" ; + } + else + { + out $payload, "Extra Data", $data; + error $FH->tell() - length($payload), "'Extra Data' Malformed"; + } + + return undef; + } + + myRead($id, ZIP_EXTRA_SUBFIELD_ID_SIZE); + $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE; + my $lookID = unpack "v", $id ; + if ($lookID == 0) + { + # check for null padding at end of extra + my $here = $FH->tell(); + my $rest; + myRead($rest, $XLEN - $offset); + if ($rest =~ /^\x00+$/) + { + my $len = length ($id . $rest) ; + out $id . $rest, "Null Padding in Extra"; + info $FH->tell() - $len, decimalHex0x($len) . " Null Padding Bytes in Extra Field"; + return undef; + } + + seekTo($here); + } + + my ($who, $decoder, $local_min, $local_max, $central_min, $central_max) = @{ $Extras{$lookID} // ['', undef, undef, undef, undef, undef ] }; + + my $idString = Value_v($lookID) ; + $idString .= " '$who'" + if $who; + + out $id, "Extra ID #$count", $idString ; + info $FH->tell() - 2, "Unknown Extra ID $idString" + if ! exists $Extras{$lookID} ; + + myRead($buff, ZIP_EXTRA_SUBFIELD_LEN_SIZE); + $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE; + + $subLen = unpack("v", $buff); + out2 $buff, "Length", Value_v($subLen) ; + + $remaining = $XLEN - $offset; + if ($subLen > $remaining ) + { + error $FH->tell() -2, + extraFieldIdentifier($lookID) . ": 'Length' field invalid", + sprintf("value %s > %s bytes remaining", decimalHex0x($subLen), decimalHex0x($remaining)); + outSomeData $remaining, " Extra Payload"; + return undef; + } + + if (! defined $decoder) + { + if ($subLen) + { + myRead($payload, $subLen); + my $data = hexDump16($payload); + + out2 $payload, "Extra Payload", $data; + } + } + else + { + if (testExtraLimits($lookID, $subLen, $entry->inCentralDir)) + { + my $endExtraOffset = $FH->tell() + $subLen; + $decoder->($lookID, $subLen, $entry) ; + + # Belt & Braces - should now be at $endExtraOffset + # error here means issue in an extra handler + # should noy happen, but just in case + # TODO -- need tests for this + my $here = $FH->tell() ; + if ($here > $endExtraOffset) + { + # gone too far, so need to bomb out now + internalFatal $here, "Overflow processing " . extraFieldIdentifier($lookID) . ".", + sprintf("Should be at offset %s, actually at %s", decimalHex0x($endExtraOffset), decimalHex0x($here)); + } + elsif ($here < $endExtraOffset) + { + # not gone far enough, can recover + error $here, + sprintf("Expected to be at offset %s after processing %s, actually at %s", decimalHex0x($endExtraOffset), extraFieldIdentifier($lookID), decimalHex0x($here)), + "Skipping " . decimalHex0x($endExtraOffset - $here) . " bytes"; + outSomeData $endExtraOffset - $here, " Extra Data"; + } + } + } + + $offset += $subLen ; + } + + return undef ; +} + +sub testExtraLimits +{ + my $lookID = shift; + my $size = shift; + my $inCentralDir = shift; + + my ($who, undef, $local_min, $local_max, $central_min, $central_max) = @{ $Extras{$lookID} // ['', undef, undef, undef, undef, undef ] }; + + my ($min, $max) = $inCentralDir + ? ($central_min, $central_max) + : ($local_min, $local_max) ; + + return 1 + if ! defined $min && ! defined $max ; + + if (defined $min && defined $max) + { + # both the same + if ($min == $max) + { + if ($size != $min) + { + error $FH->tell() -2, sprintf "%s: 'Length' field invalid: expected %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($size); + outSomeData $size, " Extra Payload" if $size; + return 0; + } + } + else # min != max + { + if ($size < $min || $size > $max) + { + error $FH->tell() -2, sprintf "%s: 'Length' field invalid: value must be betweem %s and %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($max), decimalHex0x($size); + outSomeData $size, " Extra Payload" if $size ; + return 0; + } + } + + } + else # must be defined $min & undefined max + { + if ($size < $min) + { + error $FH->tell() -2, sprintf "%s: 'Length' field invalid: value must be at least %s, got %s", extraFieldIdentifier($lookID), decimalHex0x($min), decimalHex0x($size); + outSomeData $size, " Extra Payload" if $size; + return 0; + } + } + + return 1; + +} + +sub full32 +{ + return ($_[0] // 0) == MAX32 ; +} + +sub full16 +{ + return ($_[0] // 0) == MAX16 ; +} + +sub decode_Zip64 +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + myRead(my $payload, $len); + if ($entry->inCentralDir() ) + { + walk_Zip64_in_CD($extraID, $payload, $entry, 1) ; + } + else + { + walk_Zip64_in_LD($extraID, $payload, $entry, 1) ; + + } +} + +sub walk_Zip64_in_LD +{ + my $extraID = shift ; + my $zip64Extended = shift; + my $entry = shift; + my $display = shift // 1 ; + + my $fieldStart = $FH->tell() - length $zip64Extended; + my $fieldOffset = $fieldStart ; + + $ZIP64 = 1; + $entry->zip64(1); + + if (length $zip64Extended == 0) + { + info $fieldOffset, extraFieldIdentifier($extraID) . ": Length is Zero"; + return; + } + + my $assumeLengthsPresent = (length($zip64Extended) == 16) ; + my $assumeAllFieldsPresent = (length($zip64Extended) == 28) ; + + if ($assumeLengthsPresent || $assumeAllFieldsPresent || full32 $entry->std_uncompressedSize ) + { + # TODO defer a warning if in local header & central/local don't have std_uncompressedSizeset to 0xffffffff + if (length $zip64Extended < 8) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for 'Uncompressed Size': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 8; + my $data = substr($zip64Extended, 0, 8, "") ; + $entry->uncompressedSize(unpack "Q<", $data); + out2 $data, "Uncompressed Size", Value_Q($entry->uncompressedSize) + if $display; + } + + if ($assumeLengthsPresent || $assumeAllFieldsPresent || full32 $entry->std_compressedSize) + { + if (length $zip64Extended < 8) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for 'Compressed Size': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 8; + + my $data = substr($zip64Extended, 0, 8, "") ; + $entry->compressedSize( unpack "Q<", $data); + out2 $data, "Compressed Size", Value_Q($entry->compressedSize) + if $display; + } + + # Zip64 in local header should not have localHeaderOffset or disk number + # but some zip files do + + if ($assumeAllFieldsPresent) + { + $fieldOffset += 8; + + my $data = substr($zip64Extended, 0, 8, "") ; + my $localHeaderOffset = unpack "Q<", $data; + out2 $data, "Offset to Local Dir", Value_Q($localHeaderOffset) + if $display; + } + + if ($assumeAllFieldsPresent) + { + $fieldOffset += 4; + + my $data = substr($zip64Extended, 0, 4, "") ; + my $diskNumber = unpack "v", $data; + out2 $data, "Disk Number", Value_V($diskNumber) + if $display; + } + + if (length $zip64Extended) + { + if ($display) + { + out2 $zip64Extended, "Unexpected Data", hexDump16 $zip64Extended ; + info $fieldOffset, extraFieldIdentifier($extraID) . ": Unexpected Data: " . decimalHex0x(length $zip64Extended) . " bytes"; + } + } + +} + +sub walk_Zip64_in_CD +{ + my $extraID = shift ; + my $zip64Extended = shift; + my $entry = shift; + my $display = shift // 1 ; + + my $fieldStart = $FH->tell() - length $zip64Extended; + my $fieldOffset = $fieldStart ; + + $ZIP64 = 1; + $entry->zip64(1); + + if (length $zip64Extended == 0) + { + info $fieldOffset, extraFieldIdentifier($extraID) . ": Length is Zero"; + return; + } + + my $assumeAllFieldsPresent = (length($zip64Extended) == 28) ; + + if ($assumeAllFieldsPresent || full32 $entry->std_uncompressedSize ) + { + if (length $zip64Extended < 8) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for 'Uncompressed Size': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 8; + my $data = substr($zip64Extended, 0, 8, "") ; + $entry->uncompressedSize(unpack "Q<", $data); + out2 $data, "Uncompressed Size", Value_Q($entry->uncompressedSize) + if $display; + } + + if ($assumeAllFieldsPresent || full32 $entry->std_compressedSize) + { + if (length $zip64Extended < 8) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for 'Compressed Size': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 8; + + my $data = substr($zip64Extended, 0, 8, "") ; + $entry->compressedSize(unpack "Q<", $data); + out2 $data, "Compressed Size", Value_Q($entry->compressedSize) + if $display; + } + + if ($assumeAllFieldsPresent || full32 $entry->std_localHeaderOffset) + { + if (length $zip64Extended < 8) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(8) . " bytes for 'Offset to Local Dir': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 8; + + my $here = $FH->tell(); + my $data = substr($zip64Extended, 0, 8, "") ; + $entry->localHeaderOffset(unpack "Q<", $data); + out2 $data, "Offset to Local Dir", Value_Q($entry->localHeaderOffset) + if $display; + + my $commonMessage = "'Offset to Local Dir' field in 'Zip64 Extra Field' is invalid"; + $entry->localHeaderOffset(checkOffsetValue($entry->localHeaderOffset, $fieldStart, 0, $commonMessage, $fieldStart, ZIP_LOCAL_HDR_SIG, 0) ); + } + + if ($assumeAllFieldsPresent || full16 $entry->std_diskNumber) + { + if (length $zip64Extended < 4) + { + my $message = extraFieldIdentifier($extraID) . ": Expected " . decimalHex0x(4) . " bytes for 'Disk Number': only " . decimalHex0x(length $zip64Extended) . " bytes present"; + error $fieldOffset, $message; + out2 $zip64Extended, $message; + return; + } + + $fieldOffset += 4; + + my $here = $FH->tell(); + my $data = substr($zip64Extended, 0, 4, "") ; + $entry->diskNumber(unpack "v", $data); + out2 $data, "Disk Number", Value_V($entry->diskNumber) + if $display; + $entry->zip64_diskNumberPresent(1); + } + + if (length $zip64Extended) + { + if ($display) + { + out2 $zip64Extended, "Unexpected Data", hexDump16 $zip64Extended ; + info $fieldOffset, extraFieldIdentifier($extraID) . ": Unexpected Data: " . decimalHex0x(length $zip64Extended) . " bytes"; + } + } +} + +sub Ntfs2Unix +{ + my $m = shift; + my $v = shift; + + # NTFS offset is 19DB1DED53E8000 + + my $hex = Value_Q($v) ; + + # Treat empty value as special case + # Could decode to 1 Jan 1601 + return "$hex 'No Date/Time'" + if $v == 0; + + $v -= 0x19DB1DED53E8000 ; + my $ns = ($v % 10000000) * 100; + my $elapse = int ($v/10000000); + return "$hex '" . getT($elapse) . + " " . sprintf("%0dns'", $ns); +} + +sub decode_NTFS_Filetimes +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + out_V " Reserved"; + out_v " Tag1"; + out_v " Size1" ; + + my ($m, $s1) = read_Q; + out $m, " Mtime", Ntfs2Unix($m, $s1); + + my ($a, $s3) = read_Q; + out $a, " Atime", Ntfs2Unix($a, $s3); + + my ($c, $s2) = read_Q; + out $c, " Ctime", Ntfs2Unix($c, $s2); +} + +sub OpenVMS_DateTime +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + # VMS epoch is 17 Nov 1858 + # Offset to Unix Epoch is -0x7C95674C3DA5C0 (-35067168005400000) + + my ($data, $value) = read_Q(); + + my $datetime = "No Date Time'"; + if ($value != 0) + { + my $v = $value - 0x007C95674C3DA5C0 ; + my $ns = ($v % 10000000) * 100 ; + my $seconds = int($v / 10000000) ; + $datetime = getT($seconds) . + " " . sprintf("%0dns'", $ns); + } + + out2 $data, " Attribute", Value_Q($value) . " '$datetime"; +} + +sub OpenVMS_DumpBytes +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + myRead(my $data, $size); + + out($data, " Attribute", hexDump16($data)); + +} + +sub OpenVMS_4ByteValue +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + my ($data, $value) = read_V(); + + out2 $data, " Attribute", Value_V($value); +} + +sub OpenVMS_UCHAR +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + state $FCH = { + 0 => 'FCH$M_WASCONTIG', + 1 => 'FCH$M_NOBACKUP', + 2 => 'FCH$M_WRITEBACK', + 3 => 'FCH$M_READCHECK', + 4 => 'FCH$M_WRITCHECK', + 5 => 'FCH$M_CONTIGB', + 6 => 'FCH$M_LOCKED', + 6 => 'FCH$M_CONTIG', + 11 => 'FCH$M_BADACL', + 12 => 'FCH$M_SPOOL', + 13 => 'FCH$M_DIRECTORY', + 14 => 'FCH$M_BADBLOCK', + 15 => 'FCH$M_MARKDEL', + 16 => 'FCH$M_NOCHARGE', + 17 => 'FCH$M_ERASE', + 18 => 'FCH$M_SHELVED', + 20 => 'FCH$M_SCRATCH', + 21 => 'FCH$M_NOMOVE', + 22 => 'FCH$M_NOSHELVABLE', + } ; + + my ($data, $value) = read_V(); + + out2 $data, " Attribute", Value_V($value); + + for my $bit ( sort { $a <=> $b } keys %{ $FCH } ) + { + # print "$bit\n"; + if ($value & (1 << $bit) ) + { + out1 " [Bit $bit]", $FCH->{$bit} ; + } + } +} + +sub OpenVMS_2ByteValue +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + my ($data, $value) = read_v(); + + out2 $data, " Attribute", Value_v($value); +} + +sub OpenVMS_revision +{ + my $ix = shift; + my $tag = shift; + my $size = shift; + + my ($data, $value) = read_v(); + + out2 $data, " Attribute", Value_v($value) . "'Revision Count " . Value_v($value) . "'"; +} + +sub decode_OpenVMS +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + state $openVMS_tags = { + 0x04 => [ 'ATR$C_RECATTR', \&OpenVMS_DumpBytes ], + 0x03 => [ 'ATR$C_UCHAR', \&OpenVMS_UCHAR ], + 0x11 => [ 'ATR$C_CREDATE', \&OpenVMS_DateTime ], + 0x12 => [ 'ATR$C_REVDATE', \&OpenVMS_DateTime ], + 0x13 => [ 'ATR$C_EXPDATE', \&OpenVMS_DateTime ], + 0x14 => [ 'ATR$C_BAKDATE', \&OpenVMS_DateTime ], + 0x0D => [ 'ATR$C_ASCDATES', \&OpenVMS_revision ], + 0x15 => [ 'ATR$C_UIC', \&OpenVMS_4ByteValue ], + 0x16 => [ 'ATR$C_FPRO', \&OpenVMS_DumpBytes ], + 0x17 => [ 'ATR$C_RPRO', \&OpenVMS_2ByteValue ], + 0x1D => [ 'ATR$C_JOURNAL', \&OpenVMS_DumpBytes ], + 0x1F => [ 'ATR$C_ADDACLENT', \&OpenVMS_DumpBytes ], + } ; + + out_V " CRC"; + $len -= 4; + + my $ix = 1; + while ($len) + { + my ($data, $tag) = read_v(); + my $tagname = 'Unknown Tag'; + my $decoder = undef; + + if ($openVMS_tags->{$tag}) + { + ($tagname, $decoder) = @{ $openVMS_tags->{$tag} } ; + } + + out2 $data, "Tag #$ix", Value_v($tag) . " '" . $tagname . "'" ; + my $size = out_v " Size"; + + if (defined $decoder) + { + $decoder->($ix, $tag, $size) ; + } + else + { + outSomeData($size, " Attribute"); + } + + ++ $ix; + $len -= $size + 2 + 2; + } + +} + +sub getT +{ + my $time = shift ; + + if ($opt_utc) + { return scalar gmtime($time) // 'Unknown'} + else + { return scalar localtime($time) // 'Unknown' } +} + +sub getTime +{ + my $time = shift ; + + return "'Invalid Date or Time'" + if ! defined $time; + + return "'" . getT($time) . "'"; +} + +sub LastModTime +{ + my $value = shift ; + + return "'No Date/Time'" + if $value == 0; + + return getTime(_dosToUnixTime($value)) +} + +sub _dosToUnixTime +{ + my $dt = shift; + + # Mozilla xpi files have empty datetime + # This is not a valid Dos datetime value + return 0 if $dt == 0 ; + + my $year = ( ( $dt >> 25 ) & 0x7f ) + 80; + my $mon = ( ( $dt >> 21 ) & 0x0f ) - 1; + my $mday = ( ( $dt >> 16 ) & 0x1f ); + + my $hour = ( ( $dt >> 11 ) & 0x1f ); + my $min = ( ( $dt >> 5 ) & 0x3f ); + my $sec = ( ( $dt << 1 ) & 0x3e ); + + use Time::Local ; + my $time_t; + eval + { + # Use eval to catch crazy dates + $time_t = Time::Local::timegm( $sec, $min, $hour, $mday, $mon, $year); + } + or do + { + my $dosDecode = $year+1900 . sprintf "-%02u-%02u %02u:%02u:%02u", $mon, $mday, $hour, $min, $sec; + warning $FH->tell(), "'Last Mod Date/Time' value " . decimalHex0x($dt, 4) . " decodes to '$dosDecode': not a valid DOS date/time" ; + return undef + }; + + return $time_t; + +} + +sub decode_UT +{ + # 0x5455 'UT: Extended Timestamp' + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # Definition in IZ APPNOTE + + # NOTE: Although the IZ appnote says that the central directory + # doesn't store the Acces & Creation times, there are + # some implementations that do poopulate the CD incorrectly. + + # Caller has determined that at least one byte is available + + # When $full is true assume all timestamps are present + my $full = ($len == 13) ; + + my $remaining = $len; + + my ($data, $flags) = read_C(); + + my $v = Value_C $flags; + my @f ; + push @f, "mod" if $flags & 1; + push @f, "access" if $flags & 2; + push @f, "change" if $flags & 4; + $v .= " '" . join(' ', @f) . "'" + if @f; + + out $data, " Flags", $v; + + info $FH->tell() - 1, extraFieldIdentifier($extraID) . ": Reserved bits set in 'Flags' field" + if $flags & ~0x7; + + -- $remaining; + + if ($flags & 1 || $full) + { + if ($remaining == 0 ) + { + # Central Dir only has mod time + error $FH->tell(), extraFieldIdentifier($extraID) . ": Missing field 'Mod Time'" ; + return; + } + else + { + info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Mod Time' present" + if ! ($flags & 1) ; + + if ($remaining < 4) + { + outSomeData $remaining, " Extra Data"; + error $FH->tell() - $remaining, + extraFieldIdentifier($extraID) . ": Truncated reading 'Mod Time'", + "Expected " . decimalHex0x(4) . " bytes, got " . decimalHex0x($remaining) . " bytes"; + return; + } + + my ($data, $time) = read_V(); + + out2 $data, "Mod Time", Value_V($time) . " " . getTime($time) ; + + $remaining -= 4 ; + } + } + + # The remaining sub-fields are only present in the Local Header + + if ($flags & 2 || $full) + { + if ($remaining == 0 && $entry->inCentralDir) + { + # Central Dir doesn't have access time + } + else + { + info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Access Time' present" + if ! ($flags & 2) || $entry->inCentralDir ; + + if ($remaining < 4) + { + outSomeData $remaining, " Extra Data"; + error $FH->tell() - $remaining, + extraFieldIdentifier($extraID) . ": Truncated reading 'Access Time'" , + "Expected " . decimalHex0x(4) . " bytes, got " . decimalHex0x($remaining) . " bytes"; + + return; + } + + my ($data, $time) = read_V(); + + out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ; + $remaining -= 4 ; + } + } + + if ($flags & 4 || $full) + { + if ($remaining == 0 && $entry->inCentralDir) + { + # Central Dir doesn't have change time + } + else + { + info $FH->tell(), extraFieldIdentifier($extraID) . ": Unexpected 'Change Time' present" + if ! ($flags & 4) || $entry->inCentralDir ; + + if ($remaining < 4) + { + outSomeData $remaining, " Extra Data"; + + error $FH->tell() - $remaining, + extraFieldIdentifier($extraID) . ": Truncated reading 'Change Time'" , + "Expected " . decimalHex0x(4) . " bytes, got " . decimalHex0x($remaining) . " bytes"; + + return; + } + + my ($data, $time) = read_V(); + + out2 $data, "Change Time", Value_V($time) . " " . getTime($time) ; + } + } +} + + +sub decode_Minizip_Signature +{ + # 0x10c5 Minizip CMS Signature + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#cms-signature-0x10c5 + + $CentralDirectory->setMiniZipEncrypted(); + + if ($len == 0) + { + info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Zero length Signature"; + return; + } + + outHexdump($len, " Signature"); + +} + +sub decode_Minizip_Hash +{ + # 0x1a51 Minizip Hash + # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#hash-0x1a51 + + # caller ckecks there are at least 4 bytes available + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + state $Algorithm = { + 10 => 'MD5', + 20 => 'SHA1', + 23 => 'SHA256', + }; + + my $remaining = $len; + + $CentralDirectory->setMiniZipEncrypted(); + + my ($data, $alg) = read_v(); + my $algorithm = $Algorithm->{$alg} // "Unknown"; + + out $data, " Algorithm", Value_v($alg) . " '$algorithm'"; + if (! exists $Algorithm->{$alg}) + { + info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown algorithm ID " .Value_v($alg); + } + + my ($d, $digestSize) = read_v(); + out $d, " Digest Size", Value_v($digestSize); + + $remaining -= 4; + + if ($digestSize == 0) + { + info $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Zero length Digest"; + } + elsif ($digestSize > $remaining) + { + error $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Digest Size " . decimalHex0x($digestSize) . " > " . decimalHex0x($remaining) . " bytes remaining in extra field" ; + $digestSize = $remaining ; + } + + outHexdump($digestSize, " Digest"); + + $remaining -= $digestSize; + + if ($remaining) + { + outHexdump($remaining, " Unexpected Data"); + error $FH->tell() - $remaining, extraFieldIdentifier($extraID) . ": " . decimalHex0x($remaining) . " unexpected trailing bytes" ; + } +} + +sub decode_Minizip_CD +{ + # 0xcdcd Minizip Central Directory + # Definition in https://github.com/zlib-ng/minizip-ng/blob/master/doc/mz_extrafield.md#central-directory-0xcdcd + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + $entry->minizip_secure(1); + $CentralDirectory->setMiniZipEncrypted(); + + my $size = out_Q " Entries"; + + } + +sub decode_AES +{ + # ref https://www.winzip.com/en/support/aes-encryption/ + # Document version: 1.04 + # Last modified: January 30, 2009 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + return if $len == 0 ; + + my $validAES = 1; + + state $lookup = { 1 => "AE-1", 2 => "AE-2" }; + my $vendorVersion = out_v " Vendor Version", sub { $lookup->{$_[0]} || "Unknown" } ; + if (! $lookup->{$vendorVersion}) + { + $validAES = 0; + warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Vendor Version' $vendorVersion. Valid values are 1,2" + } + + my $id ; + myRead($id, 2); + my $idValue = out $id, " Vendor ID", unpackValue_v($id) . " '$id'"; + + if ($id ne 'AE') + { + $validAES = 0; + warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Vendor ID' '$idValue'. Valid value is 'AE'" + } + + state $strengths = {1 => "128-bit encryption key", + 2 => "192-bit encryption key", + 3 => "256-bit encryption key", + }; + + my $strength = out_C " Encryption Strength", sub {$strengths->{$_[0]} || "Unknown" } ; + + if (! $strengths->{$strength}) + { + $validAES = 0; + warning $FH->tell() - 1, extraFieldIdentifier($extraID) . ": Unknown 'Encryption Strength' $strength. Valid values are 1,2,3" + } + + my ($bmethod, $method) = read_v(); + out $bmethod, " Compression Method", compressionMethod($method) ; + if (! defined $ZIP_CompressionMethods{$method}) + { + $validAES = 0; + warning $FH->tell() - 2, extraFieldIdentifier($extraID) . ": Unknown 'Compression Method' ID " . decimalHex0x($method, 2) + } + + $entry->aesStrength($strength) ; + $entry->aesValid($validAES) ; +} + +sub decode_Reference +{ + # ref https://www.winzip.com/en/support/compression-methods/ + + my $len = shift; + my $entry = shift; + + out_V " CRC"; + myRead(my $uuid, 16); + # UUID is big endian + out2 $uuid, "UUID", + unpack('H*', substr($uuid, 0, 4)) . '-' . + unpack('H*', substr($uuid, 4, 2)) . '-' . + unpack('H*', substr($uuid, 6, 2)) . '-' . + unpack('H*', substr($uuid, 8, 2)) . '-' . + unpack('H*', substr($uuid, 10, 6)) ; +} + +sub decode_DUMMY +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + out_V " Data"; +} + +sub decode_GrowthHint +{ + # APPNOTE 6.3.10, sec 4.6.10 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # caller has checked that 4 bytes are available, + # so can output values without checking available space + out_v " Signature" ; + out_v " Initial Value"; + + my $padding; + myRead($padding, $len - 4); + + out2 $padding, "Padding", hexDump16($padding); + + if ($padding !~ /^\x00+$/) + { + info $FH->tell(), extraFieldIdentifier($extraID) . ": 'Padding' is not all NULL bytes"; + } +} + +sub decode_DataStreamAlignment +{ + # APPNOTE 6.3.10, sec 4.6.11 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + my $inCentralHdr = $entry->inCentralDir ; + + return if $len == 0 ; + + my ($data, $alignment) = read_v(); + + out $data, " Alignment", Value_v($alignment) ; + + my $recompress_value = $alignment & 0x8000 ? 1 : 0; + + my $recompressing = $recompress_value ? "True" : "False"; + $alignment &= 0x7FFF ; + my $hexAl = sprintf("%X", $alignment); + + out1 " [Bit 15]", "$recompress_value 'Recompress $recompressing'"; + out1 " [Bits 0-14]", "$hexAl 'Minimal Alignment $alignment'"; + + if (! $inCentralHdr && $len - 2 > 0) + { + my $padding; + myRead($padding, $len - 2); + + out2 $padding, "Padding", hexDump16($padding); + } +} + + +sub decode_UX +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + my $inCentralHdr = $entry->inCentralDir ; + + return if $len == 0 ; + + my ($data, $time) = read_V(); + out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ; + + ($data, $time) = read_V(); + out2 $data, "Mod Time", Value_V($time) . " " . getTime($time) ; + + if (! $inCentralHdr ) { + out_v " UID" ; + out_v " GID"; + } +} + +sub decode_Ux +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + return if $len == 0 ; + out_v " UID" ; + out_v " GID"; +} + +sub decodeLitteEndian +{ + my $value = shift ; + + if (length $value == 8) + { + return unpackValueQ ($value) + } + elsif (length $value == 4) + { + return unpackValue_V ($value) + } + elsif (length $value == 2) + { + return unpackValue_v ($value) + } + elsif (length $value == 1) + { + return unpackValue_C ($value) + } + else { + # TODO - fix this + internalFatal undef, "unsupported decodeLitteEndian length '" . length ($value) . "'"; + } +} + +sub decode_ux +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # caller has checked that 3 bytes are available + + return if $len == 0 ; + + my $version = out_C " Version" ; + info $FH->tell() - 1, extraFieldIdentifier($extraID) . ": 'Version' should be " . decimalHex0x(1) . ", got " . decimalHex0x($version, 1) + if $version != 1 ; + + my $available = $len - 1 ; + + my $uidSize = out_C " UID Size"; + $available -= 1; + + if ($uidSize) + { + if ($available < $uidSize) + { + outSomeData($available, " Bad Extra Data"); + error $FH->tell() - $available, + extraFieldIdentifier($extraID) . ": truncated reading 'UID'", + "Expected " . decimalHex0x($uidSize) . " bytes, got " . decimalHex0x($available) . " bytes"; + return; + } + + myRead(my $data, $uidSize); + out2 $data, "UID", decodeLitteEndian($data); + $available -= $uidSize ; + } + + if ($available < 1) + { + error $FH->tell(), + extraFieldIdentifier($extraID) . ": truncated reading 'GID Size'", + "Expected " . decimalHex0x($uidSize) . " bytes, got " . decimalHex0x($available) . " bytes"; + return ; + } + + my $gidSize = out_C " GID Size"; + $available -= 1 ; + if ($gidSize) + { + if ($available < $gidSize) + { + outSomeData($available, " Bad Extra Data"); + error $FH->tell() - $available, + extraFieldIdentifier($extraID) . ": truncated reading 'GID'", + "Expected " . decimalHex0x($gidSize) . " bytes, got " . decimalHex0x($available) . " bytes"; + return; + } + + myRead(my $data, $gidSize); + out2 $data, "GID", decodeLitteEndian($data); + $available -= $gidSize ; + } + +} + +sub decode_Java_exe +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + +} + +sub decode_up +{ + # APPNOTE 6.3.10, sec 4.6.9 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + out_C " Version"; + out_V " NameCRC32"; + + if ($len - 5 > 0) + { + myRead(my $data, $len - 5); + + outputFilename($data, 1, " UnicodeName"); + } +} + +sub decode_ASi_Unix +{ + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # https://stackoverflow.com/questions/76581811/why-does-unzip-ignore-my-zip64-end-of-central-directory-record + + out_V " CRC"; + my $native_attrib = out_v " Mode"; + + # TODO - move to separate sub & tidy + if (1) # Unix + { + + state $mask = { + 0 => '---', + 1 => '--x', + 2 => '-w-', + 3 => '-wx', + 4 => 'r--', + 5 => 'r-x', + 6 => 'rw-', + 7 => 'rwx', + } ; + + my $rwx = ($native_attrib & 0777); + + if ($rwx) + { + my $output = ''; + $output .= $mask->{ ($rwx >> 6) & 07 } ; + $output .= $mask->{ ($rwx >> 3) & 07 } ; + $output .= $mask->{ ($rwx >> 0) & 07 } ; + + out1 " [Bits 0-8]", Value_v($rwx) . " 'Unix attrib: $output'" ; + out1 " [Bit 9]", "1 'Sticky'" + if $rwx & 0x200 ; + out1 " [Bit 10]", "1 'Set GID'" + if $rwx & 0x400 ; + out1 " [Bit 11]", "1 'Set UID'" + if $rwx & 0x800 ; + + my $not_rwx = (($native_attrib >> 12) & 0xF); + if ($not_rwx) + { + state $masks = { + 0x0C => 'Socket', # 0x0C 0b1100 + 0x0A => 'Symbolic Link', # 0x0A 0b1010 + 0x08 => 'Regular File', # 0x08 0b1000 + 0x06 => 'Block Device', # 0x06 0b0110 + 0x04 => 'Directory', # 0x04 0b0100 + 0x02 => 'Character Device', # 0x02 0b0010 + 0x01 => 'FIFO', # 0x01 0b0001 + }; + + my $got = $masks->{$not_rwx} // 'Unknown Unix attrib' ; + out1 " [Bits 12-15]", Value_C($not_rwx) . " '$got'" + } + } + } + + + my $s = out_V " SizDev"; + out_v " UID"; + out_v " GID"; + +} + +sub decode_uc +{ + # APPNOTE 6.3.10, sec 4.6.8 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + out_C " Version"; + out_V " ComCRC32"; + + if ($len - 5 > 0) + { + myRead(my $data, $len - 5); + + outputFilename($data, 1, " UnicodeCom"); + } +} + +sub decode_Xceed_unicode +{ + # 0x554e + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + my $data ; + my $remaining = $len; + + # No public definition available, so reverse engineer the content. + + # See https://github.com/pmqs/zipdetails/issues/13 for C# source that populates + # this field. + + # Fiddler https://www.telerik.com/fiddler) creates this field. + + # Local Header only has UTF16LE filename + # + # Field definition + # 4 bytes Signature always XCUN + # 2 bytes Filename Length (divided by 2) + # Filename + + # Central has UTF16LE filename & comment + # + # Field definition + # 4 bytes Signature always XCUN + # 2 bytes Filename Length (divided by 2) + # 2 bytes Comment Length (divided by 2) + # Filename + # Comment + + # First 4 bytes appear to be little-endian "XCUN" all the time + # Just double check + my ($idb, $id) = read_V(); + $remaining -= 4; + + my $outid = decimalHex0x($id); + $outid .= " 'XCUN'" + if $idb eq 'NUCX'; + + out $idb, " ID", $outid; + + # Next 2 bytes contains a count of the filename length divided by 2 + # Dividing by 2 gives the number of UTF-16 characters. + my $filenameLength = out_v " Filename Length"; + $filenameLength *= 2; # Double to get number of bytes to read + $remaining -= 2; + + my $commentLength = 0; + + if ($entry->inCentralDir) + { + # Comment length only in Central Directory + # Again stored divided by 2. + $commentLength = out_v " Comment Length"; + $commentLength *= 2; # Double to get number of bytes to read + $remaining -= 2; + } + + # next is a UTF16 encoded filename + + if ($filenameLength) + { + if ($filenameLength > $remaining ) + { + myRead($data, $remaining); + out redactData($data), " UTF16LE Filename", "'" . redactFilename(decode("UTF16LE", $data)) . "'"; + + error $FH->tell() - $remaining, + extraFieldIdentifier($extraID) . ": Truncated reading 'UTF16LE Filename'", + "Expected " . decimalHex0x($filenameLength) . " bytes, got " . decimalHex0x($remaining) . " bytes"; + return undef; + } + + myRead($data, $filenameLength); + out redactData($data), " UTF16LE Filename", "'" . redactFilename(decode("UTF16LE", $data)) . "'"; + $remaining -= $filenameLength; + } + + # next is a UTF16 encoded comment + + if ($commentLength) + { + if ($commentLength > $remaining ) + { + myRead($data, $remaining); + out redactData($data), " UTF16LE Comment", "'" . redactFilename(decode("UTF16LE", $data)) . "'"; + + error $FH->tell() - $remaining, + extraFieldIdentifier($extraID) . ": Truncated reading 'UTF16LE Comment'", + "Expected " . decimalHex0x($filenameLength) . " bytes, got " . decimalHex0x($remaining) . " bytes"; + return undef; + } + + myRead($data, $commentLength); + out redactData($data), " UTF16LE Comment", "'" . redactFilename(decode("UTF16LE", $data)) . "'"; + $remaining -= $commentLength; + } + + if ($remaining) + { + outHexdump($remaining, " Unexpected Data"); + error $FH->tell() - $remaining, extraFieldIdentifier($extraID) . ": " . decimalHex0x($remaining) . " unexpected trailing bytes" ; + } +} + +sub decode_Key_Value_Pair +{ + # 0x564B 'KV' + # https://github.com/sozip/keyvaluepairs-spec/blob/master/zip_keyvalue_extra_field_specification.md + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + my $remaining = $len; + + myRead(my $signature, 13); + $remaining -= 13; + + if ($signature ne 'KeyValuePairs') + { + error $FH->tell() - 13, extraFieldIdentifier($extraID) . ": 'Signature' field not 'KeyValuePairs'" ; + myRead(my $payload, $remaining); + my $data = hexDump16($signature . $payload); + + out2 $signature . $payload, "Extra Payload", $data; + + return ; + } + + out $signature, ' Signature', "'KeyValuePairs'"; + my $kvPairs = out_C " KV Count"; + $remaining -= 1; + + for my $index (1 .. $kvPairs) + { + my $key; + my $klen = out_v " Key size #$index"; + $remaining -= 4; + + myRead($key, $klen); + outputFilename $key, 1, " Key #$index"; + $remaining -= $klen; + + my $value; + my $vlen = out_v " Value size #$index"; + $remaining -= 4; + + myRead($value, $vlen); + outputFilename $value, 1, " Value #$index"; + $remaining -= $vlen; + } + + # TODO check that + # * count of kv pairs is accurate + # * no truncation in middle of kv data + # * no trailing data +} + +sub decode_NT_security +{ + # IZ Appnote + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + my $inCentralHdr = $entry->inCentralDir ; + + out_V " Uncompressed Size" ; + + if (! $inCentralHdr) { + + out_C " Version" ; + + out_v " CType", sub { "'" . ($ZIP_CompressionMethods{$_[0]} || "Unknown Method") . "'" }; + + out_V " CRC" ; + + my $plen = $len - 4 - 1 - 2 - 4; + outHexdump $plen, " Extra Payload"; + } +} + +sub decode_MVS +{ + # APPNOTE 6.3.10, Appendix + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # data in Big-Endian + myRead(my $data, $len); + my $ID = unpack("N", $data); + + if ($ID == 0xE9F3F9F0) # EBCDIC for "Z390" + { + my $d = substr($data, 0, 4, '') ; + out($d, " ID", "'Z390'"); + } + + out($data, " Extra Payload", hexDump16($data)); +} + +sub decode_strong_encryption +{ + # APPNOTE 6.3.10, sec 4.5.12 & 7.4.2 + + my $extraID = shift ; + my $len = shift; + my $entry = shift; + + # TODO check for overflow is contents > $len + out_v " Format"; + out_v " AlgId", sub { $AlgIdLookup{ $_[0] } // "Unknown algorithm" } ; + out_v " BitLen"; + out_v " Flags", sub { $FlagsLookup{ $_[0] } // "reserved for certificate processing" } ; + + # see APPNOTE 6.3.10, sec 7.4.2 for this part + my $recipients = out_V " Recipients"; + + my $available = $len - 12; + + if ($recipients) + { + if ($available < 2) + { + outSomeData($available, " Badly formed extra data"); + # TODO - need warning + return; + } + + out_v " HashAlg", sub { $HashAlgLookup{ $_[0] } // "Unknown algorithm" } ; + $available -= 2; + + if ($available < 2) + { + outSomeData($available, " Badly formed extra data"); + # TODO - need warning + return; + } + + my $HSize = out_v " HSize" ; + $available -= 2; + + # should have $recipients * $HSize bytes available + if ($recipients * $HSize != $available) + { + outSomeData($available, " Badly formed extra data"); + # TODO - need warning + return; + } + + my $ix = 1; + for (0 .. $recipients-1) + { + myRead(my $payload, $HSize); + my $data = hexDump16($payload); + + out2 $payload, sprintf("Key #%X", $ix), $data; + ++ $ix; + } + } +} + + +sub printAes +{ + # ref https://www.winzip.com/en/support/aes-encryption/ + + my $entry = shift; + + return 0 + if ! $entry->aesValid; + + my %saltSize = ( + 1 => 8, + 2 => 12, + 3 => 16, + ); + + myRead(my $salt, $saltSize{$entry->aesStrength } // 0); + out $salt, "AES Salt", hexDump16($salt); + myRead(my $pwv, 2); + out $pwv, "AES Pwd Ver", hexDump16($pwv); + + return $saltSize{$entry->aesStrength} + 2 + 10; +} + +sub printLzmaProperties +{ + my $len = 0; + + my $b1; + my $b2; + my $buffer; + + myRead($b1, 2); + my ($verHi, $verLow) = unpack ("CC", $b1); + + out $b1, "LZMA Version", sprintf("%02X%02X", $verHi, $verLow) . " '$verHi.$verLow'"; + my $LzmaPropertiesSize = out_v "LZMA Properties Size"; + $len += 4; + + my $LzmaInfo = out_C "LZMA Info", sub { $_[0] == 93 ? "(Default)" : ""}; + + my $PosStateBits = 0; + my $LiteralPosStateBits = 0; + my $LiteralContextBits = 0; + $PosStateBits = int($LzmaInfo / (9 * 5)); + $LzmaInfo -= $PosStateBits * 9 * 5; + $LiteralPosStateBits = int($LzmaInfo / 9); + $LiteralContextBits = $LzmaInfo - $LiteralPosStateBits * 9; + + out1 " PosStateBits", $PosStateBits; + out1 " LiteralPosStateBits", $LiteralPosStateBits; + out1 " LiteralContextBits", $LiteralContextBits; + + out_V "LZMA Dictionary Size"; + + # TODO - assumption that this is 5 + $len += $LzmaPropertiesSize; + + skip($FH, $LzmaPropertiesSize - 5) + if $LzmaPropertiesSize != 5 ; + + return $len; +} + +sub peekAtOffset +{ + # my $fh = shift; + my $offset = shift; + my $len = shift; + + my $here = $FH->tell(); + + seekTo($offset) ; + + my $buffer; + myRead($buffer, $len); + seekTo($here); + + length $buffer == $len + or return ''; + + return $buffer; +} + +sub readFromOffset +{ + # my $fh = shift; + my $offset = shift; + my $len = shift; + + seekTo($offset) ; + + my $buffer; + myRead($buffer, $len); + + length $buffer == $len + or return ''; + + return $buffer; +} + +sub readSignatureFromOffset +{ + my $offset = shift ; + + # catch use case where attempting to read past EOF + # sub is expecting to return a 32-bit value so return 54-bit out-of-bound value + return MAX64 + if $offset + 4 > $FILELEN ; + + my $here = $FH->tell(); + my $buffer = readFromOffset($offset, 4); + my $gotSig = unpack("V", $buffer) ; + seekTo($here); + + return $gotSig; +} + + +sub chckForAPKSigningBlock +{ + my $fh = shift; + my $cdOffset = shift; + my $cdSize = shift; + + # APK Signing Block comes directy before the Central directory + # See https://source.android.com/security/apksigning/v2 + + # If offset less than + # + # len1 8 + # id 4 + # kv with zero len 8 + # len1 8 + # magic 16 + # ---------- + # 44 + + return (0, 0, '') + if $cdOffset < 44 || $FILELEN - $cdSize < 44 ; + + # Step 1 - 16 bytes before CD is literal string "APK Sig Block 42" + my $magicOffset = $cdOffset - 16; + my $buffer = readFromOffset($magicOffset, 16); + + return (0, 0, '') + if $buffer ne "APK Sig Block 42" ; + + # Step 2 - read the two length fields + # and check they are identical + $buffer = readFromOffset($cdOffset - 16 - 8, 8); + my $len2 = unpack("Q<", $buffer); + + return (0, 0, '') + if $len2 == 0 ; + + my $startApkOffset = $cdOffset - 8 - $len2 ; + + $buffer = readFromOffset($startApkOffset, 8); + my $len1 = unpack("Q<", $buffer); + + return (0, 0, '') + if $len1 != $len2; + + return ($startApkOffset, $cdOffset - 16 - 8, $buffer); +} + +sub scanApkBlock +{ + state $IDs = { + 0x7109871a => "APK Signature v2", + 0xf05368c0 => "APK Signature v3", + 0x42726577 => "Verity Padding Block", # from https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java + 0x6dff800d => "Source Stamp", + 0x504b4453 => "Dependency Info", + 0x71777777 => "APK Channel Block", + 0xff3b5998 => "Zero Block", + 0x2146444e => "Play Metadata", + } ; + + + seekTo($FH->tell() - 4) ; + print "\n"; + out "", "APK SIGNING BLOCK"; + + scanApkPadding(''); + out_Q "Block Length Copy #1"; + my $ix = 1; + + while ($FH->tell() < $APK - 8) + { + my ($bytes, $id, $len); + ($bytes, $len) = read_Q ; + out $bytes, "ID/Value Length #" . sprintf("%X", $ix), Value_Q($len); + + ($bytes, $id) = read_V; + + out $bytes, " ID", Value_V($id) . " '" . ($IDs->{$id} // 'Unknown ID') . "'"; + + outSomeData($len-4, " Value"); + ++ $ix; + } + + out_Q "Block Length Copy #2"; + + my $magic ; + myRead($magic, 16); + + out $magic, "Magic", qq['$magic']; +} + +sub scanApkPadding +{ + my $bytes = shift ; + + # padding + my $here = $FH->tell(); + my $got; + myRead($got, $APK - $here); + $got = $bytes . $got; + if ($got =~ /^(\x00+)($APK_LEN)?/) + { + my $len = length $1 ; + seekTo($here - length($bytes)); + outSomeData($len, "Null Padding"); + } + else + { + seekTo($here); + } +} + +sub scanCentralDirectory +{ + my $fh = shift; + + my $here = $fh->tell(); + + # Use cases + # 1 32-bit CD + # 2 64-bit CD + + my ($offset, $size) = findCentralDirectoryOffset($fh); + $CentralDirectory->{CentralDirectoryOffset} = $offset; + $CentralDirectory->{CentralDirectorySize} = $size; + + return () + if ! defined $offset; + + $fh->seek($offset, SEEK_SET) ; + + # Now walk the Central Directory Records + my $buffer ; + my $cdIndex = 0; + my $cdEntryOffset = 0; + + while ($fh->read($buffer, ZIP_CD_FILENAME_OFFSET) == ZIP_CD_FILENAME_OFFSET && + unpack("V", $buffer) == ZIP_CENTRAL_HDR_SIG) { + + my $startHeader = $fh->tell() - ZIP_CD_FILENAME_OFFSET; + + my $cdEntryOffset = $fh->tell() - ZIP_CD_FILENAME_OFFSET; + $HeaderOffsetIndex->addOffsetNoPrefix($cdEntryOffset, ZIP_CENTRAL_HDR_SIG) ; + + ++ $cdIndex ; + + my $extractVer = unpack("v", substr($buffer, 6, 1)); + my $gpFlag = unpack("v", substr($buffer, 8, 2)); + my $lastMod = unpack("V", substr($buffer, 10, 4)); + my $crc = unpack("V", substr($buffer, 16, 4)); + my $compressedSize = unpack("V", substr($buffer, 20, 4)); + my $uncompressedSize = unpack("V", substr($buffer, 24, 4)); + my $filename_length = unpack("v", substr($buffer, 28, 2)); + my $extra_length = unpack("v", substr($buffer, 30, 2)); + my $comment_length = unpack("v", substr($buffer, 32, 2)); + my $diskNumber = unpack("v", substr($buffer, 34, 2)); + my $locHeaderOffset = unpack("V", substr($buffer, 42, 4)); + + my $cdZip64 = 0; + my $zip64Sizes = 0; + + if (! full32 $locHeaderOffset) + { + # Check for corrupt offset + # 1. ponting paset EOF + # 2. offset points forward in the file + # 3. value at offset is not a CD record signature + + my $commonMessage = "'Local Header Offset' field in '" . Signatures::name(ZIP_CENTRAL_HDR_SIG) . "' is invalid"; + checkOffsetValue($locHeaderOffset, $startHeader, 0, $commonMessage, $startHeader + 42, ZIP_LOCAL_HDR_SIG, 1) ; + } + + $fh->read(my $filename, $filename_length) ; + + my $cdEntry = CentralDirectoryEntry->new(); + + $cdEntry->centralHeaderOffset($startHeader) ; + $cdEntry->localHeaderOffset($locHeaderOffset) ; + $cdEntry->compressedSize($compressedSize) ; + $cdEntry->uncompressedSize($uncompressedSize) ; + $cdEntry->extractVersion($extractVer); + $cdEntry->generalPurposeFlags($gpFlag); + $cdEntry->filename($filename) ; + $cdEntry->lastModDateTime($lastMod); + $cdEntry->languageEncodingFlag($gpFlag & ZIP_GP_FLAG_LANGUAGE_ENCODING) ; + $cdEntry->diskNumber($diskNumber) ; + $cdEntry->crc32($crc) ; + $cdEntry->zip64ExtraPresent($cdZip64) ; + + $cdEntry->std_localHeaderOffset($locHeaderOffset) ; + $cdEntry->std_compressedSize($compressedSize) ; + $cdEntry->std_uncompressedSize($uncompressedSize) ; + $cdEntry->std_diskNumber($diskNumber) ; + + + if ($extra_length) + { + $fh->read(my $extraField, $extra_length) ; + + # Check for Zip64 + my $zip64Extended = findID(0x0001, $extraField); + + if ($zip64Extended) { - out $payload, "Null Padding in Extra"; + $cdZip64 = 1; + walk_Zip64_in_CD(1, $zip64Extended, $cdEntry, 0); } - else - { - out $payload, "Malformed Extra Data", $data; + } + + $cdEntry->offsetStart($startHeader) ; + $cdEntry->offsetEnd($FH->tell() - 1); + + # don't call addEntry until after the extra fields have been scanned + # the localheader offset value may be updated in th ezip64 extra field. + $CentralDirectory->addEntry($cdEntry); + $HeaderOffsetIndex->addOffset($cdEntry->localHeaderOffset, ZIP_LOCAL_HDR_SIG) ; + + skip($fh, $comment_length ) ; + } + + $FH->seek($fh->tell() - ZIP_CD_FILENAME_OFFSET, SEEK_SET); + + # Check for Digital Signature + $HeaderOffsetIndex->addOffset($fh->tell() - 4, ZIP_DIGITAL_SIGNATURE_SIG) + if $fh->read($buffer, 4) == 4 && + unpack("V", $buffer) == ZIP_DIGITAL_SIGNATURE_SIG ; + + $CentralDirectory->sortByLocalOffset(); + $HeaderOffsetIndex->sortOffsets(); + + $fh->seek($here, SEEK_SET) ; + +} + +use constant ZIP64_END_CENTRAL_LOC_HDR_SIZE => 20; +use constant ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE => 56; + +sub offsetFromZip64 +{ + my $fh = shift ; + my $here = shift; + my $eocdSize = shift; + + #### Zip64 end of central directory locator + + # check enough bytes available for zip64 locator record + fatal_truncated "Cannot find signature for " . Signatures::nameAndHex(ZIP64_END_CENTRAL_LOC_HDR_SIG) # 'Zip64 end of central directory locator': 0x07064b50" + if $here < ZIP64_END_CENTRAL_LOC_HDR_SIZE ; + + $fh->seek($here - ZIP64_END_CENTRAL_LOC_HDR_SIZE, SEEK_SET) ; + $here = $FH->tell(); + + my $buffer; + my $got = 0; + $fh->read($buffer, ZIP64_END_CENTRAL_LOC_HDR_SIZE); + + my $gotSig = unpack("V", $buffer); + fatal_tryWalk $here - 4, sprintf("Expected signature for " . Signatures::nameAndHex(ZIP64_END_CENTRAL_LOC_HDR_SIG) . " not found, got 0x%X", $gotSig) + if $gotSig != ZIP64_END_CENTRAL_LOC_HDR_SIG ; + + $HeaderOffsetIndex->addOffset($fh->tell() - ZIP64_END_CENTRAL_LOC_HDR_SIZE, ZIP64_END_CENTRAL_LOC_HDR_SIG) ; + + + my $cd64 = unpack "Q<", substr($buffer, 8, 8); + my $totalDisks = unpack "V", substr($buffer, 16, 4); + + testPossiblePrefix($cd64, ZIP64_END_CENTRAL_REC_HDR_SIG); + + if ($totalDisks > 0) + { + my $commonMessage = "'Offset to Zip64 End of Central Directory Record' field in '" . Signatures::name(ZIP64_END_CENTRAL_LOC_HDR_SIG) . "' is invalid"; + $cd64 = checkOffsetValue($cd64, $here, 0, $commonMessage, $here + 8, ZIP64_END_CENTRAL_REC_HDR_SIG, 1) ; + } + + my $delta = $here - $cd64; + + #### Zip64 end of central directory record + + my $zip64eocd_name = "'" . Signatures::name(ZIP64_END_CENTRAL_REC_HDR_SIG) . "'"; + my $zip64eocd_name_value = Signatures::nameAndHex(ZIP64_END_CENTRAL_REC_HDR_SIG); + my $zip64eocd_value = Signatures::hexValue(ZIP64_END_CENTRAL_REC_HDR_SIG); + + # check enough bytes available + # fatal_tryWalk sprintf "Size of 'Zip64 End of Central Directory Record' 0x%X too small", $cd64 + fatal_tryWalk undef, sprintf "Size of $zip64eocd_name 0x%X too small", $cd64 + if $delta < ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE; + + # Seek to Zip64 End of Central Directory Record + $fh->seek($cd64, SEEK_SET) ; + $HeaderOffsetIndex->addOffsetNoPrefix($fh->tell(), ZIP64_END_CENTRAL_REC_HDR_SIG) ; + + $fh->read($buffer, ZIP64_END_CENTRAL_REC_HDR_MIN_SIZE) ; + + my $sig = unpack("V", substr($buffer, 0, 4)) ; + fatal_tryWalk undef, sprintf "Cannot find $zip64eocd_name: expected $zip64eocd_value but got 0x%X", $sig + if $sig != ZIP64_END_CENTRAL_REC_HDR_SIG ; + + # pkzip sets the extract zip spec to 6.2 (0x3E) to signal a v2 record + # See APPNOTE 6.3.10, section, 7.3.3 + + # Version 1 header is 44 bytes (assuming no extensible data sector) + # Version 2 header (see APPNOTE 6.3.7, section) is > 44 bytes + + my $extractSpec = unpack "C", substr($buffer, 14, 1); + my $diskNumber = unpack "V", substr($buffer, 16, 4); + my $cdDiskNumber = unpack "V", substr($buffer, 20, 4); + my $entriesOnThisDisk = unpack "Q<", substr($buffer, 24, 8); + my $totalEntries = unpack "Q<", substr($buffer, 32, 8); + my $centralDirSize = unpack "Q<", substr($buffer, 40, 8); + my $centralDirOffset = unpack "Q<", substr($buffer, 48, 8); + + if ($extractSpec >= 0x3E) + { + $opt_walk = 1; + $CentralDirectory->setPkEncryptedCD(); + } + + if (! emptyArchive($here, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirSize, $centralDirOffset)) + { + my $commonMessage = "'Offset to Central Directory' field in $zip64eocd_name is invalid"; + $centralDirOffset = checkOffsetValue($centralDirOffset, $here, 0, $commonMessage, $here + 48, ZIP_CENTRAL_HDR_SIG, 1, $extractSpec < 0x3E) ; + } + + # TODO - APPNOTE allows an extensible data sector here (see APPNOTE 6.3.10, section 4.3.14.2) -- need to take this into account + + return ($centralDirOffset, $centralDirSize) ; +} + +use constant Pack_ZIP_END_CENTRAL_HDR_SIG => pack("V", ZIP_END_CENTRAL_HDR_SIG); + +sub findCentralDirectoryOffset +{ + my $fh = shift ; + + # Most common use-case is where there is no comment, so + # know exactly where the end of central directory record + # should be. + + need ZIP_EOCD_MIN_SIZE, Signatures::name(ZIP_END_CENTRAL_HDR_SIG); + + $fh->seek(-ZIP_EOCD_MIN_SIZE(), SEEK_END) ; + my $here = $fh->tell(); + + my $is64bit = $here > MAX32; + my $over64bit = $here & (~ MAX32); + + my $buffer; + $fh->read($buffer, ZIP_EOCD_MIN_SIZE); + + my $zip64 = 0; + my $diskNumber ; + my $cdDiskNumber ; + my $entriesOnThisDisk ; + my $totalEntries ; + my $centralDirSize ; + my $centralDirOffset ; + my $commentLength = 0; + my $trailingBytes = 0; + + if ( unpack("V", $buffer) == ZIP_END_CENTRAL_HDR_SIG ) { + + $HeaderOffsetIndex->addOffset($here + $PREFIX_DELTA, ZIP_END_CENTRAL_HDR_SIG) ; + + $diskNumber = unpack("v", substr($buffer, 4, 2)); + $cdDiskNumber = unpack("v", substr($buffer, 6, 2)); + $entriesOnThisDisk= unpack("v", substr($buffer, 8, 2)); + $totalEntries = unpack("v", substr($buffer, 10, 2)); + $centralDirSize = unpack("V", substr($buffer, 12, 4)); + $centralDirOffset = unpack("V", substr($buffer, 16, 4)); + $commentLength = unpack("v", substr($buffer, 20, 2)); + } + else { + $fh->seek(0, SEEK_END) ; + + my $fileLen = $fh->tell(); + my $want = 0 ; + + while(1) { + $want += 1024 * 32; + my $seekTo = $fileLen - $want; + if ($seekTo < 0 ) { + $seekTo = 0; + $want = $fileLen ; + } + $fh->seek( $seekTo, SEEK_SET); + $fh->read($buffer, $want) ; + my $pos = rindex( $buffer, Pack_ZIP_END_CENTRAL_HDR_SIG); + + if ($pos >= 0 && $want - $pos > ZIP_EOCD_MIN_SIZE) { + $here = $seekTo + $pos ; + $HeaderOffsetIndex->addOffset($here + $PREFIX_DELTA, ZIP_END_CENTRAL_HDR_SIG) ; + + $diskNumber = unpack("v", substr($buffer, $pos + 4, 2)); + $cdDiskNumber = unpack("v", substr($buffer, $pos + 6, 2)); + $entriesOnThisDisk= unpack("v", substr($buffer, $pos + 8, 2)); + $totalEntries = unpack("v", substr($buffer, $pos + 10, 2)); + $centralDirSize = unpack("V", substr($buffer, $pos + 12, 4)); + $centralDirOffset = unpack("V", substr($buffer, $pos + 16, 4)); + $commentLength = unpack("v", substr($buffer, $pos + 20, 2)) // 0; + + my $expectedEof = $fileLen - $want + $pos + ZIP_EOCD_MIN_SIZE + $commentLength ; + # check for trailing data after end of zip + if ($expectedEof < $fileLen ) { + $TRAILING = $expectedEof ; + $trailingBytes = $FILELEN - $expectedEof ; + } + last ; } - return undef; + return undef + if $want == $fileLen; + } + } - myRead($id, ZIP_EXTRA_SUBFIELD_ID_SIZE); - $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE; - my $lookID = unpack "v", $id ; - if ($lookID == 0) - { - # check for null padding at end of extra - my $here = $FH->tell(); - my $rest; - myRead($rest, $XLEN - $offset); - if ($rest =~ /^\x00+$/) - { - out $id . $rest, "Null Padding in Extra"; - return undef; - } + $EOCD_Present = 1; - seekTo($here); + # Empty zip file can just contain an EOCD record + return (0, 0) + if ZIP_EOCD_MIN_SIZE + $commentLength + $trailingBytes == $FILELEN ; + + if (needZip64EOCDLocator($diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirOffset, $centralDirSize) && + ! emptyArchive($here, $diskNumber, $cdDiskNumber, $entriesOnThisDisk, $totalEntries, $centralDirOffset, $centralDirSize)) + { + ($centralDirOffset, $centralDirSize) = offsetFromZip64($fh, $here, ZIP_EOCD_MIN_SIZE + $commentLength + $trailingBytes) + } + elsif ($is64bit) + { + # use-case is where a 64-bit zip file doesn't use the 64-bit + # extensions. + # print "EOCD not 64-bit $centralDirOffset ($here)\n" ; + + fatal_tryWalk $here, "Zip file > 4Gig. Expected 'Offset to Central Dir' to be 0xFFFFFFFF, got " . hexValue($centralDirOffset); + + $centralDirOffset += $over64bit; + $is64In32 = 1; + } + else + { + if ($centralDirSize) + { + my $commonMessage = "'Offset to Central Directory' field in '" . Signatures::name(ZIP_END_CENTRAL_HDR_SIG) . "' is invalid"; + $centralDirOffset = checkOffsetValue($centralDirOffset, $here, $centralDirSize, $commonMessage, $here + 16, ZIP_CENTRAL_HDR_SIG, 1) ; } + } - my ($who, $decoder) = @{ $Extras{$lookID} // ['', undef ] }; + return (0, 0) + if $totalEntries == 0 && $entriesOnThisDisk == 0; - $who = "$id: $who" - if $id =~ /\w\w/ ; + ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($fh, $centralDirOffset, ZIP_EOCD_MIN_SIZE + $commentLength); - $who = "'$who'"; - out $id, "Extra ID #" . Value_v($count), unpackValue_v($id) . " $who" ; + return ($centralDirOffset, $centralDirSize) ; +} - myRead($buff, ZIP_EXTRA_SUBFIELD_LEN_SIZE); - $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE; +sub findID +{ + my $id_want = shift ; + my $data = shift; - $subLen = unpack("v", $buff); - out2 $buff, "Length", Value_v($subLen) ; + my $XLEN = length $data ; + + my $offset = 0 ; + while ($offset < $XLEN) { return undef - if $offset + $subLen > $XLEN ; + if $offset + ZIP_EXTRA_SUBFIELD_HEADER_SIZE > $XLEN ; - if (! defined $decoder) - { - if ($subLen) - { - myRead($payload, $subLen); - my $data = hexDump($payload); + my $id = substr($data, $offset, ZIP_EXTRA_SUBFIELD_ID_SIZE); + $id = unpack("v", $id); + $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE; - out2 $payload, "Extra Payload", $data; - } - } - else - { - $decoder->($subLen, $context) ; - } + my $subLen = unpack("v", substr($data, $offset, + ZIP_EXTRA_SUBFIELD_LEN_SIZE)); + $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE ; + + return undef + if $offset + $subLen > $XLEN ; + + return substr($data, $offset, $subLen) + if $id eq $id_want ; $offset += $subLen ; } @@ -1395,1026 +5948,1421 @@ sub walkExtra } -sub full32 +sub nibbles { - return $_[0] == MAX32 ; + my @nibbles = ( + [ 16 => 0x1000000000000000 ], + [ 15 => 0x100000000000000 ], + [ 14 => 0x10000000000000 ], + [ 13 => 0x1000000000000 ], + [ 12 => 0x100000000000 ], + [ 11 => 0x10000000000 ], + [ 10 => 0x1000000000 ], + [ 9 => 0x100000000 ], + [ 8 => 0x10000000 ], + [ 7 => 0x1000000 ], + [ 6 => 0x100000 ], + [ 5 => 0x10000 ], + [ 4 => 0x1000 ], + [ 4 => 0x100 ], + [ 4 => 0x10 ], + [ 4 => 0x1 ], + ); + my $value = shift ; + + for my $pair (@nibbles) + { + my ($count, $limit) = @{ $pair }; + + return $count + if $value >= $limit ; + } } -sub decode_Zip64 { - my $len = shift; - my $context = shift; + package HeaderOffsetEntry; - my $z64Data = $context->{Zip64}; - my $inCentralHdr = $context->{InCentralDir} ; - my $inLocalHdr = ! $inCentralHdr ; + sub new + { + my $class = shift ; + my $offset = shift ; + my $signature = shift; - $ZIP64 = 1; + bless [ $offset, $signature, Signatures::name($signature)] , $class; - if ($inLocalHdr || full32 $z64Data->[0]) { - out_Q " Uncompressed Size"; } - if ($inLocalHdr || full32 $z64Data->[1]) { - $context->{CompressedLength} = out_Q " Compressed Size"; + sub offset + { + my $self = shift; + return $self->[0]; } - if (full32 $z64Data->[2] ) { - $context->{LocalHdrOffset} = out_Q " Offset to Local Dir"; + sub signature + { + my $self = shift; + return $self->[1]; } - if ($z64Data->[3] == 0xFFFF ) { - out_V " Disk Number"; + sub name + { + my $self = shift; + return $self->[2]; } + } -sub Ntfs2Unix { - my $m = shift; - my $v = shift; + package HeaderOffsetIndex; - # NTFS offset is 19DB1DED53E8000 + # Store a list of header offsets recorded when scannning the central directory - my $hex = Value_Q($v) ; - $v -= 0x19DB1DED53E8000 ; - my $ns = ($v % 10000000) * 100; - my $elapse = int ($v/10000000); - return "$hex '" . getT($elapse) . - " " . sprintf("%0dns'", $ns); -} + sub new + { + my $class = shift ; -sub decode_NTFS_Filetimes -{ - my $len = shift; - my $context = shift; + my %object = ( + 'offsetIndex' => [], + 'offset2Index' => {}, + 'offset2Signature' => {}, + 'currentIndex' => -1, + 'currentSignature' => 0, + # 'sigNames' => $sigNames, + ) ; - out_V " Reserved"; - out_v " Tag1"; - out_v " Size1" ; + bless \%object, $class; + } - my ($m, $s1) = read_Q; - out $m, " Mtime", Ntfs2Unix($m, $s1); + sub sortOffsets + { + my $self = shift ; - my ($a, $s3) = read_Q; - out $m, " Atime", Ntfs2Unix($m, $s3); + @{ $self->{offsetIndex} } = sort { $a->[0] <=> $b->[0] } + @{ $self->{offsetIndex} }; + my $ix = 0; + $self->{offset2Index}{$_} = $ix++ + for @{ $self->{offsetIndex} } ; + } - my ($c, $s2) = read_Q; - out $c, " Ctime", Ntfs2Unix($m, $s2); -} + sub addOffset + { + my $self = shift ; + my $offset = shift ; + my $signature = shift ; -sub getT -{ - my $time = shift ; + $offset += $PREFIX_DELTA ; + $self->addOffsetNoPrefix($offset, $signature); + } - if ($opt_utc) - { return scalar gmtime($time) } - else - { return scalar localtime($time) } -} + sub addOffsetNoPrefix + { + my $self = shift ; + my $offset = shift ; + my $signature = shift ; -sub getTime -{ - my $time = shift ; + my $name = Signatures::name($signature); - return "'" . getT($time) . "'"; -} + if (! defined $self->{offset2Signature}{$offset}) + { + push @{ $self->{offsetIndex} }, HeaderOffsetEntry->new($offset, $signature) ; + $self->{offset2Signature}{$offset} = $signature; + } + } -sub decode_UT -{ - my $len = shift; - my $context = shift; + sub getNextIndex + { + my $self = shift ; + my $offset = shift ; - my ($data, $flags) = read_C(); + $self->{currentIndex} ++; - my $f = Value_C $flags; - $f .= " mod" if $flags & 1; - $f .= " access" if $flags & 2; - $f .= " change" if $flags & 4; + return ${ $self->{offsetIndex} }[$self->{currentIndex}] // undef + } - out $data, " Flags", "'$f'"; + sub rewindIndex + { + my $self = shift ; + my $offset = shift ; - -- $len; + $self->{currentIndex} --; + } - if ($flags & 1) + sub dump { - my ($data, $time) = read_V(); - - out2 $data, "Mod Time", Value_V($time) . " " . getTime($time) ; + my $self = shift; - $len -= 4 ; + say "### HeaderOffsetIndex"; + say "### Offset\tSignature"; + for my $x ( @{ $self->{offsetIndex} } ) + { + my ($offset, $sig) = @$x; + printf "### %X %d\t\t" . $x->name() . "\n", $x->offset(), $x->offset(); + } } + sub checkForOverlap + { + my $self = shift ; + my $need = shift; - if ($flags & 2 && $len > 0 ) - { - my ($data, $time) = read_V(); + my $needOffset = $FH->tell() + $need; - out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ; - $len -= 4 ; - } + for my $hdrOffset (@{ $self->{offsetIndex} }) + { + my $delta = $hdrOffset - $needOffset; + return [$self->{offsetIndex}{$hdrOffset}, $needOffset - $hdrOffset] + if $delta <= 0 ; + } - if ($flags & 4 && $len > 0) - { - my ($data, $time) = read_V(); + return [undef, undef]; + } - out2 $data, "Change Time", Value_V($time) . " " . getTime($time) ; - } } +{ + package FieldsAndAccessors; + + sub Add + { + use Data::Dumper ; + my $classname = shift; + my $object = shift; + my $fields = shift ; + my $no_handler = shift // {}; -sub decode_AES -{ - # ref https://www.winzip.com/win/es/aes_info.html + state $done = {}; - my $len = shift; - my $context = shift; - return if $len == 0 ; + while (my ($name, $value) = each %$fields) + { + my $method = "${classname}::$name"; - my %lookup = ( 1 => "AE-1", 2 => "AE-2"); - out_v " Vendor Version", sub { $lookup{$_[0]} || "Unknown" } ; + $object->{$name} = $value; - my $id ; - myRead($id, 2); - out $id, " Vendor ID", unpackValue_v($id) . " '$id'"; + # don't auto-create a handler + next + if $no_handler->{$name}; - my %strengths = (1 => "128-bit encryption key", - 2 => "192-bit encryption key", - 3 => "256-bit encryption key", - ); + no strict 'refs'; - my $strength = out_C " Encryption Strength", sub {$strengths{$_[0]} || "Unknown" } ; + # Don't use lvalue sub for now - vscode debugger breaks with it enabled. + # https://github.com/richterger/Perl-LanguageServer/issues/194 + # *$method = sub : lvalue { + # $_[0]->{$name} ; + # } + # unless defined $done->{$method}; - my ($bmethod, $method) = read_v(); - out $bmethod, " Compression Method", compressionMethod($method) ; + # Auto-generate getter/setter + *$method = sub { + $_[0]->{$name} = $_[1] + if @_ == 2; + return $_[0]->{$name} ; + } + unless defined $done->{$method}; - $context->{AesStrength} = $strength ; + ++ $done->{$method}; + + + } + } } -sub decode_GrowthHint { - my $len = shift; - my $context = shift; - my $inCentralHdr = $context->{InCentralDir} ; + package BaseEntry ; - return if $len == 0 ; + sub new + { + my $class = shift ; - out_v " Signature" ; - out_v " Initial Value"; + state $index = 0; + + my %fields = ( + 'index' => $index ++, + 'zip64' => 0, + 'offsetStart' => 0, + 'offsetEnd' => 0, + 'inCentralDir' => 0, + 'encapsulated' => 0, # enclosed in outer zip + 'childrenCount' => 0, # this entry is a zip with enclosed children + 'streamed' => 0, + 'languageEncodingFlag' => 0, + 'entryType' => 0, + ) ; - my $padding; - myRead($padding, $len - 4); - my $data = hexDump($padding); + my $self = bless {}, $class; + + FieldsAndAccessors::Add($class, $self, \%fields) ; - out2 $padding, "Padding", $data; + return $self; + } + + sub increment_childrenCount + { + my $self = shift; + $self->{childrenCount} ++; + } } -sub decode_DataStreamAlignment { - my $len = shift; - my $context = shift; - my $inCentralHdr = $context->{InCentralDir} ; + package LocalCentralEntryBase ; - return if $len == 0 ; + use parent -norequire , 'BaseEntry' ; - my ($data, $alignment) = read_v(); + sub new + { + my $class = shift ; - out $data, " Alignment", Value_v($alignment) ; + my $self = $class->SUPER::new(); + + + my %fields = ( + # fields from the header + 'centralHeaderOffset' => 0, + 'localHeaderOffset' => 0, + + 'extractVersion' => 0, + 'generalPurposeFlags' => 0, + 'compressedMethod' => 0, + 'lastModDateTime' => 0, + 'crc32' => 0, + 'compressedSize' => 0, + 'uncompressedSize' => 0, + 'filename' => '', + 'outputFilename' => '', + # inferred data + # 'InCentralDir' => 0, + # 'zip64' => 0, + + 'zip64ExtraPresent' => 0, + 'zip64SizesPresent' => 0, + 'payloadOffset' => 0, + + # zip64 extra + 'zip64_compressedSize' => undef, + 'zip64_uncompressedSize' => undef, + 'zip64_localHeaderOffset' => undef, + 'zip64_diskNumber' => undef, + 'zip64_diskNumberPresent' => 0, + + # Values direct from the header before merging any Zip64 values + 'std_compressedSize' => undef, + 'std_uncompressedSize' => undef, + 'std_localHeaderOffset' => undef, + 'std_diskNumber' => undef, + + # AES + 'aesStrength' => 0, + 'aesValid' => 0, + + # Minizip CD encryption + 'minizip_secure' => 0, - my $recompress_value = $alignment & 0x8000 ? 1 : 0; + ) ; - my $recompressing = $recompress_value ? "True" : "False"; - $alignment &= 0x7FFF ; - my $hexAl = sprintf("%X", $alignment); + FieldsAndAccessors::Add($class, $self, \%fields) ; - out1 " [Bit 15]", "$recompress_value 'Recompress $recompressing'"; - out1 " [Bits 0-14]", "$hexAl 'Minimal Alignment $alignment'"; + return $self; + } +} - if (! $inCentralHdr && $len - 2 > 0) +{ + package Zip64EndCentralHeaderEntry ; + + use parent -norequire , 'LocalCentralEntryBase' ; + + sub new { - my $padding; - myRead($padding, $len - 2); + my $class = shift ; + + my $self = $class->SUPER::new(); + - out2 $padding, "Padding", hexDump($padding); + my %fields = ( + 'inCentralDir' => 1, + ) ; + + FieldsAndAccessors::Add($class, $self, \%fields) ; + + return $self; } -} +} -sub decode_UX { - my $len = shift; - my $context = shift; - my $inCentralHdr = $context->{InCentralDir} ; + package CentralDirectoryEntry; - return if $len == 0 ; + use parent -norequire , 'LocalCentralEntryBase' ; - my ($data, $time) = read_V(); - out2 $data, "Access Time", Value_V($time) . " " . getTime($time) ; + sub new + { + my $class = shift ; + my $offset = shift; - ($data, $time) = read_V(); - out2 $data, "Mod Time", Value_V($time) . " " . getTime($time) ; + # check for existing entry + return $CentralDirectory->{byCentralOffset}{$offset} + if defined $offset && defined $CentralDirectory->{byCentralOffset}{$offset} ; - if (! $inCentralHdr ) { - out_v " UID" ; - out_v " GID"; - } -} + my $self = $class->SUPER::new(); -sub decode_Ux -{ - my $len = shift; - my $context = shift; + my %fields = ( + 'diskNumber' => 0, + 'comment' => "", + 'ldEntry' => undef, + ) ; - return if $len == 0 ; - out_v " UID" ; - out_v " GID"; + FieldsAndAccessors::Add($class, $self, \%fields) ; + + $self->inCentralDir(1) ; + $self->entryType(::ZIP_CENTRAL_HDR_SIG) ; + + return $self; + } } -sub decodeLitteEndian { - my $value = shift ; + package CentralDirectory; - if (length $value == 4) + sub new { - return Value_V unpack ("V", $value) - } - else { - # TODO - fix this - die "unsupported\n"; - } + my $class = shift ; - my $got = 0 ; - my $shift = 0; + my %object = ( + 'entries' => [], + 'count' => 0, + 'byLocalOffset' => {}, + 'byCentralOffset' => {}, + 'byName' => {}, + 'offset2Index' => {}, + 'normalized_filenames' => {}, + 'CentralDirectoryOffset' => 0, + 'CentralDirectorySize' => 0, + 'zip64' => 0, + 'encryptedCD' => 0, + 'minizip_secure' => 0, + 'alreadyScanned' => 0, + ) ; - #hexDump - #reverse - #my @a =unpack "C*", $value; - #@a = reverse @a; - #hexDump(@a); + bless \%object, $class; + } - for (reverse unpack "C*", $value) + sub addEntry { - $got = ($got << 8) + $_ ; - } + my $self = shift ; + my $entry = shift ; - return $got ; -} + my $localHeaderOffset = $entry->localHeaderOffset ; + my $CentralDirectoryOffset = $entry->centralHeaderOffset ; + my $filename = $entry->filename ; -sub decode_ux -{ - my $len = shift; - my $context = shift; + Nesting::add($entry); - return if $len == 0 ; - out_C " Version" ; - my $uidSize = out_C " UID Size"; - myRead(my $data, $uidSize); - out2 $data, "UID", decodeLitteEndian($data); + # Create a reference from Central to Local header entries + my $ldEntry = Nesting::getLdEntryByOffset($localHeaderOffset); + if ($ldEntry) + { + $entry->ldEntry($ldEntry) ; - my $gidSize = out_C " GID Size"; - myRead($data, $gidSize); - out2 $data, "GID", decodeLitteEndian($data); + # LD -> CD + # can have multiple LD entries point to same CD + # so need to keep a list + $ldEntry->addCdEntry($entry); + } -} + # only check for duplicate in real CD scan + if ($self->{alreadyScanned} && ! $entry->encapsulated ) + { + my $existing = $self->{byName}{$filename} ; + if ($existing && $existing->centralHeaderOffset != $entry->centralHeaderOffset) + { + ::error $CentralDirectoryOffset, + "Duplicate Central Directory entries for filename '$filename'", + "Current Central Directory entry at offset " . ::decimalHex0x($CentralDirectoryOffset), + "Duplicate Central Directory entry at offset " . ::decimalHex0x($self->{byName}{$filename}{centralHeaderOffset}); -sub decode_Java_exe -{ - my $len = shift; - my $context = shift; + # not strictly illegal to have duplicate filename, so save this one + } + else + { + my $existingNormalizedEntry = $self->normalize_filename($entry, $filename); + if ($existingNormalizedEntry) + { + ::warning $CentralDirectoryOffset, + "Portability Issue: Found case-insensitive duplicate for filename '$filename'", + "Current Central Directory entry at offset " . ::decimalHex0x($CentralDirectoryOffset), + "Duplicate Central Directory entry for filename '" . $existingNormalizedEntry->outputFilename . "' at offset " . ::decimalHex0x($existingNormalizedEntry->centralHeaderOffset); + } + } + } -} + # CD can get processed twice, so return if already processed + return + if $self->{byCentralOffset}{$CentralDirectoryOffset} ; -sub decode_up -{ - my $len = shift; - my $context = shift; + if (! $entry->encapsulated ) + { + push @{ $self->{entries} }, $entry; + $self->{byLocalOffset}{$localHeaderOffset} = $entry; + $self->{byCentralOffset}{$CentralDirectoryOffset} = $entry; + $self->{byName}{ $filename } = $entry; + $self->{offset2Index} = $self->{count} ++; + } - out_C " Version"; - out_V " NameCRC32"; + } - myRead(my $data, $len - 5); + sub exists + { + my $self = shift ; - out $data, " UnicodeName", $data; -} + return scalar @{ $self->{entries} }; + } -sub decode_Xceed_unicode -{ - my $len = shift; - my $context = shift; + sub sortByLocalOffset + { + my $self = shift ; - my $data ; + @{ $self->{entries} } = sort { $a->localHeaderOffset() <=> $b->localHeaderOffset() } + @{ $self->{entries} }; + } + + sub getByLocalOffset + { + my $self = shift ; + my $offset = shift ; + + # TODO - what happens if none exists? + my $entry = $self->{byLocalOffset}{$offset} ; + return $entry ; + } + + sub localOffset + { + my $self = shift ; + my $offset = shift ; + + # TODO - what happens if none exists? + return $self->{byLocalOffset}{$offset} ; + } - # No public definition available, so guess the fields used for this one - # Data analysis based on output from Fiddler (https://www.telerik.com/fiddler), - # which uses this field + sub getNextLocalOffset + { + my $self = shift ; + my $offset = shift ; - # First 4 bytes appear to be "NUCX" all the time - myRead($data, 4); - out $data, " ID", $data; + my $index = $self->{offset2Index} ; - # Next 2 bytes contains a count of the remaining bytes/2 - # Probably the number of UTF-16 characters, including the - # optional NULL prefix - out_v " Length"; + if ($index + 1 >= $self->{count}) + { + return 0; + } - # next is a UTF16 encode filename - # sometimes preceeded by a two Null bytes + return ${ $self->{entries} }[$index+1]->localHeaderOffset() ; + } - myRead($data, $len - 6); + sub inCD + { + my $self = shift ; + $FH->tell() >= $self->{CentralDirectoryOffset}; + } - my $prefix = substr($data, 0, 2); - if ($prefix eq "\x00\x00") + sub setPkEncryptedCD { - # Found the Null prefix - out $prefix, " Null", '0000'; - substr($data, 0, 2) = ''; + my $self = shift ; + + $self->{encryptedCD} = 1 ; + } - out $data, " UTF16LE Name", decode("UTF16LE", $data); -} + sub setMiniZipEncrypted + { + my $self = shift ; + $self->{minizip_secure} = 1 ; + } -sub decode_NT_security -{ - my $len = shift; - my $context = shift; - my $inCentralHdr = $context->{InCentralDir} ; + sub isMiniZipEncrypted + { + my $self = shift ; + return $self->{minizip_secure}; + } - out_V " Uncompressed Size" ; + sub isEncryptedCD + { + my $self = shift ; + return $self->{encryptedCD} && ! $self->{minizip_secure}; + } - if (! $inCentralHdr) { + sub normalize_filename + { + # check if there is a filename that already exists + # with the same name when normalized to lower case - out_C " Version" ; + my $self = shift ; + my $entry = shift; + my $filename = shift; - out_v " Type"; + my $nFilename = lc $filename; - out_V " NameCRC32" ; + my $lookup = $self->{normalized_filenames}{$nFilename}; + # if ($lookup && $lookup ne $filename) + if ($lookup) + { + return $lookup, + } - my $plen = $len - 4 - 1 - 2 - 4; - myRead(my $payload, $plen); - out $plen, " Extra Payload", hexDump($payload); + $self->{normalized_filenames}{$nFilename} = $entry; + + return undef; } } -sub decodeMVS { - my $len = shift; - my $context = shift; + package LocalDirectoryEntry; - # data in Big-Endian - myRead(my $data, $len); - my $ID = unpack("N", $data); + use parent -norequire , 'LocalCentralEntryBase' ; - if ($ID == 0xE9F3F9F0) + sub new { - out($data, " ID", "'Z390'"); - substr($data, 0, 4) = ''; + my $class = shift ; + + my $self = $class->SUPER::new(); + + my %fields = ( + 'streamedMatch' => 0, + 'readDataDescriptor' => 0, + 'cdEntryIndex' => {}, + 'cdEntryList' => [], + ) ; + + FieldsAndAccessors::Add($class, $self, \%fields) ; + + $self->inCentralDir(0) ; + $self->entryType(::ZIP_LOCAL_HDR_SIG) ; + + return $self; } - out($data, " Extra Payload", hexDump($data)); -} + sub addCdEntry + { + my $self = shift ; + my $entry = shift; -sub printAes -{ - # ref https://www.winzip.com/win/es/aes_info.html + # don't want encapsulated entries + # and protect against duplicates + return + if $entry->encapsulated || + $self->{cdEntryIndex}{$entry->index} ++ >= 1; - my $context = shift ; + push @{ $self->{cdEntryList} }, $entry ; + } - my %saltSize = ( - 1 => 8, - 2 => 12, - 3 => 16, - ); + sub getCdEntry + { + my $self = shift ; - myRead(my $salt, $saltSize{$context->{AesStrength} }); - out $salt, "AES Salt", hexDump($salt); - myRead(my $pwv, 2); - out $pwv, "AES Pwd Ver", hexDump($pwv); + return [] + if ! $self->{cdEntryList} ; + + return $self->{cdEntryList}[0] ; + } - return $saltSize{$context->{AesStrength}} + 2 + 10; + sub getCdEntries + { + my $self = shift ; + return $self->{cdEntryList} ; + } } -sub printLzmaProperties { - my $len = 0; - - my $b1; - my $b2; - my $buffer; - - myRead($b1, 2); - my ($verHi, $verLow) = unpack ("CC", $b1); + package LocalDirectory; - out $b1, "LZMA Version", sprintf("%02X%02X", $verHi, $verLow) . " '$verHi.$verLow'"; - my $LzmaPropertiesSize = out_v "LZMA Properties Size"; - $len += 4; + sub new + { + my $class = shift ; - my $LzmaInfo = out_C "LZMA Info", sub { $_[0] == 93 ? "(Default)" : ""}; + my %object = ( + 'entries' => [], + 'count' => 0, + 'byLocalOffset' => {}, + 'byName' => {}, + 'offset2Index' => {}, + 'normalized_filenames' => {}, + 'CentralDirectoryOffset' => 0, + 'CentralDirectorySize' => 0, + 'zip64' => 0, + 'encryptedCD' => 0, + 'streamedPresent' => 0, + ) ; - my $PosStateBits = 0; - my $LiteralPosStateBits = 0; - my $LiteralContextBits = 0; - $PosStateBits = int($LzmaInfo / (9 * 5)); - $LzmaInfo -= $PosStateBits * 9 * 5; - $LiteralPosStateBits = int($LzmaInfo / 9); - $LiteralContextBits = $LzmaInfo - $LiteralPosStateBits * 9; + bless \%object, $class; + } - out1 " PosStateBits", $PosStateBits; - out1 " LiteralPosStateBits", $LiteralPosStateBits; - out1 " LiteralContextBits", $LiteralContextBits; + sub isLocalEntryNested + { + my $self = shift ; + my $localEntry = shift; - out_V "LZMA Dictionary Size"; + return Nesting::getFirstEncapsulation($localEntry); - # TODO - assumption that this is 5 - $len += $LzmaPropertiesSize; + } - skip($FH, $LzmaPropertiesSize - 5) - if $LzmaPropertiesSize != 5 ; + sub addEntry + { + my $self = shift ; + my $localEntry = shift ; - return $len; -} + my $filename = $localEntry->filename ; + my $localHeaderOffset = $localEntry->localHeaderOffset; + my $payloadOffset = $localEntry->payloadOffset ; -sub readFromOffset -{ - # my $fh = shift; - my $offset = shift; - my $len = shift; + my $existingEntry = $self->{byName}{$filename} ; - seekTo($offset) ; + my $endSurfaceArea = $payloadOffset + ($localEntry->compressedSize //0) ; - my $buffer; - myRead($buffer, $len); - # $fh->read($buffer, $len) == $len - length $buffer == $len - or return ''; + if ($existingEntry) + { + ::error $localHeaderOffset, + "Duplicate Local Directory entry for filename '$filename'", + "Current Local Directory entry at offset " . ::decimalHex0x($localHeaderOffset), + "Duplicate Local Directory entry at offset " . ::decimalHex0x($existingEntry->localHeaderOffset), + } + else + { - return $buffer; -} + my ($existing_filename, $offset) = $self->normalize_filename($filename); + if ($existing_filename) + { + ::warning $localHeaderOffset, + "Portability Issue: Found case-insensitive duplicate for filename '$filename'", + "Current Local Directory entry at offset " . ::decimalHex0x($localHeaderOffset), + "Duplicate Local Directory entry for filename '$existing_filename' at offset " . ::decimalHex0x($offset); + } + } -sub chckForAPKSigningBlock -{ - my $fh = shift; - my $cdOffset = shift; + # keep nested local entries for zipbomb deteection + push @{ $self->{entries} }, $localEntry; - # APK Signing Block comes directy before the Central directory + $self->{byLocalOffset}{$localHeaderOffset} = $localEntry; + $self->{byName}{ $filename } = $localEntry; - # If offset less than - # - # len1 8 - # id 4 - # kv with zero len 8 - # len1 8 - # magic 16 - # ---------- - # 44 + $self->{streamedPresent} ++ + if $localEntry->streamed; - return (0, 0, '') - if $cdOffset < 44; + Nesting::add($localEntry); + } - # Step 1 - 16 bytes before CD is literal string "APK Sig Block 42" - my $magicOffset = $cdOffset - 16; - my $buffer = readFromOffset($magicOffset, 16); + sub exists + { + my $self = shift ; - return (0, 0, '') - if $buffer ne "APK Sig Block 42" ; + return scalar @{ $self->{entries} }; + } - # Step 2 - read the two length fields - # and check they are identical - $buffer = readFromOffset($cdOffset - 16 - 8, 8); - my $len2 = unpack("Q<", $buffer); + sub sortByLocalOffset + { + my $self = shift ; - return (0, 0, '') - if $len2 == 0 ; + @{ $self->{entries} } = sort { $a->localHeaderOffset() <=> $b->localHeaderOffset() } + @{ $self->{entries} }; + } - my $startApkOffset = $cdOffset - 8 - $len2 ; + sub localOffset + { + my $self = shift ; + my $offset = shift ; - $buffer = readFromOffset($startApkOffset, 8); - my $len1 = unpack("Q<", $buffer); + return $self->{byLocalOffset}{$offset} ; + } - return (0, 0, '') - if $len1 != $len2; + sub getByLocalOffset + { + my $self = shift ; + my $offset = shift ; - return ($startApkOffset, $cdOffset - 16 - 8, $buffer); -} + # TODO - what happens if none exists? + my $entry = $self->{byLocalOffset}{$offset} ; + return $entry ; + } -sub scanApkBlock -{ - state $IDs = { - 0x7109871a => "APK Signature v2", - 0xf05368c0 => "APK Signature v3", - 0x42726577 => "Verity Padding Block", # from https://android.googlesource.com/platform/tools/apksig/+/master/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java - 0x6dff800d => "Source Stamp", - 0x504b4453 => "Dependency Info", - 0x71777777 => "APK Channel Block", - 0xff3b5998 => "Zero Block", - 0x2146444e => "Play Metadata", - } ; + sub getNextLocalOffset + { + my $self = shift ; + my $offset = shift ; + my $index = $self->{offset2Index} ; - seekTo($FH->tell() - 4) ; - print "\nAPK SIGNING BLOCK\n"; + if ($index + 1 >= $self->{count}) + { + return 0; + } - scanApkPadding(''); - out_Q "Block Length Copy #1"; - my $ix = 1; + return ${ $self->{entries} }[$index+1]->localHeaderOffset ; + } - while ($FH->tell() < $APK - 8) + sub lastStreamedEntryAdded { - my ($bytes, $id, $len); - ($bytes, $len) = read_Q ; - out $bytes, "ID/Value Length #" . sprintf("%X", $ix), Value_Q($len); - - ($bytes, $id) = read_V; + my $self = shift ; + my $offset = shift ; - out $bytes, " ID", Value_V($id) . " '" . ($IDs->{$id} // 'Unknown ID') . "'"; + for my $entry ( reverse @{ $self->{entries} } ) + { + if ($entry->streamed)# && ! $entry->streamedMatch) + { + $entry->streamedMatch($entry->streamedMatch + 1) ; + return $entry; + } + } - outSomeData($len-4, " Value"); - ++ $ix; + return undef; } - out_Q "Block Length Copy #2"; + sub inCD + { + my $self = shift ; + $FH->tell() >= $self->{CentralDirectoryOffset}; + } - my $magic ; - myRead($magic, 16); + sub setPkEncryptedCD + { + my $self = shift ; - out $magic, "Magic", qq['$magic']; -} + $self->{encryptedCD} = 1 ; -sub scanApkPadding -{ - my $bytes = shift ; + } - # padding - my $here = $FH->tell(); - my $got; - myRead($got, $APK - $here); - $got = $bytes . $got; - if ($got =~ /^(\x00+)($APK_LEN)?/) + sub isEncryptedCD { - my $len = length $1 ; - seekTo($here - length($bytes)); - outSomeData($len, "Null Padding"); + my $self = shift ; + return $self->{encryptedCD} ; } - else + + sub anyStreamedEntries { - seekTo($here); + my $self = shift ; + return $self->{streamedPresent} ; } + sub normalize_filename + { + # check if there is a filename that already exists + # with the same name when normalized to lower case + + my $self = shift ; + my $filename = shift; + + my $nFilename = lc $filename; + + my $lookup = $self->{normalized_filenames}{$nFilename}; + if ($lookup && $lookup ne $filename) + { + return $self->{byName}{$lookup}{outputFilename}, + $self->{byName}{$lookup}{localHeaderOffset} + } + + $self->{normalized_filenames}{$nFilename} = $filename; + + return undef, undef; + } } -sub scanCentralDirectory { - my $fh = shift; + package Eocd ; - my $here = $fh->tell(); - - # Use cases - # 1 32-bit CD - # 2 64-bit CD + sub new + { + my $class = shift ; - my @CD = (); - my $offset = findCentralDirectoryOffset($fh); + my %object = ( + 'zip64' => 0, + ) ; - return () - if ! defined $offset; + bless \%object, $class; + } +} - $fh->seek($offset, SEEK_SET) ; +sub displayFileInfo +{ + return; - # Now walk the Central Directory Records - my $buffer ; - my $cdIndex = 0; - while ($fh->read($buffer, 46) == 46 && - unpack("V", $buffer) == ZIP_CENTRAL_HDR_SIG) { + my $filename = shift; - my $cdEntryOffset = $fh->tell() - 46; - ++ $cdIndex ; + info undef, + "Filename : '$filename'", + "Size : " . (-s $filename) . " (" . decimalHex0x(-s $filename) . ")", + # "Native Encoding: '" . TextEncoding::getNativeLocaleName() . "'", +} - my $compressedLength = unpack("V", substr($buffer, 20, 4)); - my $uncompressedLength = unpack("V", substr($buffer, 24, 4)); - my $filename_length = unpack("v", substr($buffer, 28, 2)); - my $extra_length = unpack("v", substr($buffer, 30, 2)); - my $comment_length = unpack("v", substr($buffer, 32, 2)); - my $locHeaderOffset = unpack("V", substr($buffer, 42, 4)); - my $cdZip64 = 0; - my $zip64Sizes = 0; +{ + package TextEncoding; - skip($fh, $filename_length ) ; + my $nativeLocaleEncoding = getNativeLocale(); + my $opt_EncodingFrom = $nativeLocaleEncoding; + my $opt_EncodingTo = $nativeLocaleEncoding ; + my $opt_Encoding_Enabled; + my $opt_Debug_Encoding; + my $opt_use_LanguageEncodingFlag; - if ($extra_length) - { - $fh->read(my $extraField, $extra_length) ; - # $self->smartReadExact(\$extraField, $extra_length); + sub setDefaults + { + $nativeLocaleEncoding = getNativeLocale(); + $opt_EncodingFrom = $nativeLocaleEncoding; + $opt_EncodingTo = $nativeLocaleEncoding ; + $opt_Encoding_Enabled = 1; + $opt_Debug_Encoding = 0; + $opt_use_LanguageEncodingFlag = 1; + } - # Check for Zip64 - # my SizesExtended = findID("\x01\x00", $extraField); - my $zip64Extended = findID(0x0001, $extraField); + sub getNativeLocale + { + state $enc; - if ($zip64Extended) + if (! defined $enc) + { + eval { - $cdZip64 = 1; - - if ($uncompressedLength == MAX32) + require encoding ; + my $encoding = encoding::_get_locale_encoding() ; + if (! $encoding) { - $uncompressedLength = unpack "Q<", substr($zip64Extended, 0, 8, ""); - $zip64Sizes = 1; + # CP437 is the legacy default for zip files + $encoding = 'cp437'; + # ::warning undef, "Cannot determine system charset: defaulting to '$encoding'" } - if ($compressedLength == MAX32) - { - $compressedLength = unpack "Q<", substr($zip64Extended, 0, 8, ""); - $zip64Sizes = 1; + $enc = Encode::find_encoding($encoding) ; + } ; + } - } - if ($locHeaderOffset == MAX32) - { - $locHeaderOffset = unpack "Q<", substr($zip64Extended, 0, 8, ""); - } + return $enc; + } + + sub getNativeLocaleName + { + state $name; + + return $name + if defined $name ; + + if (! defined $name) + { + my $enc = getNativeLocale(); + if ($enc) + { + $name = $enc->name() + } + else + { + $name = 'unknown' } } - my $got = [$locHeaderOffset, $compressedLength, $cdZip64, $zip64Sizes, $cdIndex, $cdEntryOffset] ; + return $name ; + } - skip($fh, $comment_length ) ; + sub parseEncodingOption + { + my $opt_name = shift; + my $opt_value = shift; - push @CD, $got ; + my $enc = Encode::find_encoding($opt_value) ; + die "Encoding '$opt_value' not found for option '$opt_name'\n" + unless ref $enc; + + if ($opt_name eq 'encoding') + { + $opt_EncodingFrom = $enc; + } + elsif ($opt_name eq 'output-encoding') + { + $opt_EncodingTo = $enc; + } + else + { + die "Unknown option $opt_name\n" + } } - $fh->seek($here, SEEK_SET) ; + sub NoEncoding + { + my $opt_name = shift; + my $opt_value = shift; - # @CD = sort { $a->[0]->cmp($b->[0]) } @CD ; - @CD = sort { $a->[0] <=> $b->[0] } @CD ; + $opt_Encoding_Enabled = 0 ; + } - # Set the first Local File Header offset. - $START = $CD[0]->[0] - if @CD ; + sub LanguageEncodingFlag + { + my $opt_name = shift; + my $opt_value = shift; - return (1, $offset, @CD); -} + $opt_use_LanguageEncodingFlag = $opt_value ; + } + sub debugEncoding + { + if (@_) + { + $opt_Debug_Encoding = 1 ; + } -sub offsetFromZip64 -{ - my $fh = shift ; - my $here = shift; + return $opt_Debug_Encoding ; + } - $fh->seek($here - 20, SEEK_SET) - # TODO - fix this - or die "xx $!" ; + sub encodingInfo + { + return + unless $opt_Encoding_Enabled && $opt_Debug_Encoding ; - my $buffer; - my $got = 0; - ($got = $fh->read($buffer, 20)) == 20 - # TODO - fix this - or die "xxx + my $enc = TextEncoding::getNativeLocaleName(); + my $from = $opt_EncodingFrom->name(); + my $to = $opt_EncodingTo->name(); + + ::debug undef, "Debug Encoding Enabled", + "System Default Encoding: '$enc'", + "Encoding used when reading from zip file: '$from'", + "Encoding used for display output: '$to'"; + } + sub cleanEval + { + chomp $_[0] ; + $_[0] =~ s/ at .+ line \d+\.$// ; + return $_[0]; + } - $here $got $!" ; + sub decode + { + my $name = shift ; + my $type = shift ; + my $LanguageEncodingFlag = shift ; - if ( unpack("V", $buffer) == ZIP64_END_CENTRAL_LOC_HDR_SIG ) { - my $cd64 = unpack "Q<", substr($buffer, 8, 8); + return $name + if ! $opt_Encoding_Enabled ; - $fh->seek($cd64, SEEK_SET) ; + # TODO - check for badly formed content + if ($LanguageEncodingFlag && $opt_use_LanguageEncodingFlag) + { + eval { $name = Encode::decode('utf8', $name, Encode::FB_CROAK ) } ; + ::warning $FH->tell() - length $name, "Could not decode 'utf8' $type: " . cleanEval $@ + if $@ ; + } + else + { + eval { $name = $opt_EncodingFrom->decode($name, Encode::FB_CROAK ) } ; + ::warning $FH->tell() - length $name, "Could not decode '" . $opt_EncodingFrom->name() . "' $type: " . cleanEval $@ + if $@; + } - $fh->read($buffer, 4) == 4 - # TODO - fix this - or die "xxx" ; + # remove any BOM + $name =~ s/^\x{FEFF}//; - if ( unpack("V", $buffer) == ZIP64_END_CENTRAL_REC_HDR_SIG ) { + return $name ; + } - $fh->read($buffer, 8) == 8 - # TODO - fix this - or die "xxx" ; - my $size = unpack "Q<", $buffer; - $fh->read($buffer, $size) == $size - # TODO - fix this - or die "xxx" ; + sub encode + { + my $name = shift ; + my $type = shift ; + my $LanguageEncodingFlag = shift ; - my $cd64 = unpack "Q<", substr($buffer, 36, 8); + return $name + if ! $opt_Encoding_Enabled; - return $cd64 ; + if ($LanguageEncodingFlag && $opt_use_LanguageEncodingFlag) + { + eval { $name = Encode::encode('utf8', $name, Encode::FB_CROAK ) } ; + ::warning $FH->tell() - length $name, "Could not encode 'utf8' $type: " . cleanEval $@ + if $@ ; + } + else + { + eval { $name = $opt_EncodingTo->encode($name, Encode::FB_CROAK ) } ; + ::warning $FH->tell() - length $name, "Could not encode '" . $opt_EncodingTo->name() . "' $type: " . cleanEval $@ + if $@; } - die "Cannot find 'Zip64 end of central directory record': 0x06054b50\nTry running with --scan option.\n" ; + return $name; } - - die "Cannot find signature for 'Zip64 end of central directory locator': 0x07064b50 \nTry running with --scan option.\n" ; } -use constant Pack_ZIP_END_CENTRAL_HDR_SIG => pack("V", ZIP_END_CENTRAL_HDR_SIG); - -sub findCentralDirectoryOffset { - my $fh = shift ; + package Nesting; - # Most common use-case is where there is no comment, so - # know exactly where the end of central directory record - # should be. + use Data::Dumper; - $fh->seek(-22, SEEK_END) ; - my $here = $fh->tell(); + my @nestingStack = (); + my %encapsulations; + my %inner2outer; + my $encapsulationCount = 0; + my %index2entry ; + my %offset2entry ; - my $is64bit = $here > MAX32; - my $over64bit = $here & (~ MAX32); + # my %localOffset2cdEntry; - my $buffer; - $fh->read($buffer, 22) == 22 - # TODO - fix this - or die "xxx" ; + sub clearStack + { + @nestingStack = (); + %encapsulations = (); + %inner2outer = (); + %index2entry = (); + %offset2entry = (); + $encapsulationCount = 0; + } - my $zip64 = 0; - my $centralDirOffset ; + sub dump + { + my $indent = shift // 0; - if ( unpack("V", $buffer) == ZIP_END_CENTRAL_HDR_SIG ) { - $centralDirOffset = unpack("V", substr($buffer, 16, 4)); + for my $offset (sort {$a <=> $b} keys %offset2entry) + { + my $leading = " " x $indent ; + say $leading . "\nOffset $offset" ; + say Dumper($offset2entry{$offset}) + } } - else { - $fh->seek(0, SEEK_END) ; - my $fileLen = $fh->tell(); - my $want = 0 ; + sub add + { + my $entry = shift; - while(1) { - $want += 1024 * 32; - my $seekTo = $fileLen - $want; - if ($seekTo < 0 ) { - $seekTo = 0; - $want = $fileLen ; - } - $fh->seek( $seekTo, SEEK_SET) - # TODO - fix this - or die "xxx $!" ; - my $got; - ($got = $fh->read($buffer, $want)) == $want - # TODO - fix this - or die "xxx $got $!" ; - my $pos = rindex( $buffer, Pack_ZIP_END_CENTRAL_HDR_SIG); + getEnclosingEntry($entry); + push @nestingStack, $entry; + $index2entry{ $entry->index } = $entry; + $offset2entry{ $entry->offsetStart } = $entry; + } - if ($pos >= 0 && $want - $pos > 22) { - $here = $seekTo + $pos ; - $centralDirOffset = unpack("V", substr($buffer, $pos + 16, 4)); - my $commentLength = unpack("V", substr($buffer, $pos + 20, 2)); - $commentLength = 0 if ! defined $commentLength ; + sub getEnclosingEntry + { + my $entry = shift; - my $expectedEof = $fileLen - $want + $pos + 22 + $commentLength ; - # check for trailing data after end of zip - if ($expectedEof < $fileLen ) { - $TRAILING = $expectedEof ; - } - last ; - } + my $filename = $entry->filename; - return undef - if $want == $fileLen; + pop @nestingStack + while @nestingStack && $entry->offsetStart > $nestingStack[-1]->offsetEnd ; + + my $match = undef; + + if (@nestingStack && + $entry->offsetStart >= $nestingStack[-1]->offsetStart && + $entry->offsetEnd <= $nestingStack[-1]->offsetEnd && + $entry->index != $nestingStack[-1]->index) + { + # Nested entry found + $match = $nestingStack[-1]; + push @{ $encapsulations{ $match->index } }, $entry; + $inner2outer{ $entry->index} = $match->index; + ++ $encapsulationCount; + + $entry->encapsulated(1) ; + $match->increment_childrenCount(); + + if ($NESTING_DEBUG) + { + say "#### nesting " . (caller(1))[3] . " index #" . $entry->index . ' "' . + $entry->outputFilename . '" [' . $entry->offsetStart . "->" . $entry->offsetEnd . "]" . + " in #" . $match->index . ' "' . + $match->outputFilename . '" [' . $match->offsetStart . "->" . $match->offsetEnd . "]" ; + } } - } - if (full32 $centralDirOffset) - { - $centralDirOffset = offsetFromZip64($fh, $here) + return $match; } - elsif ($is64bit) + + sub isNested { - # use-case is where a 64-bit zip file doesn't use the 64-bit - # extensions. - print "EOCD not 64-bit $centralDirOffset ($here)\n" ; + my $offsetStart = shift; + my $offsetEnd = shift; - push @Messages, - sprintf "Zip file > 4Gig. Expected 'Offset to Central Dir' to be 0xFFFFFFFF, got 0x%X\n", $centralDirOffset; + if ($NESTING_DEBUG) + { + say "### Want: offsetStart " . ::decimalHex0x($offsetStart) . " offsetEnd " . ::decimalHex0x($offsetEnd); + for my $entry (@nestingStack) + { + say "### Have: offsetStart " . ::decimalHex0x($entry->offsetStart) . " offsetEnd " . ::decimalHex0x($entry->offsetEnd); + } + } - $centralDirOffset += $over64bit; - $is64In32 = 1; - } + return 0 + unless @nestingStack ; - ($START_APK, $APK, $APK_LEN) = chckForAPKSigningBlock($fh, $centralDirOffset); + my @copy = @nestingStack ; - return $centralDirOffset ; -} + pop @copy + while @copy && $offsetStart > $copy[-1]->offsetEnd ; -sub findID -{ - my $id_want = shift ; - my $data = shift; + return @copy && + $offsetStart >= $copy[-1]->offsetStart && + $offsetEnd <= $copy[-1]->offsetEnd ; + } - my $XLEN = length $data ; + sub getOuterEncapsulation + { + my $entry = shift; - my $offset = 0 ; - while ($offset < $XLEN) { + my $outerIndex = $inner2outer{ $entry->index } ; return undef - if $offset + ZIP_EXTRA_SUBFIELD_HEADER_SIZE > $XLEN ; + if ! defined $outerIndex ; - my $id = substr($data, $offset, ZIP_EXTRA_SUBFIELD_ID_SIZE); - $id = unpack("v", $id); - $offset += ZIP_EXTRA_SUBFIELD_ID_SIZE; + return $index2entry{$outerIndex} // undef; + } - my $subLen = unpack("v", substr($data, $offset, - ZIP_EXTRA_SUBFIELD_LEN_SIZE)); - $offset += ZIP_EXTRA_SUBFIELD_LEN_SIZE ; + sub getEncapsulations + { + my $entry = shift; - return undef - if $offset + $subLen > $XLEN ; + return $encapsulations{ $entry->index } ; + } - return substr($data, $offset, $subLen) - if $id eq $id_want ; + sub getFirstEncapsulation + { + my $entry = shift; - $offset += $subLen ; + my $got = $encapsulations{ $entry->index } ; + + return defined $got ? $$got[0] : undef; } - return undef ; -} + sub encapsulations + { + return \%encapsulations; + } + sub encapsulationCount + { + return $encapsulationCount; + } -sub _dosToUnixTime -{ - my $dt = shift; + sub childrenInCentralDir + { + # find local header entries that have children that are not referenced in the CD + # tis means it is likely a benign nextd zip file + my $entry = shift; - my $year = ( ( $dt >> 25 ) & 0x7f ) + 80; - my $mon = ( ( $dt >> 21 ) & 0x0f ) - 1; - my $mday = ( ( $dt >> 16 ) & 0x1f ); + for my $child (@{ $encapsulations{$entry->index} } ) + { + next + unless $child->entryType == ::ZIP_LOCAL_HDR_SIG ; - my $hour = ( ( $dt >> 11 ) & 0x1f ); - my $min = ( ( $dt >> 5 ) & 0x3f ); - my $sec = ( ( $dt << 1 ) & 0x3e ); + return 1 + if @{ $child->cdEntryList }; + } + return 0; + } - use POSIX 'mktime'; + sub entryByIndex + { + my $index = shift; + return $index2entry{$index}; + } - # Force mktime to return a UTC time - local $ENV{TZ} = "UTC"; + sub getEntryByOffset + { + my $offset = shift; + return $offset2entry{$offset}; + } - my $time_t = mktime( $sec, $min, $hour, $mday, $mon, $year, 0, 0, -1 ); - return 0 if ! defined $time_t; - return $time_t; -} + sub getLdEntryByOffset + { + my $offset = shift; + my $entry = $offset2entry{$offset}; -sub nibbles -{ - my @nibbles = ( - [ 16 => 0x1000000000000000 ], - [ 15 => 0x100000000000000 ], - [ 14 => 0x10000000000000 ], - [ 13 => 0x1000000000000 ], - [ 12 => 0x100000000000 ], - [ 11 => 0x10000000000 ], - [ 10 => 0x1000000000 ], - [ 9 => 0x100000000 ], - [ 8 => 0x10000000 ], - [ 7 => 0x1000000 ], - [ 6 => 0x100000 ], - [ 5 => 0x10000 ], - [ 4 => 0x1000 ], - [ 4 => 0x100 ], - [ 4 => 0x10 ], - [ 4 => 0x1 ], - ); - my $value = shift ; + return $entry + if $entry && $entry->entryType == ::ZIP_LOCAL_HDR_SIG; - for my $pair (@nibbles) - { - my ($count, $limit) = @{ $pair }; + return undef; + } - return $count - if $value >= $limit ; + sub getEntriesByOffset + { + return \%offset2entry ; } } - { - package Seen; + package SimpleTable ; + + use List::Util qw(max sum); sub new { - my $class = shift ; - - my %object = ( overlaps => [], - duplicates => [], - detail => [], - duplicate_count => 0, - overlap_count => 0, - ) ; + my $class = shift; + my %object = ( + header => [], + data => [], + columns => 0, + prefix => '# ', + ); bless \%object, $class; } - sub save + sub addHeaderRow { - my $self = shift ; - my $hdrId = shift; - my $name = shift; - my $extras = shift; + my $self = shift; + push @{ $self->{header} }, [ @_ ] ; + $self->{columns} = max($self->{columns}, scalar @_ ) ; + } + + sub addDataRow + { + my $self = shift; + + push @{ $self->{data} }, [ @_ ] ; + $self->{columns} = max($self->{columns}, scalar @_ ) ; + } - my $from_offset = $extras->{LocalHdrOffset}; - my $to_offset = $from_offset ; - $to_offset += $extras->{CompressedLength} - 1 ; + sub hasData + { + my $self = shift; - my $crc = $extras->{CRC}; + return scalar @{ $self->{data} } ; + } - $name = ::redactFilename($name) ; + sub display + { + my $self = shift; - for my $entry ( @{ $self->{detail} } ) + # work out the column widths + my @colW = (0) x $self->{columns} ; + for my $row (@{ $self->{data} }, @{ $self->{header} }) { - if ( $from_offset == $entry->{from} && $to_offset == $entry->{to} && $crc == $entry->{crc}) + my @r = @$row; + for my $ix (0 .. $self->{columns} -1) { - $self->{duplicate_count} ++; - print "$hdrId: '$name' matches with $entry->{str}\n" - } - elsif ( ($from_offset >= $entry->{from} && $from_offset <= $entry->{to} ) || - ($to_offset >= $entry->{from} && $to_offset <= $entry->{to} ) - ) - { - # die "overlap!" - # push @{ $self->{overlap} }, - $self->{overlap_count} ++; - - print "$hdrId: '$name' overlaps with $entry->{str}\n"; + $colW[$ix] = max($colW[$ix], + 3 + length( $r[$ix] ) + ); } } - # warn "ADD $from_offset $to_offset $hdrId: $name\n"; - push @{ $self->{detail} }, - { - from => $from_offset, - to => $to_offset, - length => $extras->{CompressedLength}, - id => $hdrId, - crc => $crc, - name => $name, - str => "$hdrId: '$name'", - } ; - } - - sub short_summary - { - my $self = shift; + my $width = sum(@colW) ; #+ @colW ; + my @template ; + for my $w (@colW) + { + push @template, ' ' x ($w - 3); + } - my $duplicates = $self->{duplicate_count}; - push @Messages, "$duplicates duplicate entries" - if $duplicates; + print $self->{prefix} . '-' x ($width + 1) . "\n"; - my $overlaps = $self->{overlap_count}; - push @Messages, "$overlaps overlap entries" - if $overlaps; - } + for my $row (@{ $self->{header} }) + { + my @outputRow = @template; - sub summary - { - my $self = shift ; + print $self->{prefix} . '| '; + for my $ix (0 .. $self->{columns} -1) + { + my $field = $template[$ix] ; + substr($field, 0, length($row->[$ix]), $row->[$ix]); + print $field . ' | '; + } + print "\n"; - # disable for now - return; + } - print "\n"; - for my $entry ( sort { $a->{from}->cmp($b->{from}) } @{ $self->{detail} } ) + print $self->{prefix} . '-' x ($width + 1) . "\n"; + for my $row (@{ $self->{data} }) { - my $from_offset = $entry->{from}; - my $to_offset = $entry->{to}; - my $hdrId = $entry->{id}; - my $name = $entry->{name}; - print "$hdrId\t" . $entry->{from} . "\n"; - + my @outputRow = @template; + print $self->{prefix} . '| '; + for my $ix (0 .. $self->{columns} -1) + { + my $field = $template[$ix] ; + substr($field, 0, length($row->[$ix]), $row->[$ix]); + print $field . ' | '; + } + print "\n"; } - } + print $self->{prefix} . '-' x ($width + 1) . "\n"; + print "#\n"; + } } sub Usage { - if (@_) - { - warn "$_\n" - for @_ ; - warn "\n"; - } + my $enc = TextEncoding::getNativeLocaleName(); - die < file +files. You should have a copy of the zip file definition, +L, at hand to help understand the output from this program. =head2 Default Behaviour By default the program expects to be given a well-formed zip file. It will -navigate the Zip file by first parsing the zip central directory at the end -of the file. If that is found, it will then walk through the zip records -starting at the beginning of the file. Any badly formed zip data structures -encountered are likely to terminate the program. - -If the program finds any structural problems with the zip file it will -print a summary at the end of the output report. The set of error cases -reported is very much a work in progress, so don't rely on this feature to -find all the possible errors in a zip file. If you have suggestions for -use-cases where this could be enhanced please consider creating an -enhancement request (see L<"SUPPORT">). - -Date/time fields are found in zip files are displayed in local time. Use -the C<--utc> option to display these fields in Coordinated Universal Time -(UTC). - -=head2 Scan-Mode - -If you do have a potentially corrupt zip file, particulatly where the -central directory at the end of the file is absent/incomplete, you can try -usng the C<--scan> option to search for zip records that are still present. - -When Scan-mode is enabled, the program will walk the zip file from the -start, blindly looking for the 4-byte signatures that preceed each of the -zip data structures. If it finds any of the recognised signatures it will -attempt to dump the associated zip record. For very large zip files, this -operation can take a long time to run. - -Note that the 4-byte signatures used in zip files can sometimes match with -random data stored in the zip file, so care is needed interpreting the -results. +navigate the zip file by first parsing the zip C at the end +of the file. If the C is found, it will then walk +sequentally through the zip records starting at the beginning of the file. +See L for other processing options. + +If the program finds any structural or portability issues with the zip file +it will print a message at the point it finds the issue and/or in a summary +at the end of the output report. Whilst the set of issues that can be +detected it exhaustive, don't assume that this program can find I the +possible issues in a zip file - there are likely edge conditions that need +to be addressed. + +If you have suggestions for use-cases where this could be enhanced please +consider creating an enhancement request (see L<"SUPPORT">). + +=head3 Date & Time fields + +Date/time fields found in zip files are displayed in local time. Use the +C<--utc> option to display these fields in Coordinated Universal Time (UTC). + +=head3 Filenames & Comments + +Filenames and comments are decoded/encoded using the default system +encoding of the host running C. When the sytem encoding cannot +be determined C will be used. + +The exceptions are + +=over 5 + +=item * + +when the C is set in the zip file, the +filename/comment fields are assumed to be encoded in UTF-8. + +=item * + +the definition for the metadata field implies UTF-8 charset encoding + +=back + +See L<"Filename Encoding Issues"> and L for ways to control the encoding of filename/comment fields. =head2 OPTIONS +=head3 General Options + =over 5 -=item -h +=item C<-h>, C<--help> Display help -=item --redact +=item C<--redact> -Obscure filenames in the output. Handy for the use case where the zip files -contains sensitive data that cannot be shared. +Obscure filenames and payload data in the output. Handy for the use case +where the zip files contains sensitive data that cannot be shared. -=item --scan +=item C<--scan> -Walk the zip file loking for possible zip records. Can be error-prone. -See L<"Scan-Mode"> +Pessimistically scan the zip file loking for possible zip records. Can be +error-prone. For very large zip files this option is slow. Consider using +the C<--walk> option first. See L<"Advanced Analysis Options"> -=item --utc +=item C<--utc> -By default, date/time fields are displayed in local time. Use this option -to display them in in Coordinated Universal Time (UTC). +By default, date/time fields are displayed in local time. Use this option to +display them in in Coordinated Universal Time (UTC). -=item -v +=item C<-v> Enable Verbose mode. See L<"Verbose Output">. -=item --version +=item C<--version> Display version number of the program and exit. +=item C<--walk> + +Optimistically walk the zip file looking for possible zip records. +See L<"Advanced Analysis Options"> + +=back + +=head3 Filename & Comment Encoding Options + +See L<"Filename Encoding Issues"> + +=over 5 + +=item C<--encoding name> + +Use encoding "name" when reading filenames/comments from the zip file. + +When this option is not specified the default the system encoding is used. + +=item C< --no-encoding> + +Disable all filename & comment encoding/decoding. Filenames/comments are +processed as byte streams. + +This option is not enabled by default. + +=item C<--output-encoding name> + +Use encoding "name" when writing filename/comments to the display. By +default the system encoding will be used. + +=item C<--language-encoding>, C<--no-language-encoding> + +Modern zip files set a metadata entry in zip files, called the "Language +encoding flag", when they write filenames/comments encoded in UTF-8. + +Occasionally some applications set the C but write +data that is not UTF-8 in the filename/comment fields of the zip file. This +will usually result in garbled text being output for the +filenames/comments. + +To deal with this use-case, set the C<--no-language-encoding> option and, +if needed, set the C<--encoding name> option to encoding actually used. + +Default is C<--language-encoding>. + +=item C<--debug-encoding> + +Display extra debugging info when a filename/comment encoding has changed. + +=back + +=head3 Message Control Options + +=over 5 + +=item C<--messages>, C<--no-messages> + +Enable/disable the output of all info/warning/error messages. + +Disabling messages means that no checks are carried out to check that the +zip file is well-formed. + +Default is enabled. + +=item C<--exit-bitmask>, C<--no-exit-bitmask> + +Enable/disable exit status bitmask for messages. Default disabled. +Bitmask values are: 1 for info, 2 for warning and 4 for error. + =back + =head2 Default Output -By default zipdetails will output the details of the zip file in three -columns. +By default C will output each metadata field from the zip file +in three columns. + +=over 5 + +=item 1 + +The offset, in hex, to the start of the field relative to the beginning of +the file. + +=item 2 + +The name of the field. + +=item 3 + +Detailed information about the contents of the field. The format depends on +the type of data: =over 5 -=item Column 1 +=item * Numeric Values + +If the field contains an 8-bit, 16-bit, 32-bit or 64-bit numeric value, it +will be displayed in both hex and decimal -- for example "C<002A (42)>". -This contains the offset from the start of the file in hex. +Note that Zip files store most numeric values in I encoding +(there area few rare instances where I is used). The value read +from the zip file will have the I encoding removed before being +displayed. -=item Column 2 +Next, is an optional description of what the numeric value means. -This contains a textual description of the field. +=item * String -=item Column 3 +If the field corresponds to a printable string, it will be output enclosed +in single quotes. -If the field contains a numeric value it will be displayed in hex. Zip -stores most numbers in little-endian format - the value displayed will have -the little-endian encoding removed. +=item * Binary Data -Next, is an optional description of what the value means. +The term I is just a catch-all for all other metadata in the +zip file. This data is displayed as a series of ascii-hex byte values in +the same order they are stored in the zip file. =back -For example, assuming you have a zip file with two entries, like this +=back + +For example, assuming you have a zip file, C, with one entry - $ unzip -l test.zip - Archive: setup/test.zip + $ unzip -l test.zip + Archive: test.zip Length Date Time Name --------- ---------- ----- ---- - 6 2021-03-23 18:52 latters.txt - 6 2021-03-23 18:52 numbers.txt + 446 2023-03-22 20:03 lorem.txt --------- ------- - 12 2 files + 446 1 file Running C will gives this output $ zipdetails test.zip - 0000 LOCAL HEADER #1 04034B50 - 0004 Extract Zip Spec 0A '1.0' - 0005 Extract OS 00 'MS-DOS' - 0006 General Purpose Flag 0000 - 0008 Compression Method 0000 'Stored' - 000A Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 000E CRC 0F8A149C - 0012 Compressed Length 00000006 - 0016 Uncompressed Length 00000006 - 001A Filename Length 000B - 001C Extra Length 0000 - 001E Filename 'letters.txt' - 0029 PAYLOAD abcde. - - 002F LOCAL HEADER #2 04034B50 - 0033 Extract Zip Spec 0A '1.0' - 0034 Extract OS 00 'MS-DOS' - 0035 General Purpose Flag 0000 - 0037 Compression Method 0000 'Stored' - 0039 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 003D CRC 261DAFE6 - 0041 Compressed Length 00000006 - 0045 Uncompressed Length 00000006 - 0049 Filename Length 000B - 004B Extra Length 0000 - 004D Filename 'numbers.txt' - 0058 PAYLOAD 12345. - - 005E CENTRAL HEADER #1 02014B50 - 0062 Created Zip Spec 1E '3.0' - 0063 Created OS 03 'Unix' - 0064 Extract Zip Spec 0A '1.0' - 0065 Extract OS 00 'MS-DOS' - 0066 General Purpose Flag 0000 - 0068 Compression Method 0000 'Stored' - 006A Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 006E CRC 0F8A149C - 0072 Compressed Length 00000006 - 0076 Uncompressed Length 00000006 - 007A Filename Length 000B - 007C Extra Length 0000 - 007E Comment Length 0000 - 0080 Disk Start 0000 - 0082 Int File Attributes 0001 - [Bit 0] 1 Text Data - 0084 Ext File Attributes 81B40000 - 0088 Local Header Offset 00000000 - 008C Filename 'letters.txt' - - 0097 CENTRAL HEADER #2 02014B50 - 009B Created Zip Spec 1E '3.0' - 009C Created OS 03 'Unix' - 009D Extract Zip Spec 0A '1.0' - 009E Extract OS 00 'MS-DOS' - 009F General Purpose Flag 0000 - 00A1 Compression Method 0000 'Stored' - 00A3 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 00A7 CRC 261DAFE6 - 00AB Compressed Length 00000006 - 00AF Uncompressed Length 00000006 - 00B3 Filename Length 000B - 00B5 Extra Length 0000 - 00B7 Comment Length 0000 - 00B9 Disk Start 0000 - 00BB Int File Attributes 0001 - [Bit 0] 1 Text Data - 00BD Ext File Attributes 81B40000 - 00C1 Local Header Offset 0000002F - 00C5 Filename 'numbers.txt' - - 00D0 END CENTRAL HEADER 06054B50 - 00D4 Number of this disk 0000 - 00D6 Central Dir Disk no 0000 - 00D8 Entries in this disk 0002 - 00DA Total Entries 0002 - 00DC Size of Central Dir 00000072 - 00E0 Offset to Central Dir 0000005E - 00E4 Comment Length 0000 - Done + 0000 LOCAL HEADER #1 04034B50 (67324752) + 0004 Extract Zip Spec 14 (20) '2.0' + 0005 Extract OS 00 (0) 'MS-DOS' + 0006 General Purpose Flag 0000 (0) + [Bits 1-2] 0 'Normal Compression' + 0008 Compression Method 0008 (8) 'Deflated' + 000A Last Mod Date/Time 5676A072 (1450614898) 'Wed Mar 22 20:03:36 2023' + 000E CRC F90EE7FF (4178503679) + 0012 Compressed Size 0000010E (270) + 0016 Uncompressed Size 000001BE (446) + 001A Filename Length 0009 (9) + 001C Extra Length 0000 (0) + 001E Filename 'lorem.txt' + 0027 PAYLOAD + + 0135 CENTRAL HEADER #1 02014B50 (33639248) + 0139 Created Zip Spec 1E (30) '3.0' + 013A Created OS 03 (3) 'Unix' + 013B Extract Zip Spec 14 (20) '2.0' + 013C Extract OS 00 (0) 'MS-DOS' + 013D General Purpose Flag 0000 (0) + [Bits 1-2] 0 'Normal Compression' + 013F Compression Method 0008 (8) 'Deflated' + 0141 Last Mod Date/Time 5676A072 (1450614898) 'Wed Mar 22 20:03:36 2023' + 0145 CRC F90EE7FF (4178503679) + 0149 Compressed Size 0000010E (270) + 014D Uncompressed Size 000001BE (446) + 0151 Filename Length 0009 (9) + 0153 Extra Length 0000 (0) + 0155 Comment Length 0000 (0) + 0157 Disk Start 0000 (0) + 0159 Int File Attributes 0001 (1) + [Bit 0] 1 'Text Data' + 015B Ext File Attributes 81ED0000 (2179792896) + [Bits 16-24] 01ED (493) 'Unix attrib: rwxr-xr-x' + [Bits 28-31] 08 (8) 'Regular File' + 015F Local Header Offset 00000000 (0) + 0163 Filename 'lorem.txt' + + 016C END CENTRAL HEADER 06054B50 (101010256) + 0170 Number of this disk 0000 (0) + 0172 Central Dir Disk no 0000 (0) + 0174 Entries in this disk 0001 (1) + 0176 Total Entries 0001 (1) + 0178 Size of Central Dir 00000037 (55) + 017C Offset to Central Dir 00000135 (309) + 0180 Comment Length 0000 (0) + # + # Done =head2 Verbose Output -If the C<-v> option is present, column 1 is expanded to include +If the C<-v> option is present, the metadata output is split into the +following columns: =over 5 -=item * +=item 1 -The offset from the start of the file in hex. +The offset, in hex, to the start of the field relative to the beginning of +the file. -=item * +=item 2 -The length of the field in hex. +The offset, in hex, to the end of the field relative to the beginning of +the file. -=item * +=item 3 -A hex dump of the bytes in field in the order they are stored in the zip -file. +The length, in hex, of the field. + +=item 4 + +A hex dump of the bytes in field in the order they are stored in the zip file. + +=item 5 + +A textual description of the field. + +=item 6 + +Information about the contents of the field. See the description in the +L for more details. =back -Here is the same zip file dumped using the C C<-v> option: +Here is the same zip file, C, dumped using the C +C<-v> option: $ zipdetails -v test.zip - 0000 0004 50 4B 03 04 LOCAL HEADER #1 04034B50 - 0004 0001 0A Extract Zip Spec 0A '1.0' - 0005 0001 00 Extract OS 00 'MS-DOS' - 0006 0002 00 00 General Purpose Flag 0000 - 0008 0002 00 00 Compression Method 0000 'Stored' - 000A 0004 3D 98 77 52 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 000E 0004 9C 14 8A 0F CRC 0F8A149C - 0012 0004 06 00 00 00 Compressed Length 00000006 - 0016 0004 06 00 00 00 Uncompressed Length 00000006 - 001A 0002 0B 00 Filename Length 000B - 001C 0002 00 00 Extra Length 0000 - 001E 000B 6C 65 74 74 Filename 'letters.txt' - 65 72 73 2E - 74 78 74 - 0029 0006 61 62 63 64 PAYLOAD abcde. - 65 0A - - 002F 0004 50 4B 03 04 LOCAL HEADER #2 04034B50 - 0033 0001 0A Extract Zip Spec 0A '1.0' - 0034 0001 00 Extract OS 00 'MS-DOS' - 0035 0002 00 00 General Purpose Flag 0000 - 0037 0002 00 00 Compression Method 0000 'Stored' - 0039 0004 3D 98 77 52 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 003D 0004 E6 AF 1D 26 CRC 261DAFE6 - 0041 0004 06 00 00 00 Compressed Length 00000006 - 0045 0004 06 00 00 00 Uncompressed Length 00000006 - 0049 0002 0B 00 Filename Length 000B - 004B 0002 00 00 Extra Length 0000 - 004D 000B 6E 75 6D 62 Filename 'numbers.txt' - 65 72 73 2E - 74 78 74 - 0058 0006 31 32 33 34 PAYLOAD 12345. - 35 0A - - 005E 0004 50 4B 01 02 CENTRAL HEADER #1 02014B50 - 0062 0001 1E Created Zip Spec 1E '3.0' - 0063 0001 03 Created OS 03 'Unix' - 0064 0001 0A Extract Zip Spec 0A '1.0' - 0065 0001 00 Extract OS 00 'MS-DOS' - 0066 0002 00 00 General Purpose Flag 0000 - 0068 0002 00 00 Compression Method 0000 'Stored' - 006A 0004 3D 98 77 52 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 006E 0004 9C 14 8A 0F CRC 0F8A149C - 0072 0004 06 00 00 00 Compressed Length 00000006 - 0076 0004 06 00 00 00 Uncompressed Length 00000006 - 007A 0002 0B 00 Filename Length 000B - 007C 0002 00 00 Extra Length 0000 - 007E 0002 00 00 Comment Length 0000 - 0080 0002 00 00 Disk Start 0000 - 0082 0002 01 00 Int File Attributes 0001 - [Bit 0] 1 Text Data - 0084 0004 00 00 B4 81 Ext File Attributes 81B40000 - 0088 0004 00 00 00 00 Local Header Offset 00000000 - 008C 000B 6C 65 74 74 Filename 'letters.txt' - 65 72 73 2E - 74 78 74 - - 0097 0004 50 4B 01 02 CENTRAL HEADER #2 02014B50 - 009B 0001 1E Created Zip Spec 1E '3.0' - 009C 0001 03 Created OS 03 'Unix' - 009D 0001 0A Extract Zip Spec 0A '1.0' - 009E 0001 00 Extract OS 00 'MS-DOS' - 009F 0002 00 00 General Purpose Flag 0000 - 00A1 0002 00 00 Compression Method 0000 'Stored' - 00A3 0004 3D 98 77 52 Last Mod Time 5277983D 'Tue Mar 23 19:01:58 2021' - 00A7 0004 E6 AF 1D 26 CRC 261DAFE6 - 00AB 0004 06 00 00 00 Compressed Length 00000006 - 00AF 0004 06 00 00 00 Uncompressed Length 00000006 - 00B3 0002 0B 00 Filename Length 000B - 00B5 0002 00 00 Extra Length 0000 - 00B7 0002 00 00 Comment Length 0000 - 00B9 0002 00 00 Disk Start 0000 - 00BB 0002 01 00 Int File Attributes 0001 - [Bit 0] 1 Text Data - 00BD 0004 00 00 B4 81 Ext File Attributes 81B40000 - 00C1 0004 2F 00 00 00 Local Header Offset 0000002F - 00C5 000B 6E 75 6D 62 Filename 'numbers.txt' - 65 72 73 2E - 74 78 74 - - 00D0 0004 50 4B 05 06 END CENTRAL HEADER 06054B50 - 00D4 0002 00 00 Number of this disk 0000 - 00D6 0002 00 00 Central Dir Disk no 0000 - 00D8 0002 02 00 Entries in this disk 0002 - 00DA 0002 02 00 Total Entries 0002 - 00DC 0004 72 00 00 00 Size of Central Dir 00000072 - 00E0 0004 5E 00 00 00 Offset to Central Dir 0000005E - 00E4 0002 00 00 Comment Length 0000 - Done + 0000 0003 0004 50 4B 03 04 LOCAL HEADER #1 04034B50 (67324752) + 0004 0004 0001 14 Extract Zip Spec 14 (20) '2.0' + 0005 0005 0001 00 Extract OS 00 (0) 'MS-DOS' + 0006 0007 0002 00 00 General Purpose Flag 0000 (0) + [Bits 1-2] 0 'Normal Compression' + 0008 0009 0002 08 00 Compression Method 0008 (8) 'Deflated' + 000A 000D 0004 72 A0 76 56 Last Mod Date/Time 5676A072 (1450614898) 'Wed Mar 22 20:03:36 2023' + 000E 0011 0004 FF E7 0E F9 CRC F90EE7FF (4178503679) + 0012 0015 0004 0E 01 00 00 Compressed Size 0000010E (270) + 0016 0019 0004 BE 01 00 00 Uncompressed Size 000001BE (446) + 001A 001B 0002 09 00 Filename Length 0009 (9) + 001C 001D 0002 00 00 Extra Length 0000 (0) + 001E 0026 0009 6C 6F 72 65 Filename 'lorem.txt' + 6D 2E 74 78 + 74 + 0027 0134 010E ... PAYLOAD + + 0135 0138 0004 50 4B 01 02 CENTRAL HEADER #1 02014B50 (33639248) + 0139 0139 0001 1E Created Zip Spec 1E (30) '3.0' + 013A 013A 0001 03 Created OS 03 (3) 'Unix' + 013B 013B 0001 14 Extract Zip Spec 14 (20) '2.0' + 013C 013C 0001 00 Extract OS 00 (0) 'MS-DOS' + 013D 013E 0002 00 00 General Purpose Flag 0000 (0) + [Bits 1-2] 0 'Normal Compression' + 013F 0140 0002 08 00 Compression Method 0008 (8) 'Deflated' + 0141 0144 0004 72 A0 76 56 Last Mod Date/Time 5676A072 (1450614898) 'Wed Mar 22 20:03:36 2023' + 0145 0148 0004 FF E7 0E F9 CRC F90EE7FF (4178503679) + 0149 014C 0004 0E 01 00 00 Compressed Size 0000010E (270) + 014D 0150 0004 BE 01 00 00 Uncompressed Size 000001BE (446) + 0151 0152 0002 09 00 Filename Length 0009 (9) + 0153 0154 0002 00 00 Extra Length 0000 (0) + 0155 0156 0002 00 00 Comment Length 0000 (0) + 0157 0158 0002 00 00 Disk Start 0000 (0) + 0159 015A 0002 01 00 Int File Attributes 0001 (1) + [Bit 0] 1 'Text Data' + 015B 015E 0004 00 00 ED 81 Ext File Attributes 81ED0000 (2179792896) + [Bits 16-24] 01ED (493) 'Unix attrib: rwxr-xr-x' + [Bits 28-31] 08 (8) 'Regular File' + 015F 0162 0004 00 00 00 00 Local Header Offset 00000000 (0) + 0163 016B 0009 6C 6F 72 65 Filename 'lorem.txt' + 6D 2E 74 78 + 74 + + 016C 016F 0004 50 4B 05 06 END CENTRAL HEADER 06054B50 (101010256) + 0170 0171 0002 00 00 Number of this disk 0000 (0) + 0172 0173 0002 00 00 Central Dir Disk no 0000 (0) + 0174 0175 0002 01 00 Entries in this disk 0001 (1) + 0176 0177 0002 01 00 Total Entries 0001 (1) + 0178 017B 0004 37 00 00 00 Size of Central Dir 00000037 (55) + 017C 017F 0004 35 01 00 00 Offset to Central Dir 00000135 (309) + 0180 0181 0002 00 00 Comment Length 0000 (0) + # + # Done + +=head2 Advanced Analysis + +If you have a corrupt or non-standard zip file, particulatly one where the +C metadata at the end of the file is absent/incomplete, you +can use either the C<--walk> option or the C<--scan> option to search for +any zip metadata that is still present in the file. + +When either of these options is enabled, this program will bypass the +initial step of reading the C at the end of the file and +simply scan the zip file sequentially from the start of the file looking +for zip metedata records. Although this can be error prone, for the most +part it will find any zip file metadata that is still present in the file. + +The difference between the two options is how aggressive the sequential +scan is: C<--walk> is optimistic, while C<--scan> is pessimistic. + +To understand the difference in more detail you need to know a bit about +how zip file metadata is structured. Under the hood, a zip file uses a +series of 4-byte signatures to flag the start of a each of the metadata +records it uses. When the C<--walk> or the C<--scan> option is enabled both +work identically by scanning the file from the beginning looking for any +the of these valid 4-byte metadata signatures. When a 4-byte signature is +found both options will blindly assume that it has found a vald metadata +record and display it. + +=head3 C<--walk> + +The C<--walk> option optimistically assumes that it has found a real zip +metatada record and so starts the scan for the next record directly after +the record it has just output. + +=head3 C<--scan> + +The C<--scan> option is pessimistic and assumes the 4-byte signature +sequence may have been a false-positive, so before starting the scan for +the next resord, it will rewind to the location in the file directly after +the 4-byte sequecce it just processed. This means it will rescan data that +has already been processed. For very lage zip files the C<--scan> option +can be really realy slow, so trying the C<--walk> option first. + +B: If the zip file being processed contains one or more +nested zip files, and the outer zip file uses the C compression +method, the C<--scan> option will display the zip metadata for both the +outer & inner zip files. + +=head2 Filename Encoding Issues + +Sometimes when displaying the contents of a zip file the filenames (or +comments) appear to be garbled. This section walks through the reasons and +mitigations that can be applied to work around these issues. + +=head3 Background + +When zip files were first created in the 1980's, there was no Unicode or +UTF-8. Issues around character set encoding interoperability were not a +major concern. + +Initially, the only official encoding supported in zip files was IBM Code +Page 437 (AKA C). As time went on users in locales where C +wasn't appropriate stored filenames in the encoding native to their locale. +If you were running a system that matched the locale of the zip file, all +was well. If not, you had to post-process the filenames after unzipping the +zip file. + +Fast forward to the introduction of Unicode and UTF-8 encoding. The +approach now used by all major zip implementations is to set the C (also known as C) in the zip file metadata to signal +that a filename/comment is encoded in UTF-8. + +To ensure maximum interoperability when sharing zip files store 7-bit +filenames as-is in the zip file. For anything else the C bit needs to +be set and the filename is encoded in UTF-8. Although this rule is kept to +for the most part, there are exceptions out in the wild. + +=head3 Dealing with Encoding Errors + +The most common filename encoding issue is where the C bit is not set and +the filename is stored in a character set that doesnt't match the system +encoding. This mostly impacts legacy zip files that predate the +introduction of Unicode. + +To deal with this issue you first need to know what encoding was used in +the zip file. For example, if the filename is encoded in C you +can display the filenames using the C<--encoding> option + + zipdetails --encoding ISO-8859-1 myfile.zip + +A less common variation of this is where the C bit is set, signalling +that the filename will be encoded in UTF-8, but the filename is not encoded +in UTF-8. To deal with this scenarion, use the C<--no-language-encoding> +option along with the C<--encoding> option. + =head1 LIMITATIONS @@ -2760,19 +7859,67 @@ The following zip file features are not supported by this program: =item * -Multi-part archives. +Multi-part/Split/Spanned Zip Archives. + +This program cannot give an overall report on the combined parts of a +multi-part zip file. + +The best you can do is run with either the C<--scan> or C<--walk> options +against individual parts. Some will contains zipfile metadata which will be +detected and some will only contain compressed payload data. + + +=item * + +Encrypted Central Directory + +When pkzip I is enabled in a zip file this program can +still parse most of the metadata in the zip file. The exception is when the +C of a zip file is also encrypted. This program cannot +parse any metadata from an encrypted C. + +=item * + +Corrupt Zip files + +When C encounters a corrupt zip file, it will do one or more of +the following + +=over 5 + +=item * + +Display details of the corruption and carry on + +=item * + +Display details of the corruption and terminate =item * -The strong encryption features defined in the L document. +Terminate with a generic message + +=back + +Which of the above is output is dependent in the severity of the +corruption. =back =head1 TODO -Error handling is a work in progress. If the program encounters a problem -reading a zip file it is likely to terminate with an unhelpful error -message. +=head2 JSON/YML Output + +Output some of the zip file metadata as a JSON or YML document. + +=head2 Corrupt Zip files + +Although the detection and reporting of most of the common corruption use-cases is +present in C, there are likely to be other edge cases that need +to be supported. + +If you have a corrupt Zip file that isn't being processed properly, please +report it (see L<"SUPPORT">). =head1 SUPPORT @@ -2783,17 +7930,18 @@ L. The primary reference for Zip files is -L. +L. An alternative reference is the Info-Zip appnote. This is available from L For details of WinZip AES encryption see L. +Encryption Specification AE-1 and +AE-2|https://www.winzip.com/en/support/aes-encryption/>. The C program that comes with the info-zip distribution -(L) can also display details of the structure of -a zip file. +(L) can also display details of the structure of a zip +file. =head1 AUTHOR @@ -2802,7 +7950,7 @@ Paul Marquess F. =head1 COPYRIGHT -Copyright (c) 2011-2022 Paul Marquess. All rights reserved. +Copyright (c) 2011-2024 Paul Marquess. All rights reserved. -This program is free software; you can redistribute it and/or modify it -under the same terms as Perl itself. +This program is free software; you can redistribute it and/or modify it under +the same terms as Perl itself. diff --git a/lib/Compress/Zlib.pm b/lib/Compress/Zlib.pm index eded91c..747c992 100644 --- a/lib/Compress/Zlib.pm +++ b/lib/Compress/Zlib.pm @@ -7,17 +7,17 @@ use Carp ; use IO::Handle ; use Scalar::Util qw(dualvar); -use IO::Compress::Base::Common 2.206 ; -use Compress::Raw::Zlib 2.206 ; -use IO::Compress::Gzip 2.206 ; -use IO::Uncompress::Gunzip 2.206 ; +use IO::Compress::Base::Common 2.207 ; +use Compress::Raw::Zlib 2.207 ; +use IO::Compress::Gzip 2.207 ; +use IO::Uncompress::Gunzip 2.207 ; use strict ; use warnings ; use bytes ; our ($VERSION, $XS_VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); -$VERSION = '2.206'; +$VERSION = '2.207'; $XS_VERSION = $VERSION; $VERSION = eval $VERSION; @@ -461,7 +461,7 @@ sub inflate package Compress::Zlib ; -use IO::Compress::Gzip::Constants 2.206 ; +use IO::Compress::Gzip::Constants 2.207 ; sub memGzip($) { @@ -1509,7 +1509,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 1995-2023 Paul Marquess. All rights reserved. +Copyright (c) 1995-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Adapter/Bzip2.pm b/lib/IO/Compress/Adapter/Bzip2.pm index 5321991..b34dcd5 100644 --- a/lib/IO/Compress/Adapter/Bzip2.pm +++ b/lib/IO/Compress/Adapter/Bzip2.pm @@ -4,12 +4,12 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); +use IO::Compress::Base::Common 2.207 qw(:Status); -use Compress::Raw::Bzip2 2.206 ; +use Compress::Raw::Bzip2 2.207 ; our ($VERSION); -$VERSION = '2.206'; +$VERSION = '2.207'; sub mkCompObject { diff --git a/lib/IO/Compress/Adapter/Deflate.pm b/lib/IO/Compress/Adapter/Deflate.pm index 9003ed5..167034d 100644 --- a/lib/IO/Compress/Adapter/Deflate.pm +++ b/lib/IO/Compress/Adapter/Deflate.pm @@ -4,13 +4,13 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); -use Compress::Raw::Zlib 2.206 qw( !crc32 !adler32 ) ; +use IO::Compress::Base::Common 2.207 qw(:Status); +use Compress::Raw::Zlib 2.207 qw( !crc32 !adler32 ) ; require Exporter; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, @EXPORT, %DEFLATE_CONSTANTS); -$VERSION = '2.206'; +$VERSION = '2.207'; @ISA = qw(Exporter); @EXPORT_OK = @Compress::Raw::Zlib::DEFLATE_CONSTANTS; %EXPORT_TAGS = %Compress::Raw::Zlib::DEFLATE_CONSTANTS; diff --git a/lib/IO/Compress/Adapter/Identity.pm b/lib/IO/Compress/Adapter/Identity.pm index 5ae4422..5f7abef 100644 --- a/lib/IO/Compress/Adapter/Identity.pm +++ b/lib/IO/Compress/Adapter/Identity.pm @@ -4,10 +4,10 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); +use IO::Compress::Base::Common 2.207 qw(:Status); our ($VERSION); -$VERSION = '2.206'; +$VERSION = '2.207'; sub mkCompObject { diff --git a/lib/IO/Compress/Base.pm b/lib/IO/Compress/Base.pm index 70a0572..005e4ab 100644 --- a/lib/IO/Compress/Base.pm +++ b/lib/IO/Compress/Base.pm @@ -6,7 +6,7 @@ require 5.006 ; use strict ; use warnings; -use IO::Compress::Base::Common 2.206 ; +use IO::Compress::Base::Common 2.207 ; use IO::File (); ; use Scalar::Util (); @@ -20,7 +20,7 @@ use Symbol(); our (@ISA, $VERSION); @ISA = qw(IO::File Exporter); -$VERSION = '2.206'; +$VERSION = '2.207'; #Can't locate object method "SWASHNEW" via package "utf8" (perhaps you forgot to load "utf8"?) at .../ext/Compress-Zlib/Gzip/blib/lib/Compress/Zlib/Common.pm line 16. @@ -1051,7 +1051,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Base/Common.pm b/lib/IO/Compress/Base/Common.pm index 5694f34..0c0850d 100644 --- a/lib/IO/Compress/Base/Common.pm +++ b/lib/IO/Compress/Base/Common.pm @@ -11,7 +11,7 @@ use File::GlobMapper; require Exporter; our ($VERSION, @ISA, @EXPORT, %EXPORT_TAGS, $HAS_ENCODE); @ISA = qw(Exporter); -$VERSION = '2.206'; +$VERSION = '2.207'; @EXPORT = qw( isaFilehandle isaFilename isaScalar whatIsInput whatIsOutput diff --git a/lib/IO/Compress/Bzip2.pm b/lib/IO/Compress/Bzip2.pm index a87a104..c4c580a 100644 --- a/lib/IO/Compress/Bzip2.pm +++ b/lib/IO/Compress/Bzip2.pm @@ -5,16 +5,16 @@ use warnings; use bytes; require Exporter ; -use IO::Compress::Base 2.206 ; +use IO::Compress::Base 2.207 ; -use IO::Compress::Base::Common 2.206 qw(); -use IO::Compress::Adapter::Bzip2 2.206 ; +use IO::Compress::Base::Common 2.207 qw(); +use IO::Compress::Adapter::Bzip2 2.207 ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $Bzip2Error); -$VERSION = '2.206'; +$VERSION = '2.207'; $Bzip2Error = ''; @ISA = qw(IO::Compress::Base Exporter); @@ -51,7 +51,7 @@ sub getExtraParams { my $self = shift ; - use IO::Compress::Base::Common 2.206 qw(:Parse); + use IO::Compress::Base::Common 2.207 qw(:Parse); return ( 'blocksize100k' => [IO::Compress::Base::Common::Parse_unsigned, 1], @@ -878,7 +878,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Deflate.pm b/lib/IO/Compress/Deflate.pm index 900e2e8..96592f0 100644 --- a/lib/IO/Compress/Deflate.pm +++ b/lib/IO/Compress/Deflate.pm @@ -8,16 +8,16 @@ use bytes; require Exporter ; -use IO::Compress::RawDeflate 2.206 (); -use IO::Compress::Adapter::Deflate 2.206 ; +use IO::Compress::RawDeflate 2.207 (); +use IO::Compress::Adapter::Deflate 2.207 ; -use IO::Compress::Zlib::Constants 2.206 ; -use IO::Compress::Base::Common 2.206 qw(); +use IO::Compress::Zlib::Constants 2.207 ; +use IO::Compress::Base::Common 2.207 qw(); our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, %DEFLATE_CONSTANTS, $DeflateError); -$VERSION = '2.206'; +$VERSION = '2.207'; $DeflateError = ''; @ISA = qw(IO::Compress::RawDeflate Exporter); @@ -952,7 +952,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/FAQ.pod b/lib/IO/Compress/FAQ.pod index 549ea2a..28fa521 100644 --- a/lib/IO/Compress/FAQ.pod +++ b/lib/IO/Compress/FAQ.pod @@ -682,7 +682,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Gzip.pm b/lib/IO/Compress/Gzip.pm index e44cab8..4cc8277 100644 --- a/lib/IO/Compress/Gzip.pm +++ b/lib/IO/Compress/Gzip.pm @@ -8,12 +8,12 @@ use bytes; require Exporter ; -use IO::Compress::RawDeflate 2.206 () ; -use IO::Compress::Adapter::Deflate 2.206 ; +use IO::Compress::RawDeflate 2.207 () ; +use IO::Compress::Adapter::Deflate 2.207 ; -use IO::Compress::Base::Common 2.206 qw(:Status ); -use IO::Compress::Gzip::Constants 2.206 ; -use IO::Compress::Zlib::Extra 2.206 ; +use IO::Compress::Base::Common 2.207 qw(:Status ); +use IO::Compress::Gzip::Constants 2.207 ; +use IO::Compress::Zlib::Extra 2.207 ; BEGIN { @@ -25,7 +25,7 @@ BEGIN our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, %DEFLATE_CONSTANTS, $GzipError); -$VERSION = '2.206'; +$VERSION = '2.207'; $GzipError = '' ; @ISA = qw(IO::Compress::RawDeflate Exporter); @@ -1327,7 +1327,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Gzip/Constants.pm b/lib/IO/Compress/Gzip/Constants.pm index 5f540d2..d529f55 100644 --- a/lib/IO/Compress/Gzip/Constants.pm +++ b/lib/IO/Compress/Gzip/Constants.pm @@ -9,7 +9,7 @@ require Exporter; our ($VERSION, @ISA, @EXPORT, %GZIP_OS_Names); our ($GZIP_FNAME_INVALID_CHAR_RE, $GZIP_FCOMMENT_INVALID_CHAR_RE); -$VERSION = '2.206'; +$VERSION = '2.207'; @ISA = qw(Exporter); diff --git a/lib/IO/Compress/RawDeflate.pm b/lib/IO/Compress/RawDeflate.pm index a17a811..39c1093 100644 --- a/lib/IO/Compress/RawDeflate.pm +++ b/lib/IO/Compress/RawDeflate.pm @@ -6,16 +6,16 @@ use strict ; use warnings; use bytes; -use IO::Compress::Base 2.206 ; -use IO::Compress::Base::Common 2.206 qw(:Status :Parse); -use IO::Compress::Adapter::Deflate 2.206 ; -use Compress::Raw::Zlib 2.206 qw(Z_DEFLATED Z_DEFAULT_COMPRESSION Z_DEFAULT_STRATEGY); +use IO::Compress::Base 2.207 ; +use IO::Compress::Base::Common 2.207 qw(:Status :Parse); +use IO::Compress::Adapter::Deflate 2.207 ; +use Compress::Raw::Zlib 2.207 qw(Z_DEFLATED Z_DEFAULT_COMPRESSION Z_DEFAULT_STRATEGY); require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %DEFLATE_CONSTANTS, %EXPORT_TAGS, $RawDeflateError); -$VERSION = '2.206'; +$VERSION = '2.207'; $RawDeflateError = ''; @ISA = qw(IO::Compress::Base Exporter); @@ -1070,7 +1070,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Zip.pm b/lib/IO/Compress/Zip.pm index b9381c3..6d6c2db 100644 --- a/lib/IO/Compress/Zip.pm +++ b/lib/IO/Compress/Zip.pm @@ -4,41 +4,41 @@ use strict ; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status ); -use IO::Compress::RawDeflate 2.206 (); -use IO::Compress::Adapter::Deflate 2.206 ; -use IO::Compress::Adapter::Identity 2.206 ; -use IO::Compress::Zlib::Extra 2.206 ; -use IO::Compress::Zip::Constants 2.206 ; +use IO::Compress::Base::Common 2.207 qw(:Status ); +use IO::Compress::RawDeflate 2.207 (); +use IO::Compress::Adapter::Deflate 2.207 ; +use IO::Compress::Adapter::Identity 2.207 ; +use IO::Compress::Zlib::Extra 2.207 ; +use IO::Compress::Zip::Constants 2.207 ; use File::Spec(); use Config; -use Compress::Raw::Zlib 2.206 (); +use Compress::Raw::Zlib 2.207 (); BEGIN { eval { require IO::Compress::Adapter::Bzip2 ; - IO::Compress::Adapter::Bzip2->VERSION( 2.206 ); + IO::Compress::Adapter::Bzip2->VERSION( 2.207 ); require IO::Compress::Bzip2 ; - IO::Compress::Bzip2->VERSION( 2.206 ); + IO::Compress::Bzip2->VERSION( 2.207 ); } ; eval { require IO::Compress::Adapter::Lzma ; - IO::Compress::Adapter::Lzma->VERSION( 2.206 ); + IO::Compress::Adapter::Lzma->VERSION( 2.207 ); require IO::Compress::Lzma ; - IO::Compress::Lzma->VERSION( 2.206 ); + IO::Compress::Lzma->VERSION( 2.207 ); } ; eval { require IO::Compress::Adapter::Xz ; - IO::Compress::Adapter::Xz->VERSION( 2.206 ); + IO::Compress::Adapter::Xz->VERSION( 2.207 ); require IO::Compress::Xz ; - IO::Compress::Xz->VERSION( 2.206 ); + IO::Compress::Xz->VERSION( 2.207 ); } ; eval { require IO::Compress::Adapter::Zstd ; - IO::Compress::Adapter::Zstd->VERSION( 2.206 ); + IO::Compress::Adapter::Zstd->VERSION( 2.207 ); require IO::Compress::Zstd ; - IO::Compress::Zstd->VERSION( 2.206 ); + IO::Compress::Zstd->VERSION( 2.207 ); } ; } @@ -47,7 +47,7 @@ require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, %DEFLATE_CONSTANTS, $ZipError); -$VERSION = '2.206'; +$VERSION = '2.207'; $ZipError = ''; @ISA = qw(IO::Compress::RawDeflate Exporter); @@ -2285,7 +2285,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Compress/Zip/Constants.pm b/lib/IO/Compress/Zip/Constants.pm index db5727a..de9e751 100644 --- a/lib/IO/Compress/Zip/Constants.pm +++ b/lib/IO/Compress/Zip/Constants.pm @@ -7,7 +7,7 @@ require Exporter; our ($VERSION, @ISA, @EXPORT, %ZIP_CM_MIN_VERSIONS); -$VERSION = '2.206'; +$VERSION = '2.207'; @ISA = qw(Exporter); diff --git a/lib/IO/Compress/Zlib/Constants.pm b/lib/IO/Compress/Zlib/Constants.pm index 8df1ecb..cb0676b 100644 --- a/lib/IO/Compress/Zlib/Constants.pm +++ b/lib/IO/Compress/Zlib/Constants.pm @@ -9,7 +9,7 @@ require Exporter; our ($VERSION, @ISA, @EXPORT); -$VERSION = '2.206'; +$VERSION = '2.207'; @ISA = qw(Exporter); diff --git a/lib/IO/Compress/Zlib/Extra.pm b/lib/IO/Compress/Zlib/Extra.pm index 1621c37..3821acc 100644 --- a/lib/IO/Compress/Zlib/Extra.pm +++ b/lib/IO/Compress/Zlib/Extra.pm @@ -8,9 +8,9 @@ use bytes; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS); -$VERSION = '2.206'; +$VERSION = '2.207'; -use IO::Compress::Gzip::Constants 2.206 ; +use IO::Compress::Gzip::Constants 2.207 ; sub ExtraFieldError { diff --git a/lib/IO/Uncompress/Adapter/Bunzip2.pm b/lib/IO/Uncompress/Adapter/Bunzip2.pm index 047fa6f..88385c6 100644 --- a/lib/IO/Uncompress/Adapter/Bunzip2.pm +++ b/lib/IO/Uncompress/Adapter/Bunzip2.pm @@ -4,12 +4,12 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); +use IO::Compress::Base::Common 2.207 qw(:Status); -use Compress::Raw::Bzip2 2.206 ; +use Compress::Raw::Bzip2 2.207 ; our ($VERSION, @ISA); -$VERSION = '2.206'; +$VERSION = '2.207'; sub mkUncompObject { diff --git a/lib/IO/Uncompress/Adapter/Identity.pm b/lib/IO/Uncompress/Adapter/Identity.pm index 761c245..6673b58 100755 --- a/lib/IO/Uncompress/Adapter/Identity.pm +++ b/lib/IO/Uncompress/Adapter/Identity.pm @@ -4,14 +4,14 @@ use warnings; use strict; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); +use IO::Compress::Base::Common 2.207 qw(:Status); use IO::Compress::Zip::Constants ; our ($VERSION); -$VERSION = '2.206'; +$VERSION = '2.207'; -use Compress::Raw::Zlib 2.206 (); +use Compress::Raw::Zlib 2.207 (); sub mkUncompObject { diff --git a/lib/IO/Uncompress/Adapter/Inflate.pm b/lib/IO/Uncompress/Adapter/Inflate.pm index 8f45051..e704bf6 100644 --- a/lib/IO/Uncompress/Adapter/Inflate.pm +++ b/lib/IO/Uncompress/Adapter/Inflate.pm @@ -4,11 +4,11 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status); -use Compress::Raw::Zlib 2.206 qw(Z_OK Z_BUF_ERROR Z_STREAM_END Z_FINISH MAX_WBITS); +use IO::Compress::Base::Common 2.207 qw(:Status); +use Compress::Raw::Zlib 2.207 qw(Z_OK Z_BUF_ERROR Z_STREAM_END Z_FINISH MAX_WBITS); our ($VERSION); -$VERSION = '2.206'; +$VERSION = '2.207'; diff --git a/lib/IO/Uncompress/AnyInflate.pm b/lib/IO/Uncompress/AnyInflate.pm index 68ea63a..39a4490 100644 --- a/lib/IO/Uncompress/AnyInflate.pm +++ b/lib/IO/Uncompress/AnyInflate.pm @@ -6,22 +6,22 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Parse); +use IO::Compress::Base::Common 2.207 qw(:Parse); -use IO::Uncompress::Adapter::Inflate 2.206 (); +use IO::Uncompress::Adapter::Inflate 2.207 (); -use IO::Uncompress::Base 2.206 ; -use IO::Uncompress::Gunzip 2.206 ; -use IO::Uncompress::Inflate 2.206 ; -use IO::Uncompress::RawInflate 2.206 ; -use IO::Uncompress::Unzip 2.206 ; +use IO::Uncompress::Base 2.207 ; +use IO::Uncompress::Gunzip 2.207 ; +use IO::Uncompress::Inflate 2.207 ; +use IO::Uncompress::RawInflate 2.207 ; +use IO::Uncompress::Unzip 2.207 ; require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $AnyInflateError); -$VERSION = '2.206'; +$VERSION = '2.207'; $AnyInflateError = ''; @ISA = qw(IO::Uncompress::Base Exporter); @@ -1014,7 +1014,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/AnyUncompress.pm b/lib/IO/Uncompress/AnyUncompress.pm index 94808c2..55b6c64 100644 --- a/lib/IO/Uncompress/AnyUncompress.pm +++ b/lib/IO/Uncompress/AnyUncompress.pm @@ -4,16 +4,16 @@ use strict; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 (); +use IO::Compress::Base::Common 2.207 (); -use IO::Uncompress::Base 2.206 ; +use IO::Uncompress::Base 2.207 ; require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $AnyUncompressError); -$VERSION = '2.206'; +$VERSION = '2.207'; $AnyUncompressError = ''; @ISA = qw(IO::Uncompress::Base Exporter); @@ -33,26 +33,26 @@ BEGIN # Don't trigger any __DIE__ Hooks. local $SIG{__DIE__}; - eval ' use IO::Uncompress::Adapter::Inflate 2.206 ;'; - eval ' use IO::Uncompress::Adapter::Bunzip2 2.206 ;'; - eval ' use IO::Uncompress::Adapter::LZO 2.206 ;'; - eval ' use IO::Uncompress::Adapter::Lzf 2.206 ;'; - eval ' use IO::Uncompress::Adapter::UnLzma 2.206 ;'; - eval ' use IO::Uncompress::Adapter::UnXz 2.206 ;'; - eval ' use IO::Uncompress::Adapter::UnZstd 2.206 ;'; - eval ' use IO::Uncompress::Adapter::UnLzip 2.206 ;'; - - eval ' use IO::Uncompress::Bunzip2 2.206 ;'; - eval ' use IO::Uncompress::UnLzop 2.206 ;'; - eval ' use IO::Uncompress::Gunzip 2.206 ;'; - eval ' use IO::Uncompress::Inflate 2.206 ;'; - eval ' use IO::Uncompress::RawInflate 2.206 ;'; - eval ' use IO::Uncompress::Unzip 2.206 ;'; - eval ' use IO::Uncompress::UnLzf 2.206 ;'; - eval ' use IO::Uncompress::UnLzma 2.206 ;'; - eval ' use IO::Uncompress::UnXz 2.206 ;'; - eval ' use IO::Uncompress::UnZstd 2.206 ;'; - eval ' use IO::Uncompress::UnLzip 2.206 ;'; + eval ' use IO::Uncompress::Adapter::Inflate 2.207 ;'; + eval ' use IO::Uncompress::Adapter::Bunzip2 2.207 ;'; + eval ' use IO::Uncompress::Adapter::LZO 2.207 ;'; + eval ' use IO::Uncompress::Adapter::Lzf 2.207 ;'; + eval ' use IO::Uncompress::Adapter::UnLzma 2.207 ;'; + eval ' use IO::Uncompress::Adapter::UnXz 2.207 ;'; + eval ' use IO::Uncompress::Adapter::UnZstd 2.207 ;'; + eval ' use IO::Uncompress::Adapter::UnLzip 2.207 ;'; + + eval ' use IO::Uncompress::Bunzip2 2.207 ;'; + eval ' use IO::Uncompress::UnLzop 2.207 ;'; + eval ' use IO::Uncompress::Gunzip 2.207 ;'; + eval ' use IO::Uncompress::Inflate 2.207 ;'; + eval ' use IO::Uncompress::RawInflate 2.207 ;'; + eval ' use IO::Uncompress::Unzip 2.207 ;'; + eval ' use IO::Uncompress::UnLzf 2.207 ;'; + eval ' use IO::Uncompress::UnLzma 2.207 ;'; + eval ' use IO::Uncompress::UnXz 2.207 ;'; + eval ' use IO::Uncompress::UnZstd 2.207 ;'; + eval ' use IO::Uncompress::UnLzip 2.207 ;'; } @@ -1090,7 +1090,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/Base.pm b/lib/IO/Uncompress/Base.pm index fec6cc6..60b9c6f 100644 --- a/lib/IO/Uncompress/Base.pm +++ b/lib/IO/Uncompress/Base.pm @@ -9,12 +9,12 @@ our (@ISA, $VERSION, @EXPORT_OK, %EXPORT_TAGS); @ISA = qw(IO::File Exporter); -$VERSION = '2.206'; +$VERSION = '2.207'; use constant G_EOF => 0 ; use constant G_ERR => -1 ; -use IO::Compress::Base::Common 2.206 ; +use IO::Compress::Base::Common 2.207 ; use IO::File ; use Symbol; @@ -1567,7 +1567,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/Bunzip2.pm b/lib/IO/Uncompress/Bunzip2.pm index 868f150..1e942d3 100644 --- a/lib/IO/Uncompress/Bunzip2.pm +++ b/lib/IO/Uncompress/Bunzip2.pm @@ -4,15 +4,15 @@ use strict ; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status ); +use IO::Compress::Base::Common 2.207 qw(:Status ); -use IO::Uncompress::Base 2.206 ; -use IO::Uncompress::Adapter::Bunzip2 2.206 ; +use IO::Uncompress::Base 2.207 ; +use IO::Uncompress::Adapter::Bunzip2 2.207 ; require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $Bunzip2Error); -$VERSION = '2.206'; +$VERSION = '2.207'; $Bunzip2Error = ''; @ISA = qw(IO::Uncompress::Base Exporter); @@ -922,7 +922,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/Gunzip.pm b/lib/IO/Uncompress/Gunzip.pm index 1b33cb0..4d46eef 100644 --- a/lib/IO/Uncompress/Gunzip.pm +++ b/lib/IO/Uncompress/Gunzip.pm @@ -9,12 +9,12 @@ use strict ; use warnings; use bytes; -use IO::Uncompress::RawInflate 2.206 ; +use IO::Uncompress::RawInflate 2.207 ; -use Compress::Raw::Zlib 2.206 () ; -use IO::Compress::Base::Common 2.206 qw(:Status ); -use IO::Compress::Gzip::Constants 2.206 ; -use IO::Compress::Zlib::Extra 2.206 ; +use Compress::Raw::Zlib 2.207 () ; +use IO::Compress::Base::Common 2.207 qw(:Status ); +use IO::Compress::Gzip::Constants 2.207 ; +use IO::Compress::Zlib::Extra 2.207 ; require Exporter ; @@ -28,7 +28,7 @@ Exporter::export_ok_tags('all'); $GunzipError = ''; -$VERSION = '2.206'; +$VERSION = '2.207'; sub new { @@ -1138,7 +1138,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/Inflate.pm b/lib/IO/Uncompress/Inflate.pm index 93f7b6e..c6a1c2b 100644 --- a/lib/IO/Uncompress/Inflate.pm +++ b/lib/IO/Uncompress/Inflate.pm @@ -5,15 +5,15 @@ use strict ; use warnings; use bytes; -use IO::Compress::Base::Common 2.206 qw(:Status ); -use IO::Compress::Zlib::Constants 2.206 ; +use IO::Compress::Base::Common 2.207 qw(:Status ); +use IO::Compress::Zlib::Constants 2.207 ; -use IO::Uncompress::RawInflate 2.206 ; +use IO::Uncompress::RawInflate 2.207 ; require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $InflateError); -$VERSION = '2.206'; +$VERSION = '2.207'; $InflateError = ''; @ISA = qw(IO::Uncompress::RawInflate Exporter); @@ -1010,7 +1010,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/RawInflate.pm b/lib/IO/Uncompress/RawInflate.pm index 17b07c3..64cb5ae 100755 --- a/lib/IO/Uncompress/RawInflate.pm +++ b/lib/IO/Uncompress/RawInflate.pm @@ -5,16 +5,16 @@ use strict ; use warnings; use bytes; -use Compress::Raw::Zlib 2.206 ; -use IO::Compress::Base::Common 2.206 qw(:Status ); +use Compress::Raw::Zlib 2.207 ; +use IO::Compress::Base::Common 2.207 qw(:Status ); -use IO::Uncompress::Base 2.206 ; -use IO::Uncompress::Adapter::Inflate 2.206 ; +use IO::Uncompress::Base 2.207 ; +use IO::Uncompress::Adapter::Inflate 2.207 ; require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, %DEFLATE_CONSTANTS, $RawInflateError); -$VERSION = '2.206'; +$VERSION = '2.207'; $RawInflateError = ''; @ISA = qw(IO::Uncompress::Base Exporter); @@ -1138,7 +1138,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/lib/IO/Uncompress/Unzip.pm b/lib/IO/Uncompress/Unzip.pm index 81c78e8..730bd98 100644 --- a/lib/IO/Uncompress/Unzip.pm +++ b/lib/IO/Uncompress/Unzip.pm @@ -9,14 +9,14 @@ use warnings; use bytes; use IO::File; -use IO::Uncompress::RawInflate 2.206 ; -use IO::Compress::Base::Common 2.206 qw(:Status ); -use IO::Uncompress::Adapter::Inflate 2.206 ; -use IO::Uncompress::Adapter::Identity 2.206 ; -use IO::Compress::Zlib::Extra 2.206 ; -use IO::Compress::Zip::Constants 2.206 ; +use IO::Uncompress::RawInflate 2.207 ; +use IO::Compress::Base::Common 2.207 qw(:Status ); +use IO::Uncompress::Adapter::Inflate 2.207 ; +use IO::Uncompress::Adapter::Identity 2.207 ; +use IO::Compress::Zlib::Extra 2.207 ; +use IO::Compress::Zip::Constants 2.207 ; -use Compress::Raw::Zlib 2.206 () ; +use Compress::Raw::Zlib 2.207 () ; BEGIN { @@ -38,7 +38,7 @@ require Exporter ; our ($VERSION, @ISA, @EXPORT_OK, %EXPORT_TAGS, $UnzipError, %headerLookup); -$VERSION = '2.206'; +$VERSION = '2.207'; $UnzipError = ''; @ISA = qw(IO::Uncompress::RawInflate Exporter); @@ -2012,7 +2012,7 @@ See the Changes file. =head1 COPYRIGHT AND LICENSE -Copyright (c) 2005-2023 Paul Marquess. All rights reserved. +Copyright (c) 2005-2024 Paul Marquess. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. diff --git a/t/000prereq.t b/t/000prereq.t index 72ba3f2..2602f47 100644 --- a/t/000prereq.t +++ b/t/000prereq.t @@ -25,7 +25,7 @@ BEGIN if eval { require Test::NoWarnings ; Test::NoWarnings->import; 1 }; - my $VERSION = '2.206'; + my $VERSION = '2.207'; my @NAMES = qw( Compress::Raw::Bzip2 Compress::Raw::Zlib