mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Add ZIP archive support to the node:zlib module through three classes and a set of helpers: - ZipEntry: a single archive member, with buffered reads (content()), bounded-memory streaming reads (contentIterator()), and create()/createStream() for building members. - ZipFile: random access to an archive backed by a file descriptor, reading members lazily without retaining their content and writing new members in place; opened with open()/openSync(). - ZipBuffer: a zero-copy, in-memory view over an archive already held in a Buffer. createZipArchive() serializes a sequence of entries into an archive byte stream, and setMaxZipContentSize() bounds the default in-memory decompression size. Every operation has both an asynchronous and a synchronous form. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: https://github.com/nodejs/node/pull/64339 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
151 lines
4.7 KiB
JavaScript
151 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
// Promise wrappers over the callback `fs` primitives (`ZipFile` works on a
|
|
// raw fd so one instance can serve both async and `Sync` methods), plus the
|
|
// short-read/short-write loops that guarantee a full transfer.
|
|
|
|
const {
|
|
Promise,
|
|
} = primordials;
|
|
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_STATE,
|
|
ERR_ZIP_INVALID_ARCHIVE,
|
|
},
|
|
} = require('internal/errors');
|
|
const fs = require('fs');
|
|
|
|
// Promisified adapters over the callback `fs` primitives; individually trivial,
|
|
// they only exist so the async archive paths can `await` plain fd operations.
|
|
//
|
|
// `ZipFile` operates on a plain numeric file descriptor (rather than an
|
|
// `fs.promises` `FileHandle`) so that a single instance can support both the
|
|
// async and the `Sync` methods: `fs.read`/`fs.write`/`fs.fstat`/
|
|
// `fs.ftruncate`/`fs.close` all accept a raw fd directly, same as their
|
|
// `*Sync` counterparts, so both call sites share one open file underneath.
|
|
function fsOpenAsync(path, flag) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.open(path, flag, (err, fd) => (err ? reject(err) : resolve(fd)));
|
|
});
|
|
}
|
|
|
|
function fsStatAsync(path) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.stat(path, (err, stats) => (err ? reject(err) : resolve(stats)));
|
|
});
|
|
}
|
|
|
|
function fsLstatAsync(path) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.lstat(path, (err, stats) => (err ? reject(err) : resolve(stats)));
|
|
});
|
|
}
|
|
|
|
function fsReadlinkAsync(path) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.readlink(path, 'utf8', (err, target) => (err ? reject(err) : resolve(target)));
|
|
});
|
|
}
|
|
|
|
function fsCloseAsync(fd) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.close(fd, (err) => (err ? reject(err) : resolve()));
|
|
});
|
|
}
|
|
|
|
function fsFstatAsync(fd) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.fstat(fd, (err, stats) => (err ? reject(err) : resolve(stats)));
|
|
});
|
|
}
|
|
|
|
function fsReadAsync(fd, buffer, offset, length, position) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.read(fd, buffer, offset, length, position, (err, bytesRead) => (err ? reject(err) : resolve(bytesRead)));
|
|
});
|
|
}
|
|
|
|
function fsWriteAsync(fd, buffer, offset, length, position) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.write(fd, buffer, offset, length, position, (err, bytesWritten) => (err ? reject(err) : resolve(bytesWritten)));
|
|
});
|
|
}
|
|
|
|
function fsFtruncateAsync(fd, len) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.ftruncate(fd, len, (err) => (err ? reject(err) : resolve()));
|
|
});
|
|
}
|
|
|
|
// `read(2)` may return fewer bytes than requested without hitting EOF, so loop
|
|
// until `buffer` is entirely filled; a genuine short read (0 bytes) means the
|
|
// archive is truncated where a full header/record was expected.
|
|
async function readFdFully(fd, buffer, position) {
|
|
let done = 0;
|
|
while (done < buffer.length) {
|
|
const bytesRead = await fsReadAsync(fd, buffer, done, buffer.length - done, position + done);
|
|
if (bytesRead <= 0) {
|
|
throw new ERR_ZIP_INVALID_ARCHIVE('unexpected end of file');
|
|
}
|
|
done += bytesRead;
|
|
}
|
|
}
|
|
|
|
// Synchronous counterpart of `readFdFully()`; same short-read loop.
|
|
function readFdFullySync(fd, buffer, position) {
|
|
let done = 0;
|
|
while (done < buffer.length) {
|
|
const bytesRead = fs.readSync(fd, buffer, done, buffer.length - done, position + done);
|
|
if (bytesRead <= 0) {
|
|
throw new ERR_ZIP_INVALID_ARCHIVE('unexpected end of file');
|
|
}
|
|
done += bytesRead;
|
|
}
|
|
}
|
|
|
|
// `write(2)` may write fewer bytes than asked - most notably when an error
|
|
// (ENOSPC, EIO, a full NFS commit) strikes after partial progress, which
|
|
// surfaces as a short, error-free count. Advancing archive offsets by the
|
|
// intended length would then silently corrupt the file, so every archive
|
|
// write loops until the buffer is fully on disk; retrying the remainder
|
|
// re-encounters and surfaces the underlying error.
|
|
async function writeFdFully(fd, buffer, position) {
|
|
let done = 0;
|
|
while (done < buffer.length) {
|
|
const bytesWritten =
|
|
await fsWriteAsync(fd, buffer, done, buffer.length - done, position + done);
|
|
if (bytesWritten <= 0) {
|
|
throw new ERR_INVALID_STATE('a write to the archive made no progress');
|
|
}
|
|
done += bytesWritten;
|
|
}
|
|
}
|
|
|
|
// Synchronous counterpart of `writeFdFully()`; same short-write loop.
|
|
function writeFdFullySync(fd, buffer, position) {
|
|
let done = 0;
|
|
while (done < buffer.length) {
|
|
const bytesWritten =
|
|
fs.writeSync(fd, buffer, done, buffer.length - done, position + done);
|
|
if (bytesWritten <= 0) {
|
|
throw new ERR_INVALID_STATE('a write to the archive made no progress');
|
|
}
|
|
done += bytesWritten;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
fsOpenAsync,
|
|
fsStatAsync,
|
|
fsLstatAsync,
|
|
fsReadlinkAsync,
|
|
fsCloseAsync,
|
|
fsFstatAsync,
|
|
fsFtruncateAsync,
|
|
readFdFully,
|
|
readFdFullySync,
|
|
writeFdFully,
|
|
writeFdFullySync,
|
|
};
|