Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .c8rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"all": true,
"exclude": ["*.js", "*.d.ts", "**/*.test.ts", "types/"],
"reporter": ["text", "lcovonly"]
}
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "22.x"
node-version-file: ".node-version"
cache: "npm"
registry-url: "https://registry.npmjs.org"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "22.x"
node-version-file: ".node-version"
cache: "npm"
registry-url: "https://registry.npmjs.org"

Expand Down
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.0.1
10 changes: 8 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
},
"eslint.useFlatConfig": true,
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": ["decompressors"],
"code-coverage-lcov.path.searchPath": "coverage/lcov.info"
"cSpell.words": ["decompressors", "eocdl", "eocdr"],
"code-coverage-lcov.path.searchPath": "coverage/lcov.info",
"nodejs-testing.extensions": [
{
"extensions": ["ts"],
"parameters": []
}
]
}
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ Nope! Reading a zip forwards from start to finish is something that the zip form

## Writing Zips

### With `ZipWriter.open()`

You can use `ZipWriter.open()` on Node.js to write directly to a file:

```ts
import { ZipWriter } from "zip24/writer";

Expand All @@ -106,9 +110,28 @@ await writer.addFile(
"this will be stored as-is",
);

await writer.finalize("Gordon is cool");
await writer.close();
```

### With `destination` option

You can also output to any writer you'd like. On Node, [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable) is also supported as a destination. Note that the destination must be an instance of `Writable`, not just "Writable-like".

```ts
import { ZipWriter } from "zip24/writer";

const destination = createWritableStreamSomehow();
const writer = new ZipWriter({ destination });

// etc

await writer.close();
```

### With streams

`ZipWriter` is also a [TransformStream](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream), so you can pipe `ZipEntry`/`ZipEntryInfo` instances in and pipe the output to a stream of your choice.

## About this library

### Why `zip24`?
Expand Down
3 changes: 2 additions & 1 deletion docs/records.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ This record has an optional signature value of `0x08074b50` which precedes the o
| 32 | total entries on all disks | 8 |
| 40 | size of the central directory | 8 |
| 48 | central directory offset | 8 |
| 56 | (end) | |
| 56 | extensible data sector | ... |
| ... | (56 + record size - 12) | |

## Zip64 End of Central Directory Locator (4.3.15)

Expand Down
51 changes: 40 additions & 11 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
import config from "@propulsionworks/eslint-config";
import propulsionworks, { config } from "@propulsionworks/eslint-config";

export default [
config.configs["base"],
config.configs["ts"],
export default config(
{
ignores: ["node_modules/", "lib/", "eslint.config.js"],
},
{
files: ["**/*.js", "**/*.ts"],
extends: [propulsionworks.configs.js],
},
{
files: ["**/*.ts"],
extends: [propulsionworks.configs.ts],
rules: {
//"unicorn/no-unused-properties": "warn",
"unicorn/number-literal-case": "off", // fights with prettier
"unicorn/numeric-separators-style": "off", // annoying
// we can turn this off due to the other rules that stop us abusing `any`
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ ignoreRestSiblings: true },
],
"@typescript-eslint/unified-signatures": [
"error",
{
ignoreDifferentlyNamedParameters: true,
ignoreOverloadsWithDifferentJSDoc: true,
},
],
"n/no-unsupported-features/node-builtins": "off",
},
},
{
files: ["src/**/*.test.*"],
files: ["**/*.test.ts"],
extends: [propulsionworks.configs["ts-relaxed-any"]],

rules: {
"@typescript-eslint/no-floating-promises": "off", // `describe` and `it` return promises
// `describe` and `it` return promises
"@typescript-eslint/no-floating-promises": [
"warn",
{
allowForKnownSafeCalls: [
{ from: "package", name: ["describe", "it"], package: "node:test" },
],
},
],

"@typescript-eslint/no-non-null-asserted-optional-chain": "off", // easier for testing
"@typescript-eslint/no-non-null-assertion": "off", // easier for testing
"n/no-unsupported-features/node-builtins": "off", // so we can use node:test
"@typescript-eslint/no-unused-vars": "off", // easier for testing
"@typescript-eslint/require-await": "off", // easier for testing
"unicorn/no-abusive-eslint-disable": "off",
},
},
];
);
24 changes: 10 additions & 14 deletions src/web/buffer.test.ts → exports/buffer.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import assert from "node:assert";
import { buffer } from "node:stream/consumers";
import { buffer, text } from "node:stream/consumers";
import { describe, it, mock } from "node:test";
import { CompressionMethod } from "../core/compression-core.js";
import { ZipPlatform, ZipVersion } from "../core/constants.js";
import { UnixFileAttributes } from "../core/file-attributes.js";
import { ZipEntry } from "../core/zip-entry.js";
import {
EmptyZip32,
Zip32WithThreeEntries,
generateZip,
} from "../test-util/fixtures.js";
import { ZipBufferReader } from "./buffer.js";
} from "../test-util/fixtures.ts";
import { ZipBufferReader } from "./buffer.ts";
import { ZipEntry, ZipEntryReader } from "./entry.ts";
import { CompressionMethod, ZipPlatform, ZipVersion } from "./raw/constants.ts";
import { UnixFileAttributes } from "./raw/file-attributes.ts";

