code wiki / (root) / zip_header.nx

zip_header.nx source

↩ module page · 203 lines · 7611 B

1// zip_header.nx -- parse PKZIP APPNOTE 6.3.x framing. 2// 3// Parses two key structures in a .zip archive: 4// 1. Local File Header (LFH) -- one per stored file, prefixes 5// each compressed payload. Signature 0x04034b50. 6// 2. End of Central Directory record (EOCD) -- at the tail of 7// the archive; points to the central directory. Signature 8// 0x06054b50. 9// 10// This module does NOT parse the central directory entries (CDFH) 11// or decompress payloads. With just LFH + EOCD the Nishi stack 12// can: detect zip framing, count members, read filenames, 13// identify compression method (0=stored, 8=DEFLATE, 14=LZMA, 14// 93=Zstd...), and find the offset of each file's raw bytes. 15// 16// All multi-byte values in ZIP are little-endian. 17// 18// Invariants: 19// Z1 LFH signature must be 0x04034b50. 20// Z2 EOCD signature must be 0x06054b50 (may be preceded by a 21// variable-length ZIP comment; caller scans backward). 22// Z3 Filenames are stored raw; if bit 11 of general-purpose 23// flag is set, they are UTF-8; otherwise CP437. We expose 24// offset+length and let caller decide. 25// Z4 Compressed-size / uncompressed-size may be 0xFFFFFFFF to 26// indicate Zip64 extensions; we surface that as a sentinel 27// for now. 28 29import "syscalls.nx" 30 31const ZIP_LFH_SIG: i64 = 0x04034b50 32const ZIP_EOCD_SIG: i64 = 0x06054b50 33 34const ZIP_ERR_FORMAT: i64 = -1 35const ZIP_ERR_SHORT: i64 = -2 36 37const ZIP64_MARKER: i64 = 0xFFFFFFFF 38 39struct ZipLocalHeader { 40 version_needed: i64, 41 flag: i64, 42 method: i64, // 0 stored, 8 deflate, 14 lzma, 93 zstd 43 mod_time: i64, // MS-DOS time 44 mod_date: i64, // MS-DOS date 45 crc32: i64, 46 compressed_size: i64, 47 uncompressed_size: i64, 48 name_off: i64, name_len: i64, 49 extra_off: i64, extra_len: i64, 50 // Header length so caller knows where the compressed payload starts. 51 header_len: i64, 52} 53 54struct ZipEndOfCentralDir { 55 disk_number: i64, 56 cd_start_disk: i64, 57 entries_this_disk: i64, 58 entries_total: i64, 59 cd_size: i64, 60 cd_offset: i64, 61 comment_off: i64, 62 comment_len: i64, 63} 64 65// Little-endian reads. 66func zip_read_u16(buf: *u8, off: i64) -> i64 { 67 return buf[off] | (buf[off + 1] << 8) 68} 69 70func zip_read_u32(buf: *u8, off: i64) -> i64 { 71 let b0: i64 = buf[off] 72 let b1: i64 = buf[off + 1] 73 let b2: i64 = buf[off + 2] 74 let b3: i64 = buf[off + 3] 75 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 76} 77 78// Parse a Local File Header at buf[off]. Returns 0 on success 79// or a negative ZIP_ERR_*. 80func zip_parse_lfh(buf: *u8, n: i64, off: i64, h: *ZipLocalHeader) -> i64 { 81 if n < off + 30 { return ZIP_ERR_SHORT } 82 if zip_read_u32(buf, off) != ZIP_LFH_SIG { return ZIP_ERR_FORMAT } 83 84 h.version_needed = zip_read_u16(buf, off + 4) 85 h.flag = zip_read_u16(buf, off + 6) 86 h.method = zip_read_u16(buf, off + 8) 87 h.mod_time = zip_read_u16(buf, off + 10) 88 h.mod_date = zip_read_u16(buf, off + 12) 89 h.crc32 = zip_read_u32(buf, off + 14) 90 h.compressed_size = zip_read_u32(buf, off + 18) 91 h.uncompressed_size = zip_read_u32(buf, off + 22) 92 let name_len: i64 = zip_read_u16(buf, off + 26) 93 let extra_len: i64 = zip_read_u16(buf, off + 28) 94 95 let name_off: i64 = off + 30 96 if n < name_off + name_len + extra_len { return ZIP_ERR_SHORT } 97 h.name_off = name_off 98 h.name_len = name_len 99 h.extra_off = name_off + name_len 100 h.extra_len = extra_len 101 h.header_len = 30 + name_len + extra_len 102 return 0 103} 104 105// Scan backwards from end-of-buffer for the EOCD signature. ZIP 106// allows a trailing comment up to 65535 bytes, so we bound the 107// scan to the last 66000 bytes. Returns offset of EOCD or -1. 108func zip_find_eocd(buf: *u8, n: i64) -> i64 { 109 var limit: i64 = n - 66000 110 if limit < 0 { limit = 0 } 111 // EOCD is at least 22 bytes; last possible start = n-22. 112 var off: i64 = n - 22 113 while off >= limit { 114 if off + 3 < n { 115 if zip_read_u32(buf, off) == ZIP_EOCD_SIG { return off } 116 } 117 off = off - 1 118 } 119 return -1 120} 121 122// Parse EOCD at given offset. 123func zip_parse_eocd(buf: *u8, n: i64, off: i64, 124 e: *ZipEndOfCentralDir) -> i64 { 125 if n < off + 22 { return ZIP_ERR_SHORT } 126 if zip_read_u32(buf, off) != ZIP_EOCD_SIG { return ZIP_ERR_FORMAT } 127 128 e.disk_number = zip_read_u16(buf, off + 4) 129 e.cd_start_disk = zip_read_u16(buf, off + 6) 130 e.entries_this_disk = zip_read_u16(buf, off + 8) 131 e.entries_total = zip_read_u16(buf, off + 10) 132 e.cd_size = zip_read_u32(buf, off + 12) 133 e.cd_offset = zip_read_u32(buf, off + 16) 134 let comment_len: i64 = zip_read_u16(buf, off + 20) 135 if n < off + 22 + comment_len { return ZIP_ERR_SHORT } 136 e.comment_off = off + 22 137 e.comment_len = comment_len 138 return 0 139} 140 141// Helper: map compression method code to a short name length. 142// Returns 0 if unknown. Used for debug printing; callers print 143// from a static table. 144func zip_method_known(method: i64) -> i64 { 145 if method == 0 { return 1 } // stored 146 if method == 8 { return 1 } // deflate 147 if method == 14 { return 1 } // lzma 148 if method == 93 { return 1 } // zstd 149 if method == 95 { return 1 } // xz 150 if method == 98 { return 1 } // ppmd 151 return 0 152} 153 154// Compile-only smoke: forge minimal LFH + EOCD in a buffer. 155func main() -> i64 { 156 let raw: *u8 = sys_mmap(256) 157 var i: i64 = 0 158 while i < 256 { raw[i] = 0; i = i + 1 } 159 160 // LFH at offset 0: sig, version 20, flag 0, method 0 (stored), 161 // time/date 0, crc 0, csize 5, usize 5, name_len 3 "foo", 162 // extra_len 0. 163 raw[0] = 0x50; raw[1] = 0x4b; raw[2] = 0x03; raw[3] = 0x04 164 raw[4] = 20; raw[5] = 0 165 raw[6] = 0; raw[7] = 0 166 raw[8] = 0; raw[9] = 0 167 raw[18] = 5 // compressed_size low byte 168 raw[22] = 5 // uncompressed_size low byte 169 raw[26] = 3 // name_len 170 raw[30] = 0x66 // 'f' 171 raw[31] = 0x6F // 'o' 172 raw[32] = 0x6F // 'o' 173 174 // EOCD at offset 40: sig, zeros, entries 1, cd_size 0, cd_off 38, 175 // comment_len 0. 22 bytes total. 176 let eocd: i64 = 40 177 raw[eocd] = 0x50; raw[eocd+1] = 0x4b 178 raw[eocd+2] = 0x05; raw[eocd+3] = 0x06 179 raw[eocd+8] = 1 // entries_this_disk low 180 raw[eocd+10] = 1 // entries_total low 181 raw[eocd+16] = 33 // cd_offset low (just any value) 182 183 let lfh_raw: *u8 = sys_mmap(128) 184 let lfh: *ZipLocalHeader = lfh_raw as *ZipLocalHeader 185 if zip_parse_lfh(raw, 256, 0, lfh) != 0 { return 1 } 186 if lfh.method != 0 { return 2 } 187 if lfh.name_len != 3 { return 3 } 188 if lfh.compressed_size != 5 { return 4 } 189 if lfh.header_len != 33 { return 5 } 190 191 let eocd_off: i64 = zip_find_eocd(raw, eocd + 22) 192 if eocd_off != eocd { return 6 } 193 194 let ee_raw: *u8 = sys_mmap(128) 195 let ee: *ZipEndOfCentralDir = ee_raw as *ZipEndOfCentralDir 196 if zip_parse_eocd(raw, 256, eocd_off, ee) != 0 { return 7 } 197 if ee.entries_total != 1 { return 8 } 198 if ee.cd_offset != 33 { return 9 } 199 200 if zip_method_known(8) != 1 { return 10 } 201 if zip_method_known(999) != 0 { return 11 } 202 return 0 203}