const zlib = require("node:zlib"); const { crc32 } = require("./import-estx.cjs"); // ZIP32 container used by Office Open XML and ESTX; never extracts paths to disk. function pack(files) { const local = [], central = []; let offset = 0; for (const [name, value] of files) { const filename = Buffer.from(name, "utf8"), data = Buffer.from(value), compressed = zlib.deflateRawSync(data), crc = crc32(data); const header = Buffer.alloc(30); header.writeUInt32LE(0x04034b50); header.writeUInt16LE(20, 4); header.writeUInt16LE(0x800, 6); header.writeUInt16LE(8, 8); header.writeUInt16LE(0x21, 12); header.writeUInt32LE(crc, 14); header.writeUInt32LE(compressed.length, 18); header.writeUInt32LE(data.length, 22); header.writeUInt16LE(filename.length, 26); const directory = Buffer.alloc(46); directory.writeUInt32LE(0x02014b50); directory.writeUInt16LE(20, 4); header.copy(directory, 6, 4, 30); directory.writeUInt32LE(offset, 42); local.push(header, filename, compressed); central.push(directory, filename); offset += header.length + filename.length + compressed.length; } const directory = Buffer.concat(central), end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50); end.writeUInt16LE(files.size, 8); end.writeUInt16LE(files.size, 10); end.writeUInt32LE(directory.length, 12); end.writeUInt32LE(offset, 16); return Buffer.concat([...local, directory, end]); } function unpack(buffer) { let end = buffer.length - 22; for (; end >= Math.max(0, buffer.length - 65557); end--) if (buffer.readUInt32LE(end) === 0x06054b50) break; if (end < Math.max(0, buffer.length - 65557)) throw Error("ZIP 디렉터리 없음"); const count = buffer.readUInt16LE(end + 10), files = new Map(); if (count > 3000) throw Error("ZIP 항목 수 초과"); let position = buffer.readUInt32LE(end + 16), total = 0; for (let i = 0; i < count; i++) { if (buffer.readUInt32LE(position) !== 0x02014b50) throw Error("ZIP 헤더 오류"); const flags = buffer.readUInt16LE(position + 8), method = buffer.readUInt16LE(position + 10), crc = buffer.readUInt32LE(position + 16), compressed = buffer.readUInt32LE(position + 20), size = buffer.readUInt32LE(position + 24), length = buffer.readUInt16LE(position + 28), extra = buffer.readUInt16LE(position + 30), comment = buffer.readUInt16LE(position + 32), at = buffer.readUInt32LE(position + 42), name = buffer.subarray(position + 46, position + 46 + length).toString("utf8"); if ( flags & 1 || !name || name.startsWith("/") || name.includes("\\") || name.split("/").includes("..") ) throw Error("지원하지 않는 ZIP 경로/암호화"); total += size; if (total > 256 * 1024 * 1024) throw Error("압축 해제 크기 초과"); if (buffer.readUInt32LE(at) !== 0x04034b50) throw Error("ZIP 로컬 헤더 오류"); const start = at + 30 + buffer.readUInt16LE(at + 26) + buffer.readUInt16LE(at + 28); if (start + compressed > buffer.length) throw Error("ZIP 데이터 범위 오류"); const source = buffer.subarray(start, start + compressed), data = method === 8 ? zlib.inflateRawSync(source, { maxOutputLength: 256 * 1024 * 1024 }) : method === 0 ? source : null; if (!data || data.length !== size || crc32(data) !== crc || files.has(name)) throw Error("ZIP CRC/중복 오류"); files.set(name, data); position += 46 + length + extra + comment; } return files; } module.exports = { pack, unpack };