describe("web/buffer", () => {
describe("ZipBufferReader", () => {
Expand Down Expand Up @@ -44,7 +43,7 @@ describe("web/buffer", () => {
it("iterates all the files", async () => {
const reader = new ZipBufferReader(Zip32WithThreeEntries);

const files: ZipEntry[] = [];
const files: ZipEntryReader[] = [];
for (const file of reader.filesSync()) {
files.push(file);
}
Expand Down Expand Up @@ -80,7 +79,7 @@ describe("web/buffer", () => {
assert.strictEqual(file0.isDirectory, false);
assert.strictEqual(file0.isFile, true);

assert.strictEqual(await file0.toText(), "this is the file 1 content");
assert.strictEqual(await text(file0), "this is the file 1 content");

//// FILE 1
const file1 = files[1];
Expand Down Expand Up @@ -111,10 +110,7 @@ describe("web/buffer", () => {
assert.strictEqual(file1.isDirectory, false);
assert.strictEqual(file1.isFile, true);

assert.strictEqual(
await file1.toText(),
"file 2 content goes right here",
);
assert.strictEqual(await text(file1), "file 2 content goes right here");

//// FILE 2
const file2 = files[2];
Expand Down Expand Up @@ -144,7 +140,7 @@ describe("web/buffer", () => {
assert.strictEqual(file2.isDirectory, true);
assert.strictEqual(file2.isFile, false);

assert.strictEqual(await file2.toText(), "");
assert.strictEqual(await text(file2), "");
});
});

Expand Down
86 changes: 86 additions & 0 deletions exports/buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { BufferView, type BufferLike } from "../util/binary.ts";
import { ZipEntryReader } from "./entry.ts";
import { CentralDirectoryBufferReader } from "./raw/central-directory-header.ts";
import { LocalFileHeader } from "./raw/local-file-header.ts";
import {
Eocdr,
Zip64Eocdl,
Zip64Eocdr,
ZipTrailer,
} from "./raw/zip-trailer.ts";

/**
* An object which can read zip data from a buffer.
*/
export class ZipBufferReader
implements AsyncIterable<ZipEntryReader>, Iterable<ZipEntryReader>
{
readonly #buffer: BufferView;
readonly #directory: CentralDirectoryBufferReader;

/**
* The zip file comment, if set.
*/
public get comment(): string {
return this.#directory.comment;
}

/**
* The number of file entries in the zip.
*/
public get entryCount(): number {
return this.#directory.count;
}

public constructor(buffer: BufferLike) {
this.#buffer = new BufferView(buffer);

const eocdrOffset = Eocdr.findOffset(buffer);
const eocdr = Eocdr.deserialize(buffer, eocdrOffset);
const eocdl = Zip64Eocdl.find(buffer, eocdrOffset);
const eocdr64 = eocdl && Zip64Eocdr.deserialize(buffer, eocdl.eocdrOffset);
const trailer = new ZipTrailer(eocdr, eocdr64);

this.#directory = new CentralDirectoryBufferReader(
trailer,
buffer,
trailer.offset,
);
}

/**
* Iterate through the files in the zip synchronously.
*/
public *filesSync(): IterableIterator<ZipEntryReader, void, void> {
for (const entry of this.#directory) {
const localHeaderSize = LocalFileHeader.readTotalSize(
this.#buffer,
entry.localHeaderOffset,
);

yield ZipEntryReader.fromBuffer(
entry,
this.#buffer.getOriginalBytes(
entry.localHeaderOffset + localHeaderSize,
entry.compressedSize,
),
);
}
}

/**
* Iterate through the files in the zip.
*/
// eslint-disable-next-line @typescript-eslint/require-await -- interface
public async *files(): AsyncIterableIterator<ZipEntryReader, void, void> {
yield* this.filesSync();
}

public [Symbol.iterator](): Iterator<ZipEntryReader, void, void> {
return this.filesSync();
}

public [Symbol.asyncIterator](): AsyncIterator<ZipEntryReader, void, void> {
return this.files();
}
}
Loading