Files

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 };