93 lines
3.6 KiB
JavaScript
93 lines
3.6 KiB
JavaScript
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 };
|