current commit
260801.6 · zlib: add ZipEntry, ZipFile, and ZipBuffer
'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,
};