Files
2026-07-27 14:07:26 +08:00

372 lines
10 KiB
JavaScript

import { inherits } from "util";
import crc32 from "crc-32";
import { CRC32Stream, DeflateCRC32Stream } from "crc32-stream";
import ArchiveOutputStream from "../archive-output-stream.js";
import ZipArchiveEntry from "./zip-archive-entry.js";
import GeneralPurposeBit from "./general-purpose-bit.js";
import {
LONG_ZERO,
METHOD_DEFLATED,
METHOD_STORED,
MIN_VERSION_DATA_DESCRIPTOR,
MIN_VERSION_ZIP64,
SHORT_ZERO,
SIG_EOCD,
SIG_DD,
SIG_CFH,
SIG_LFH,
SIG_ZIP64_EOCD,
SIG_ZIP64_EOCD_LOC,
VERSION_MADEBY,
ZIP64_EXTRA_ID,
ZIP64_MAGIC,
ZIP64_MAGIC_SHORT,
ZLIB_BEST_SPEED,
} from "./constants.js";
import { getEightBytes, getLongBytes, getShortBytes } from "./util.js";
function _defaults(o) {
if (typeof o !== "object") {
o = {};
}
if (typeof o.zlib !== "object") {
o.zlib = {};
}
if (typeof o.zlib.level !== "number") {
o.zlib.level = ZLIB_BEST_SPEED;
}
o.forceZip64 = !!o.forceZip64;
o.forceLocalTime = !!o.forceLocalTime;
return o;
}
export default class ZipArchiveOutputStream extends ArchiveOutputStream {
constructor(options) {
const _options = _defaults(options);
super(_options);
this.options = _options;
this._entry = null;
this._entries = [];
this._archive = {
centralLength: 0,
centralOffset: 0,
comment: "",
finish: false,
finished: false,
processing: false,
forceZip64: _options.forceZip64,
forceLocalTime: _options.forceLocalTime,
};
}
_afterAppend(ae) {
this._entries.push(ae);
if (ae.getGeneralPurposeBit().usesDataDescriptor()) {
this._writeDataDescriptor(ae);
}
this._archive.processing = false;
this._entry = null;
if (this._archive.finish && !this._archive.finished) {
this._finish();
}
}
_appendBuffer(ae, source, callback) {
if (source.length === 0) {
ae.setMethod(METHOD_STORED);
}
var method = ae.getMethod();
if (method === METHOD_STORED) {
ae.setSize(source.length);
ae.setCompressedSize(source.length);
ae.setCrc(crc32.buf(source) >>> 0);
}
this._writeLocalFileHeader(ae);
if (method === METHOD_STORED) {
this.write(source);
this._afterAppend(ae);
callback(null, ae);
return;
} else if (method === METHOD_DEFLATED) {
this._smartStream(ae, callback).end(source);
return;
} else {
callback(new Error("compression method " + method + " not implemented"));
return;
}
}
_appendStream(ae, source, callback) {
ae.getGeneralPurposeBit().useDataDescriptor(true);
ae.setVersionNeededToExtract(MIN_VERSION_DATA_DESCRIPTOR);
this._writeLocalFileHeader(ae);
var smart = this._smartStream(ae, callback);
source.once("error", function (err) {
smart.emit("error", err);
smart.end();
});
source.pipe(smart);
}
_finish() {
this._archive.centralOffset = this.offset;
this._entries.forEach(
function (ae) {
this._writeCentralFileHeader(ae);
}.bind(this),
);
this._archive.centralLength = this.offset - this._archive.centralOffset;
if (this.isZip64()) {
this._writeCentralDirectoryZip64();
}
this._writeCentralDirectoryEnd();
this._archive.processing = false;
this._archive.finish = true;
this._archive.finished = true;
this.end();
}
_normalizeEntry(ae) {
if (ae.getMethod() === -1) {
ae.setMethod(METHOD_DEFLATED);
}
if (ae.getMethod() === METHOD_DEFLATED) {
ae.getGeneralPurposeBit().useDataDescriptor(true);
ae.setVersionNeededToExtract(MIN_VERSION_DATA_DESCRIPTOR);
}
if (ae.getTime() === -1) {
ae.setTime(new Date(), this._archive.forceLocalTime);
}
ae._offsets = {
file: 0,
data: 0,
contents: 0,
};
}
_smartStream(ae, callback) {
var deflate = ae.getMethod() === METHOD_DEFLATED;
var process = deflate
? new DeflateCRC32Stream(this.options.zlib)
: new CRC32Stream();
var error = null;
function handleStuff() {
var digest = process.digest().readUInt32BE(0);
ae.setCrc(digest);
ae.setSize(process.size());
ae.setCompressedSize(process.size(true));
this._afterAppend(ae);
callback(error, ae);
}
process.once("end", handleStuff.bind(this));
process.once("error", function (err) {
error = err;
});
process.pipe(this, { end: false });
return process;
}
_writeCentralDirectoryEnd() {
var records = this._entries.length;
var size = this._archive.centralLength;
var offset = this._archive.centralOffset;
if (this.isZip64()) {
records = ZIP64_MAGIC_SHORT;
size = ZIP64_MAGIC;
offset = ZIP64_MAGIC;
}
// signature
this.write(getLongBytes(SIG_EOCD));
// disk numbers
this.write(SHORT_ZERO);
this.write(SHORT_ZERO);
// number of entries
this.write(getShortBytes(records));
this.write(getShortBytes(records));
// length and location of CD
this.write(getLongBytes(size));
this.write(getLongBytes(offset));
// archive comment
var comment = this.getComment();
var commentLength = Buffer.byteLength(comment);
this.write(getShortBytes(commentLength));
this.write(comment);
}
_writeCentralDirectoryZip64() {
// signature
this.write(getLongBytes(SIG_ZIP64_EOCD));
// size of the ZIP64 EOCD record
this.write(getEightBytes(44));
// version made by
this.write(getShortBytes(MIN_VERSION_ZIP64));
// version to extract
this.write(getShortBytes(MIN_VERSION_ZIP64));
// disk numbers
this.write(LONG_ZERO);
this.write(LONG_ZERO);
// number of entries
this.write(getEightBytes(this._entries.length));
this.write(getEightBytes(this._entries.length));
// length and location of CD
this.write(getEightBytes(this._archive.centralLength));
this.write(getEightBytes(this._archive.centralOffset));
// extensible data sector
// not implemented at this time
// end of central directory locator
this.write(getLongBytes(SIG_ZIP64_EOCD_LOC));
// disk number holding the ZIP64 EOCD record
this.write(LONG_ZERO);
// relative offset of the ZIP64 EOCD record
this.write(
getEightBytes(this._archive.centralOffset + this._archive.centralLength),
);
// total number of disks
this.write(getLongBytes(1));
}
_writeCentralFileHeader(ae) {
var gpb = ae.getGeneralPurposeBit();
var method = ae.getMethod();
var fileOffset = ae._offsets.file;
var size = ae.getSize();
var compressedSize = ae.getCompressedSize();
if (ae.isZip64() || fileOffset > ZIP64_MAGIC) {
size = ZIP64_MAGIC;
compressedSize = ZIP64_MAGIC;
fileOffset = ZIP64_MAGIC;
ae.setVersionNeededToExtract(MIN_VERSION_ZIP64);
var extraBuf = Buffer.concat(
[
getShortBytes(ZIP64_EXTRA_ID),
getShortBytes(24),
getEightBytes(ae.getSize()),
getEightBytes(ae.getCompressedSize()),
getEightBytes(ae._offsets.file),
],
28,
);
ae.setExtra(extraBuf);
}
// signature
this.write(getLongBytes(SIG_CFH));
// version made by
this.write(getShortBytes((ae.getPlatform() << 8) | VERSION_MADEBY));
// version to extract and general bit flag
this.write(getShortBytes(ae.getVersionNeededToExtract()));
this.write(gpb.encode());
// compression method
this.write(getShortBytes(method));
// datetime
this.write(getLongBytes(ae.getTimeDos()));
// crc32 checksum
this.write(getLongBytes(ae.getCrc()));
// sizes
this.write(getLongBytes(compressedSize));
this.write(getLongBytes(size));
var name = ae.getName();
var comment = ae.getComment();
var extra = ae.getCentralDirectoryExtra();
if (gpb.usesUTF8ForNames()) {
name = Buffer.from(name);
comment = Buffer.from(comment);
}
// name length
this.write(getShortBytes(name.length));
// extra length
this.write(getShortBytes(extra.length));
// comments length
this.write(getShortBytes(comment.length));
// disk number start
this.write(SHORT_ZERO);
// internal attributes
this.write(getShortBytes(ae.getInternalAttributes()));
// external attributes
this.write(getLongBytes(ae.getExternalAttributes()));
// relative offset of LFH
this.write(getLongBytes(fileOffset));
// name
this.write(name);
// extra
this.write(extra);
// comment
this.write(comment);
}
_writeDataDescriptor(ae) {
// signature
this.write(getLongBytes(SIG_DD));
// crc32 checksum
this.write(getLongBytes(ae.getCrc()));
// sizes
if (ae.isZip64()) {
this.write(getEightBytes(ae.getCompressedSize()));
this.write(getEightBytes(ae.getSize()));
} else {
this.write(getLongBytes(ae.getCompressedSize()));
this.write(getLongBytes(ae.getSize()));
}
}
_writeLocalFileHeader(ae) {
var gpb = ae.getGeneralPurposeBit();
var method = ae.getMethod();
var name = ae.getName();
var extra = ae.getLocalFileDataExtra();
if (ae.isZip64()) {
gpb.useDataDescriptor(true);
ae.setVersionNeededToExtract(MIN_VERSION_ZIP64);
}
if (gpb.usesUTF8ForNames()) {
name = Buffer.from(name);
}
ae._offsets.file = this.offset;
// signature
this.write(getLongBytes(SIG_LFH));
// version to extract and general bit flag
this.write(getShortBytes(ae.getVersionNeededToExtract()));
this.write(gpb.encode());
// compression method
this.write(getShortBytes(method));
// datetime
this.write(getLongBytes(ae.getTimeDos()));
ae._offsets.data = this.offset;
// crc32 checksum and sizes
if (gpb.usesDataDescriptor()) {
this.write(LONG_ZERO);
this.write(LONG_ZERO);
this.write(LONG_ZERO);
} else {
this.write(getLongBytes(ae.getCrc()));
this.write(getLongBytes(ae.getCompressedSize()));
this.write(getLongBytes(ae.getSize()));
}
// name length
this.write(getShortBytes(name.length));
// extra length
this.write(getShortBytes(extra.length));
// name
this.write(name);
// extra
this.write(extra);
ae._offsets.contents = this.offset;
}
getComment(comment) {
return this._archive.comment !== null ? this._archive.comment : "";
}
isZip64() {
return (
this._archive.forceZip64 ||
this._entries.length > ZIP64_MAGIC_SHORT ||
this._archive.centralLength > ZIP64_MAGIC ||
this._archive.centralOffset > ZIP64_MAGIC
);
}
setComment(comment) {
this._archive.comment = comment;
}
}