From 6c0ac96eb37007ebf86dce6d6d3099f3f9fe8dc5 Mon Sep 17 00:00:00 2001 From: Armijn Hemel Date: Thu, 5 Feb 2026 13:43:48 +0100 Subject: [PATCH] add a test file for a directory with a name not ending with a / character --- testdata/readme.directory_no_slash | 158 ++++++++++++++++++ testdata/test_dir_no_slash.zip | Bin 0 -> 160 bytes ...iplinter__test__test_dir_no_slash.zip.snap | 152 +++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 testdata/readme.directory_no_slash create mode 100644 testdata/test_dir_no_slash.zip create mode 100644 ziplinter/src/snapshots/ziplinter__test__test_dir_no_slash.zip.snap diff --git a/testdata/readme.directory_no_slash b/testdata/readme.directory_no_slash new file mode 100644 index 0000000..2cc3811 --- /dev/null +++ b/testdata/readme.directory_no_slash @@ -0,0 +1,158 @@ +# Directory names + +Most (not all) ZIP implementations rely on names of directories being stored +with a `/` at the end of the entry name, even though the official ZIP +specification does not mention that a `/` is mandatory for a directory. At some +point this just became a convention. For example, the .NET API seems to rely on +it: + + + + +as does `unzip` (comment from `zip30.tar.gz`, file `unix/unix.c`, line 163): + +``` +/* Add trailing / to the directory name */ +``` + +Python's `zipfile` module also requires it. `p7zip` on the other hand does not. + +There actually are some ZIP files that contain files where directory names do +not end in `/` and where most implementations fail: instead of creating a +directory a zero byte file with the same name as the directory is written. +Despite bug reports having been filed this is still a problem. A bug report +describing this problem can be found at: + + + +Trying to unpack the file mentioned in this bug report leads to the following +error with `unzip`: + +``` +$ unzip 1_06_03P.zip +Archive: 1_06_03P.zip + extracting: online_upgrade_img +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/bp28v_md5. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/bp28_md5. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/emergency_recovery.sh. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/J120.bp28. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/machine_type. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/md5. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/ouimg.bin. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/ouimg.ver. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/OU_Burner. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/Software_Version. +checkdir error: online_upgrade_img exists but is not directory + unable to process online_upgrade_img/V10X.bp28v. +``` + +`p7zip` will correctly unpack the archive: + +``` +$ 7z x 1_06_03P.zip + +7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 +p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-6770HQ CPU @ 2.60GHz (506E3),ASM,AES-NI) + +Scanning the drive for archives: +1 file, 180253537 bytes (172 MiB) + +Extracting archive: 1_06_03P.zip +-- +Path = 1_06_03P.zip +Type = zip +Physical Size = 180253537 + +Everything is Ok + +Folders: 1 +Files: 12 +Size: 190915623 +Compressed: 180253537 +``` + +A workaround for programs depending on Python's `zipfile` module (which cannot +correctly unpack these files) some workarounds are needed by first looking at +the "external file attributes" field from the central directory (section +4.3.12) and checking if the low order byte corresponds to the MS-DOS directory +attribute byte (section 4.4.15) while also checking that the size recorded for +the file is 0 and that Python's `zipfile` module does not recognize the file as +a directory. If this is the case, then the directory is not unpacked with +Python's `zipfile` module, and a directory with the name of the entry can be +created instead. + +This might not be entirely fool proof, but it seems to be such a very rare edge +case that so far only very few examples have been found in the wild. + +The test file `test_dir_no_slash.zip` contains a single entry with a directory +named `test`. In the file the slash character was replaced by a NUL character. +`unzip` unpacks an empty file, while `p7zip` unpacks a directory. `zipinfo` +correctly identifies it as a directory in the field `MS-DOS file attributes` +as well as in the Unix file attributes (stored as an "extra" field). + +``` +$ zipinfo -v test_dir_no_slash.zip +Archive: test_dir_no_slash.zip +There is no zipfile comment. + +End-of-central-directory record: +------------------------------- + + Zip archive file size: 160 (00000000000000A0h) + Actual end-cent-dir record offset: 138 (000000000000008Ah) + Expected end-cent-dir record offset: 138 (000000000000008Ah) + (based on the length of the central directory and its expected offset) + + This zipfile constitutes the sole disk of a single-part archive; its + central directory contains 1 entry. + The central directory is 75 (000000000000004Bh) bytes long, + and its (expected) offset in bytes from the beginning of the zipfile + is 63 (000000000000003Fh). + + +Central directory entry #1: +--------------------------- + + test + + offset of local header from start of archive: 0 + (0000000000000000h) bytes + file system or operating system of origin: Unix + version of encoding software: 3.0 + minimum file system compatibility required: MS-DOS, OS/2 or NT FAT + minimum software version required to extract: 1.0 + compression method: none (stored) + file security status: not encrypted + extended local header: no + file last modified on (DOS date/time): 2026 Feb 5 13:30:40 + file last modified on (UT extra field modtime): 2026 Feb 5 13:30:40 local + file last modified on (UT extra field modtime): 2026 Feb 5 12:30:40 UTC + 32-bit CRC value (hex): 00000000 + compressed size: 0 bytes + uncompressed size: 0 bytes + length of filename: 5 characters + length of extra field: 24 bytes + length of file comment: 0 characters + disk number on which file begins: disk 1 + apparent file type: binary + Unix file attributes (040755 octal): drwxr-xr-x + MS-DOS file attributes (10 hex): dir + + The central-directory extra field contains: + - A subfield with ID 0x5455 (universal time) and 5 data bytes. + The local extra field has UTC/GMT modification/access times. + - A subfield with ID 0x7875 (Unix UID/GID (any size)) and 11 data bytes: + 01 04 e8 03 00 00 04 e8 03 00 00. + + There is no file comment. +``` diff --git a/testdata/test_dir_no_slash.zip b/testdata/test_dir_no_slash.zip new file mode 100644 index 0000000000000000000000000000000000000000..e7f3ece38685c7d3cd79d9d55bc5d48e7e663dda GIT binary patch literal 160 zcmWIWW@h1H0D&vnt}$Q+lwf6$VJJy0E@21_;bdSg=xxcY1>({QZU#n{7t9O{U?RYq okx7mjmjMz`qXZbie)03sh4LjV8( literal 0 HcmV?d00001 diff --git a/ziplinter/src/snapshots/ziplinter__test__test_dir_no_slash.zip.snap b/ziplinter/src/snapshots/ziplinter__test__test_dir_no_slash.zip.snap new file mode 100644 index 0000000..a5d4238 --- /dev/null +++ b/ziplinter/src/snapshots/ziplinter__test__test_dir_no_slash.zip.snap @@ -0,0 +1,152 @@ +--- +source: ziplinter/src/lib.rs +expression: result +--- +{ + "comment": "", + "contents": [ + { + "central": { + "comment": "", + "compressed_size": 0, + "crc32": 0, + "creator_version": { + "host_system": "Unix", + "version": 30 + }, + "disk_nbr_start": 0, + "external_attrs": 1106051088, + "extra": [ + 85, + 84, + 5, + 0, + 3, + 112, + 141, + 132, + 105, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "header_offset": 0, + "internal_attrs": 0, + "method": "Store", + "mode": 2147484141, + "modified": "2026-02-05T12:30:40Z", + "name": "test\u0000", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uncompressed_size": 0 + }, + "local": { + "accessed": null, + "compressed_size": 0, + "crc32": 0, + "created": null, + "extra": [ + 85, + 84, + 9, + 0, + 3, + 112, + 141, + 132, + 105, + 125, + 141, + 132, + 105, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "gid": 1000, + "header_offset": 0, + "method": "Store", + "method_specific": "None", + "mode": 0, + "modified": "2026-02-05T12:30:40Z", + "name": "test\u0000", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uid": 1000, + "uncompressed_size": 0 + } + } + ], + "encoding": "Utf8", + "eocd": { + "dir": { + "inner": { + "dir_disk_nbr": 0, + "dir_records_this_disk": 1, + "directory_offset": 63, + "directory_records": 1, + "directory_size": 75, + "disk_nbr": 0 + }, + "offset": 138 + }, + "dir64": null, + "global_offset": 0 + }, + "parsed_ranges": [ + { + "contains": "end of central directory record", + "end": 160, + "start": 138 + }, + { + "contains": "central directory header", + "end": 138, + "filename": "test\u0000", + "start": 63 + }, + { + "contains": "local file header", + "end": 63, + "filename": "test\u0000", + "start": 0 + }, + { + "contains": "file data", + "end": 63, + "filename": "test\u0000", + "start": 63 + } + ], + "size": 160 +}