From ea1fcf80793c61079c6e9680286a3a2b185c944f Mon Sep 17 00:00:00 2001 From: pmqs Date: Sat, 27 Apr 2024 09:23:23 +0100 Subject: Update zipdetails to version 4.003 see https://github.com/pmqs/zipdetails/releases/tag/v4.003 for changes --- bin/zipdetails | 191 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 154 insertions(+), 37 deletions(-) diff --git a/bin/zipdetails b/bin/zipdetails index f961253..7dd1f55 100755 --- a/bin/zipdetails +++ b/bin/zipdetails @@ -29,7 +29,7 @@ use Encode; use Getopt::Long; use List::Util qw(min max); -my $VERSION = '4.002' ; +my $VERSION = '4.004' ; sub fatal_tryWalk; sub fatal_truncated ; @@ -1424,7 +1424,7 @@ exit $exit_status_code ; sub dislayMessages { - # Compare Central & LOcal for discrepencies + # Compare Central & Local for discrepencies if ($CentralDirectory->isMiniZipEncrypted) { @@ -2063,10 +2063,6 @@ sub LocalHeader $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) ; @@ -2094,6 +2090,10 @@ sub LocalHeader walkExtra($extraLength, $localEntry); } + # APPNOTE 6.3.10, sec 4.3.8 + warning $FH->tell - $filenameLength, "Directory '$filename' must not have a payload" + if ! $streaming && $filename =~ m#/$# && $localEntry->uncompressedSize ; + my @msg ; # if ($cdZip64 && ! $ZIP64) # { @@ -2384,6 +2384,94 @@ sub redactFilename return $filename; } +sub validateDirectory +{ + # Check that Directries are stored correctly + # + # 1. Filename MUST end with a "/" + # see APPNOTE 6.3.10, sec 4.3.8 + # 2. Uncompressed size == 0 + # see APPNOTE 6.3.10, sec 4.3.8 + # 3. warn if compressed size > 0 and Uncompressed size == 0 + # 4. check for presence of DOS directory attrib in External Attributes + # 5. Check for Unix extrnal attribute S_IFDIR + + my $offset = shift ; + my $filename = shift ; + my $extractVersion = shift; + my $versionMadeBy = shift; + my $compressedSize = shift; + my $uncompressedSize = shift; + my $externalAttributes = shift; + + my $dosAttributes = $externalAttributes & 0xFFFF; + my $otherAttributes = ($externalAttributes >> 16 ) & 0xFFFF; + + my $probablyDirectory = 0; + my $filenameOK = 0; + my $attributesSet = 0; + my $dosAttributeSet = 0; + my $unixAttributeSet = 0; + + if ($filename =~ m#/$#) + { + # filename claims it is a directory. + $probablyDirectory = 1; + $filenameOK = 1; + } + + if ($dosAttributes & 0x0010) # ATTR_DIRECTORY + { + $probablyDirectory = 1; + $attributesSet = 1 ; + $dosAttributeSet = 1 ; + } + + if ($versionMadeBy == 3 && $otherAttributes & 0x4000) # Unix & S_IFDIR + { + $probablyDirectory = 1; + $attributesSet = 1; + $unixAttributeSet = 1; + } + + return + unless $probablyDirectory ; + + error $offset + CentralDirectoryEntry::Offset_Filename(), + "Directory '$filename' must end in a '/'", + "'External Attributes' flag this as a directory" + if ! $filenameOK && $uncompressedSize == 0; + + info $offset + CentralDirectoryEntry::Offset_ExternalAttributes(), + "DOS Directory flag not set in 'External Attributes' for Directory '$filename'" + if $filenameOK && ! $dosAttributeSet; + + info $offset + CentralDirectoryEntry::Offset_ExternalAttributes(), + "Unix Directory flag not set in 'External Attributes' for Directory '$filename'" + if $filenameOK && $versionMadeBy == 3 && ! $unixAttributeSet; + + if ($uncompressedSize != 0) + { + # APPNOTE 6.3.10, sec 4.3.8 + error $offset + CentralDirectoryEntry::Offset_UncompressedSize(), + "Directory '$filename' must not have a payload" + } + elsif ($compressedSize != 0) + { + + info $offset + CentralDirectoryEntry::Offset_CompressedSize(), + "Directory '$filename' has compressed payload that uncompresses to nothing" + } + + if ($extractVersion < 20) + { + # APPNOTE 6.3.10, sec 4.4.3.2 + my $got = decodeZipVer($extractVersion); + warning $offset + CentralDirectoryEntry::Offset_VersionNeededToExtract(), + "'Extract Zip Spec' is '$got'. Need value >= '2.0' for Directory '$filename'" + } +} + sub validateFilename { my $filename = shift ; @@ -2624,30 +2712,28 @@ sub CentralHeader # 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 ; + 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" if $dos_attrib & 0x0080 ; + out1 "[Bit 8]", "Temporary" if $dos_attrib & 0x0100 ; + out1 "[Bit 9]", "Sparse" if $dos_attrib & 0x0200 ; + out1 "[Bit 10]", "Reparse Point" if $dos_attrib & 0x0400 ; + out1 "[Bit 11]", "Compressed" if $dos_attrib & 0x0800 ; + + out1 "[Bit 12]", "Offline" if $dos_attrib & 0x1000 ; + out1 "[Bit 13]", "Not Indexed" if $dos_attrib & 0x2000 ; + + # Zip files created on Mac seem to set this bit. Not clear why. + out1 "[Bit 14]", "Possible Mac Flag" if $dos_attrib & 0x4000 ; + + # p7Zip & 7z set this bit to flag that the high 16-bits are Unix attributes + out1 "[Bit 15]", "Possible p7zip/7z Unix Flag" if $dos_attrib & 0x8000 ; + } my $native_attrib = ($ext_file_attrib >> 16 ) & 0xFFFF; @@ -2724,7 +2810,7 @@ sub CentralHeader 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) ; + $locHeaderOffset = checkOffsetValue($locHeaderOffset, $startRecordOffset, 0, $commonMessage, $startRecordOffset + CentralDirectoryEntry::Offset_RelativeOffsetToLocal(), ZIP_LOCAL_HDR_SIG) ; } my $filename = ''; @@ -2736,10 +2822,6 @@ sub CentralHeader $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) ; @@ -2769,6 +2851,9 @@ sub CentralHeader # $cdEntry->endCentralHeaderOffset($FH->tell() - 1); + # Can only validate for directory after zip64 data is read + validateDirectory($cdEntryOffset, $filename, $extractVer, $made_by, + $cdEntry->compressedSize, $cdEntry->uncompressedSize, $ext_file_attrib); if ($comment_length) { @@ -5655,7 +5740,9 @@ sub scanCentralDirectory # 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) ; + checkOffsetValue($locHeaderOffset, $startHeader, 0, $commonMessage, + $startHeader + CentralDirectoryEntry::Offset_RelativeOffsetToLocal(), + ZIP_LOCAL_HDR_SIG, 1) ; } $fh->read(my $filename, $filename_length) ; @@ -6323,6 +6410,24 @@ sub nibbles use parent -norequire , 'LocalCentralEntryBase' ; + use constant Offset_VersionMadeBy => 4; + use constant Offset_VersionNeededToExtract => 6; + use constant Offset_GeneralPurposeFlags => 8; + use constant Offset_CompressionMethod => 10; + use constant Offset_ModificationTime => 12; + use constant Offset_ModificationDate => 14; + use constant Offset_CRC32 => 16; + use constant Offset_CompressedSize => 20; + use constant Offset_UncompressedSize => 24; + use constant Offset_FilenameLength => 28; + use constant Offset_ExtraFieldLength => 30; + use constant Offset_FileCommentLength => 32; + use constant Offset_DiskNumber => 34; + use constant Offset_InternalAttributes => 36; + use constant Offset_ExternalAttributes => 38; + use constant Offset_RelativeOffsetToLocal => 42; + use constant Offset_Filename => 46; + sub new { my $class = shift ; @@ -6551,6 +6656,18 @@ sub nibbles use parent -norequire , 'LocalCentralEntryBase' ; + use constant Offset_VersionNeededToExtract => 4; + use constant Offset_GeneralPurposeFlags => 6; + use constant Offset_CompressionMethod => 8; + use constant Offset_ModificationTime => 10; + use constant Offset_ModificationDate => 12; + use constant Offset_CRC32 => 14; + use constant Offset_CompressedSize => 18; + use constant Offset_UncompressedSize => 22; + use constant Offset_FilenameLength => 26; + use constant Offset_ExtraFieldLength => 27; + use constant Offset_Filename => 30; + sub new { my $class = shift ; @@ -6647,7 +6764,7 @@ sub nibbles my $existingEntry = $self->{byName}{$filename} ; - my $endSurfaceArea = $payloadOffset + ($localEntry->compressedSize //0) ; + my $endSurfaceArea = $payloadOffset + ($localEntry->compressedSize // 0) ; if ($existingEntry) { -- cgit v1.2.3