@layerzerolabs/deterministic-zip 0.2.113 → 0.2.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +4 -4
- package/dist/index.d.ts +24 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +46 -13
- package/tests/deterministicZip.test.ts +90 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
[34mCLI[39m Target: ES2023
|
|
11
11
|
[34mCLI[39m Cleaning output folder
|
|
12
12
|
[34mESM[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.js [22m[32m2.
|
|
14
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m2.74 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m7.33 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 86ms
|
package/.turbo/turbo-lint.log
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
> @layerzerolabs/deterministic-zip@0.0.0 lint /home/runner/work/monorepo-internal/monorepo-internal/tools/vm-utils/common/deterministic-zip
|
|
3
3
|
> eslint . --max-warnings 0 || (eslint . --fix --max-warnings 0 && false)
|
|
4
4
|
|
|
5
|
-
(node:
|
|
5
|
+
(node:83928) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/monorepo-internal/monorepo-internal/eslint.config.js?mtime=1783546211968 is not specified and it doesn't parse as CommonJS.
|
|
6
6
|
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
|
|
7
7
|
To eliminate this warning, add "type": "module" to /home/runner/work/monorepo-internal/monorepo-internal/package.json.
|
|
8
8
|
(Use `node --trace-warnings ...` to show where the warning was created)
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -7,11 +7,11 @@ Breaking changes might not follow SemVer, please pin Vitest's version when using
|
|
|
7
7
|
|
|
8
8
|
[1m[46m RUN [49m[22m [36mv3.2.6 [39m[90m/home/runner/work/monorepo-internal/monorepo-internal/tools/vm-utils/common/deterministic-zip[39m
|
|
9
9
|
|
|
10
|
-
[32m✓[39m tests/deterministicZip.test.ts [2m([22m[
|
|
10
|
+
[32m✓[39m tests/deterministicZip.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 30[2mms[22m[39m
|
|
11
11
|
|
|
12
12
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
-
[2m Tests [22m [1m[
|
|
13
|
+
[2m Tests [22m [1m[32m8 passed[39m[22m[90m (8)[39m
|
|
14
14
|
[2mType Errors [22m [2mno errors[22m
|
|
15
|
-
[2m Start at [22m
|
|
16
|
-
[2m Duration [22m
|
|
15
|
+
[2m Start at [22m 21:34:01
|
|
16
|
+
[2m Duration [22m 376ms[2m (transform 67ms, setup 0ms, collect 66ms, tests 30ms, environment 0ms, prepare 107ms)[22m
|
|
17
17
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
/** fflate deflate compression levels (0 = store, 9 = max). */
|
|
2
2
|
export type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
3
|
+
export declare enum ZipEntryType {
|
|
4
|
+
File = "file",
|
|
5
|
+
Directory = "directory"
|
|
6
|
+
}
|
|
7
|
+
export interface ZipEntryInfo {
|
|
8
|
+
/** POSIX-relative path from the zip root, no trailing slash. */
|
|
9
|
+
relPath: string;
|
|
10
|
+
type: ZipEntryType;
|
|
11
|
+
}
|
|
12
|
+
export type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;
|
|
3
13
|
export interface ZipDirectoryOptions {
|
|
14
|
+
/** Deflate compression level passed to fflate. Defaults to `6`. */
|
|
4
15
|
level?: DeflateLevel;
|
|
16
|
+
/** Modification time stamped onto every ZIP entry. */
|
|
5
17
|
mtime?: Date;
|
|
18
|
+
/**
|
|
19
|
+
* Called for every file and directory. Return `false` to drop the entry;
|
|
20
|
+
* `false` on a directory prunes its entire subtree (children are never
|
|
21
|
+
* visited). Omitted = include everything.
|
|
22
|
+
*
|
|
23
|
+
* Symlinks are not supported and always throw.
|
|
24
|
+
*/
|
|
25
|
+
pathInclusionPredicate?: PathInclusionPredicate;
|
|
6
26
|
}
|
|
7
27
|
/**
|
|
8
28
|
* Fixed modification time stamped onto every archive entry.
|
|
9
29
|
*
|
|
10
|
-
* fflate
|
|
11
|
-
*
|
|
12
|
-
* date from local fields, rather than a UTC instant, makes those components
|
|
13
|
-
* identical on every machine regardless of the builder's timezone.
|
|
30
|
+
* fflate writes ZIP timestamps from local date fields, so this value is built
|
|
31
|
+
* with local fields to keep the encoded timestamp stable across timezones.
|
|
14
32
|
*/
|
|
15
33
|
export declare const DEFAULT_DETERMINISTIC_ZIP_MTIME: Date;
|
|
16
34
|
/**
|
|
17
35
|
* Build a byte-deterministic ZIP of `sourceDir`.
|
|
18
36
|
*
|
|
19
|
-
* Entry order
|
|
20
|
-
*
|
|
37
|
+
* Entry order and timestamps are normalized so equivalent directories produce
|
|
38
|
+
* stable archive bytes.
|
|
21
39
|
*/
|
|
22
40
|
export declare const zipDirectoryToUint8Array: (sourceDir: string, options?: ZipDirectoryOptions) => Promise<Uint8Array>;
|
|
23
41
|
export declare const zipDirectoryToBuffer: (sourceDir: string, options?: ZipDirectoryOptions) => Promise<Buffer>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjE,MAAM,WAAW,mBAAmB;IAChC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjE,oBAAY,YAAY;IACpB,IAAI,SAAS;IACb,SAAS,cAAc;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;AAEtE,MAAM,WAAW,mBAAmB;IAChC,mEAAmE;IACnE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sDAAsD;IACtD,KAAK,CAAC,EAAE,IAAI,CAAC;IACb;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,MAAiC,CAAC;AAyC9E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACjC,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,UAAU,CAiBpB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAoE,CAAC;AAEtF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAwE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,14 @@ import path from 'path';
|
|
|
4
4
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var ZipEntryType = /* @__PURE__ */ (function(ZipEntryType2) {
|
|
8
|
+
ZipEntryType2["File"] = "file";
|
|
9
|
+
ZipEntryType2["Directory"] = "directory";
|
|
10
|
+
return ZipEntryType2;
|
|
11
|
+
})({});
|
|
7
12
|
var DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);
|
|
8
|
-
var
|
|
13
|
+
var INCLUDE_EVERYTHING = /* @__PURE__ */ __name(() => true, "INCLUDE_EVERYTHING");
|
|
14
|
+
var collectSortedEntries = /* @__PURE__ */ __name(async (root, includeEntry) => {
|
|
9
15
|
const zipEntries = [];
|
|
10
16
|
const walk = /* @__PURE__ */ __name(async (rel) => {
|
|
11
17
|
const entries = await fs.readdir(path.join(root, rel), {
|
|
@@ -14,16 +20,26 @@ var collectSortedEntries = /* @__PURE__ */ __name(async (root) => {
|
|
|
14
20
|
for (const entry of entries) {
|
|
15
21
|
const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;
|
|
16
22
|
if (entry.isDirectory()) {
|
|
23
|
+
if (!includeEntry({
|
|
24
|
+
relPath: childRel,
|
|
25
|
+
type: "directory"
|
|
26
|
+
})) continue;
|
|
17
27
|
zipEntries.push({
|
|
18
28
|
relPath: `${childRel}/`,
|
|
19
29
|
isDirectory: true
|
|
20
30
|
});
|
|
21
31
|
await walk(childRel);
|
|
22
32
|
} else if (entry.isFile()) {
|
|
33
|
+
if (!includeEntry({
|
|
34
|
+
relPath: childRel,
|
|
35
|
+
type: "file"
|
|
36
|
+
})) continue;
|
|
23
37
|
zipEntries.push({
|
|
24
38
|
relPath: childRel,
|
|
25
39
|
isDirectory: false
|
|
26
40
|
});
|
|
41
|
+
} else if (entry.isSymbolicLink()) {
|
|
42
|
+
throw new Error(`symlink entries are not supported: ${childRel}`);
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
}, "walk");
|
|
@@ -31,7 +47,7 @@ var collectSortedEntries = /* @__PURE__ */ __name(async (root) => {
|
|
|
31
47
|
return zipEntries.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
|
|
32
48
|
}, "collectSortedEntries");
|
|
33
49
|
var zipDirectoryToUint8Array = /* @__PURE__ */ __name(async (sourceDir, options) => {
|
|
34
|
-
const entries = await collectSortedEntries(sourceDir);
|
|
50
|
+
const entries = await collectSortedEntries(sourceDir, options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING);
|
|
35
51
|
const fileMap = {};
|
|
36
52
|
for (const entry of entries) {
|
|
37
53
|
fileMap[entry.relPath] = entry.isDirectory ? new Uint8Array(0) : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));
|
|
@@ -44,6 +60,6 @@ var zipDirectoryToUint8Array = /* @__PURE__ */ __name(async (sourceDir, options)
|
|
|
44
60
|
var zipDirectoryToBuffer = /* @__PURE__ */ __name(async (sourceDir, options) => Buffer.from(await zipDirectoryToUint8Array(sourceDir, options)), "zipDirectoryToBuffer");
|
|
45
61
|
var zipDirectoryToBase64 = /* @__PURE__ */ __name(async (sourceDir, options) => (await zipDirectoryToBuffer(sourceDir, options)).toString("base64"), "zipDirectoryToBase64");
|
|
46
62
|
|
|
47
|
-
export { DEFAULT_DETERMINISTIC_ZIP_MTIME, zipDirectoryToBase64, zipDirectoryToBuffer, zipDirectoryToUint8Array };
|
|
63
|
+
export { DEFAULT_DETERMINISTIC_ZIP_MTIME, ZipEntryType, zipDirectoryToBase64, zipDirectoryToBuffer, zipDirectoryToUint8Array };
|
|
48
64
|
//# sourceMappingURL=index.js.map
|
|
49
65
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_DETERMINISTIC_ZIP_MTIME","Date","collectSortedEntries","root","zipEntries","walk","rel","entries","fs","readdir","path","join","withFileTypes","entry","childRel","posix","name","isDirectory","
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["ZipEntryType","DEFAULT_DETERMINISTIC_ZIP_MTIME","Date","INCLUDE_EVERYTHING","collectSortedEntries","root","includeEntry","zipEntries","walk","rel","entries","fs","readdir","path","join","withFileTypes","entry","childRel","posix","name","isDirectory","relPath","type","push","isFile","isSymbolicLink","Error","sort","a","b","zipDirectoryToUint8Array","sourceDir","options","pathInclusionPredicate","fileMap","Uint8Array","readFile","zipSync","level","mtime","zipDirectoryToBuffer","Buffer","from","zipDirectoryToBase64","toString"],"mappings":"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","file":"index.js","sourcesContent":["import { type Zippable, zipSync } from 'fflate';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\n/** fflate deflate compression levels (0 = store, 9 = max). */\nexport type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n\nexport enum ZipEntryType {\n File = 'file',\n Directory = 'directory',\n}\n\nexport interface ZipEntryInfo {\n /** POSIX-relative path from the zip root, no trailing slash. */\n relPath: string;\n type: ZipEntryType;\n}\n\nexport type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;\n\nexport interface ZipDirectoryOptions {\n /** Deflate compression level passed to fflate. Defaults to `6`. */\n level?: DeflateLevel;\n /** Modification time stamped onto every ZIP entry. */\n mtime?: Date;\n /**\n * Called for every file and directory. Return `false` to drop the entry;\n * `false` on a directory prunes its entire subtree (children are never\n * visited). Omitted = include everything.\n *\n * Symlinks are not supported and always throw.\n */\n pathInclusionPredicate?: PathInclusionPredicate;\n}\n\n/**\n * Fixed modification time stamped onto every archive entry.\n *\n * fflate writes ZIP timestamps from local date fields, so this value is built\n * with local fields to keep the encoded timestamp stable across timezones.\n */\nexport const DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);\n\nconst INCLUDE_EVERYTHING: PathInclusionPredicate = () => true;\n\ntype DeterministicZipEntry = { relPath: string; isDirectory: boolean };\n\n/**\n * Recursively collect every included file and directory under `root`, sorted\n * by POSIX-relative path (UTF-16 code unit order).\n *\n * Filesystem `readdir` order varies by platform, so sorting is required for\n * reproducible archives.\n */\nconst collectSortedEntries = async (\n root: string,\n includeEntry: PathInclusionPredicate,\n): Promise<DeterministicZipEntry[]> => {\n const zipEntries: DeterministicZipEntry[] = [];\n\n const walk = async (rel: string): Promise<void> => {\n const entries = await fs.readdir(path.join(root, rel), { withFileTypes: true });\n for (const entry of entries) {\n const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;\n if (entry.isDirectory()) {\n if (!includeEntry({ relPath: childRel, type: ZipEntryType.Directory })) continue;\n // fflate treats trailing-slash zero-length entries as directories.\n zipEntries.push({ relPath: `${childRel}/`, isDirectory: true });\n await walk(childRel);\n } else if (entry.isFile()) {\n if (!includeEntry({ relPath: childRel, type: ZipEntryType.File })) continue;\n zipEntries.push({ relPath: childRel, isDirectory: false });\n } else if (entry.isSymbolicLink()) {\n throw new Error(`symlink entries are not supported: ${childRel}`);\n }\n }\n };\n\n await walk('');\n return zipEntries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));\n};\n\n/**\n * Build a byte-deterministic ZIP of `sourceDir`.\n *\n * Entry order and timestamps are normalized so equivalent directories produce\n * stable archive bytes.\n */\nexport const zipDirectoryToUint8Array = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Uint8Array> => {\n const entries = await collectSortedEntries(\n sourceDir,\n options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING,\n );\n const fileMap: Zippable = {};\n\n for (const entry of entries) {\n fileMap[entry.relPath] = entry.isDirectory\n ? new Uint8Array(0)\n : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));\n }\n\n return zipSync(fileMap, {\n level: options?.level ?? 6,\n mtime: options?.mtime ?? DEFAULT_DETERMINISTIC_ZIP_MTIME,\n });\n};\n\nexport const zipDirectoryToBuffer = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Buffer> => Buffer.from(await zipDirectoryToUint8Array(sourceDir, options));\n\nexport const zipDirectoryToBase64 = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<string> => (await zipDirectoryToBuffer(sourceDir, options)).toString('base64');\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layerzerolabs/deterministic-zip",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.115",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"tsup": "^8.4.0",
|
|
20
20
|
"vitest": "^3.2.6",
|
|
21
|
-
"@layerzerolabs/tsup-configuration": "0.2.
|
|
22
|
-
"@layerzerolabs/typescript-configuration": "0.2.
|
|
21
|
+
"@layerzerolabs/tsup-configuration": "0.2.115",
|
|
22
|
+
"@layerzerolabs/typescript-configuration": "0.2.115"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "restricted",
|
package/src/index.ts
CHANGED
|
@@ -5,31 +5,57 @@ import path from 'node:path';
|
|
|
5
5
|
/** fflate deflate compression levels (0 = store, 9 = max). */
|
|
6
6
|
export type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
7
7
|
|
|
8
|
+
export enum ZipEntryType {
|
|
9
|
+
File = 'file',
|
|
10
|
+
Directory = 'directory',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ZipEntryInfo {
|
|
14
|
+
/** POSIX-relative path from the zip root, no trailing slash. */
|
|
15
|
+
relPath: string;
|
|
16
|
+
type: ZipEntryType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;
|
|
20
|
+
|
|
8
21
|
export interface ZipDirectoryOptions {
|
|
22
|
+
/** Deflate compression level passed to fflate. Defaults to `6`. */
|
|
9
23
|
level?: DeflateLevel;
|
|
24
|
+
/** Modification time stamped onto every ZIP entry. */
|
|
10
25
|
mtime?: Date;
|
|
26
|
+
/**
|
|
27
|
+
* Called for every file and directory. Return `false` to drop the entry;
|
|
28
|
+
* `false` on a directory prunes its entire subtree (children are never
|
|
29
|
+
* visited). Omitted = include everything.
|
|
30
|
+
*
|
|
31
|
+
* Symlinks are not supported and always throw.
|
|
32
|
+
*/
|
|
33
|
+
pathInclusionPredicate?: PathInclusionPredicate;
|
|
11
34
|
}
|
|
12
35
|
|
|
13
36
|
/**
|
|
14
37
|
* Fixed modification time stamped onto every archive entry.
|
|
15
38
|
*
|
|
16
|
-
* fflate
|
|
17
|
-
*
|
|
18
|
-
* date from local fields, rather than a UTC instant, makes those components
|
|
19
|
-
* identical on every machine regardless of the builder's timezone.
|
|
39
|
+
* fflate writes ZIP timestamps from local date fields, so this value is built
|
|
40
|
+
* with local fields to keep the encoded timestamp stable across timezones.
|
|
20
41
|
*/
|
|
21
42
|
export const DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);
|
|
22
43
|
|
|
44
|
+
const INCLUDE_EVERYTHING: PathInclusionPredicate = () => true;
|
|
45
|
+
|
|
23
46
|
type DeterministicZipEntry = { relPath: string; isDirectory: boolean };
|
|
24
47
|
|
|
25
48
|
/**
|
|
26
|
-
* Recursively collect every file and directory under `root`, sorted
|
|
27
|
-
* POSIX-relative path (UTF-16 code unit order).
|
|
49
|
+
* Recursively collect every included file and directory under `root`, sorted
|
|
50
|
+
* by POSIX-relative path (UTF-16 code unit order).
|
|
28
51
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
52
|
+
* Filesystem `readdir` order varies by platform, so sorting is required for
|
|
53
|
+
* reproducible archives.
|
|
31
54
|
*/
|
|
32
|
-
const collectSortedEntries = async (
|
|
55
|
+
const collectSortedEntries = async (
|
|
56
|
+
root: string,
|
|
57
|
+
includeEntry: PathInclusionPredicate,
|
|
58
|
+
): Promise<DeterministicZipEntry[]> => {
|
|
33
59
|
const zipEntries: DeterministicZipEntry[] = [];
|
|
34
60
|
|
|
35
61
|
const walk = async (rel: string): Promise<void> => {
|
|
@@ -37,10 +63,15 @@ const collectSortedEntries = async (root: string): Promise<DeterministicZipEntry
|
|
|
37
63
|
for (const entry of entries) {
|
|
38
64
|
const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;
|
|
39
65
|
if (entry.isDirectory()) {
|
|
66
|
+
if (!includeEntry({ relPath: childRel, type: ZipEntryType.Directory })) continue;
|
|
67
|
+
// fflate treats trailing-slash zero-length entries as directories.
|
|
40
68
|
zipEntries.push({ relPath: `${childRel}/`, isDirectory: true });
|
|
41
69
|
await walk(childRel);
|
|
42
70
|
} else if (entry.isFile()) {
|
|
71
|
+
if (!includeEntry({ relPath: childRel, type: ZipEntryType.File })) continue;
|
|
43
72
|
zipEntries.push({ relPath: childRel, isDirectory: false });
|
|
73
|
+
} else if (entry.isSymbolicLink()) {
|
|
74
|
+
throw new Error(`symlink entries are not supported: ${childRel}`);
|
|
44
75
|
}
|
|
45
76
|
}
|
|
46
77
|
};
|
|
@@ -52,18 +83,20 @@ const collectSortedEntries = async (root: string): Promise<DeterministicZipEntry
|
|
|
52
83
|
/**
|
|
53
84
|
* Build a byte-deterministic ZIP of `sourceDir`.
|
|
54
85
|
*
|
|
55
|
-
* Entry order
|
|
56
|
-
*
|
|
86
|
+
* Entry order and timestamps are normalized so equivalent directories produce
|
|
87
|
+
* stable archive bytes.
|
|
57
88
|
*/
|
|
58
89
|
export const zipDirectoryToUint8Array = async (
|
|
59
90
|
sourceDir: string,
|
|
60
91
|
options?: ZipDirectoryOptions,
|
|
61
92
|
): Promise<Uint8Array> => {
|
|
62
|
-
const entries = await collectSortedEntries(
|
|
93
|
+
const entries = await collectSortedEntries(
|
|
94
|
+
sourceDir,
|
|
95
|
+
options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING,
|
|
96
|
+
);
|
|
63
97
|
const fileMap: Zippable = {};
|
|
64
98
|
|
|
65
99
|
for (const entry of entries) {
|
|
66
|
-
// A directory is a zero-length entry whose name already ends in `/`.
|
|
67
100
|
fileMap[entry.relPath] = entry.isDirectory
|
|
68
101
|
? new Uint8Array(0)
|
|
69
102
|
: new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));
|
|
@@ -4,7 +4,12 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
zipDirectoryToBase64,
|
|
9
|
+
zipDirectoryToUint8Array,
|
|
10
|
+
type ZipEntryInfo,
|
|
11
|
+
ZipEntryType,
|
|
12
|
+
} from '../src';
|
|
8
13
|
|
|
9
14
|
const testDirs: string[] = [];
|
|
10
15
|
|
|
@@ -56,4 +61,88 @@ describe('deterministic zip', () => {
|
|
|
56
61
|
|
|
57
62
|
expect(Object.keys(unzipped)).toEqual(['empty/']);
|
|
58
63
|
});
|
|
64
|
+
|
|
65
|
+
describe('pathInclusionPredicate', () => {
|
|
66
|
+
it('includes everything when no predicate is given', async () => {
|
|
67
|
+
const dir = await createTempDir();
|
|
68
|
+
await fs.writeFile(path.join(dir, '.env'), 'SECRET=1');
|
|
69
|
+
await fs.writeFile(path.join(dir, 'id_rsa'), 'key');
|
|
70
|
+
|
|
71
|
+
const unzipped = unzipSync(await zipDirectoryToUint8Array(dir));
|
|
72
|
+
|
|
73
|
+
expect(Object.keys(unzipped)).toEqual(['.env', 'id_rsa']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('filters files through the predicate while directories remain', async () => {
|
|
77
|
+
const dir = await createTempDir();
|
|
78
|
+
await fs.mkdir(path.join(dir, 'sources'), { recursive: true });
|
|
79
|
+
await fs.writeFile(path.join(dir, 'sources', 'module.move'), 'module a::m {}');
|
|
80
|
+
await fs.writeFile(path.join(dir, 'sources', '.env'), 'SECRET=1');
|
|
81
|
+
await fs.writeFile(path.join(dir, 'Move.toml'), '[package]\n');
|
|
82
|
+
await fs.writeFile(path.join(dir, 'notes.txt'), 'n');
|
|
83
|
+
|
|
84
|
+
const unzipped = unzipSync(
|
|
85
|
+
await zipDirectoryToUint8Array(dir, {
|
|
86
|
+
pathInclusionPredicate: ({ relPath, type }) =>
|
|
87
|
+
type === ZipEntryType.Directory ||
|
|
88
|
+
(type === ZipEntryType.File &&
|
|
89
|
+
/(^|\/)Move\.(toml|lock)$|\.move$/.test(relPath)),
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(Object.keys(unzipped)).toEqual(['Move.toml', 'sources/', 'sources/module.move']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('prunes an excluded directory subtree without visiting its children', async () => {
|
|
97
|
+
const dir = await createTempDir();
|
|
98
|
+
await fs.mkdir(path.join(dir, 'tests', 'nested'), { recursive: true });
|
|
99
|
+
await fs.writeFile(path.join(dir, 'tests', 't.move'), 'x');
|
|
100
|
+
await fs.writeFile(path.join(dir, 'tests', 'nested', 'u.move'), 'y');
|
|
101
|
+
await fs.writeFile(path.join(dir, 'keep.move'), 'k');
|
|
102
|
+
|
|
103
|
+
const seen: string[] = [];
|
|
104
|
+
const unzipped = unzipSync(
|
|
105
|
+
await zipDirectoryToUint8Array(dir, {
|
|
106
|
+
pathInclusionPredicate: (entry) => {
|
|
107
|
+
seen.push(entry.relPath);
|
|
108
|
+
return entry.relPath !== 'tests';
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(Object.keys(unzipped)).toEqual(['keep.move']);
|
|
114
|
+
// Children of the pruned directory are never offered to the predicate.
|
|
115
|
+
expect([...seen].sort()).toEqual(['keep.move', 'tests']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('passes a POSIX relPath without trailing slash and the entry type', async () => {
|
|
119
|
+
const dir = await createTempDir();
|
|
120
|
+
await fs.mkdir(path.join(dir, 'a'), { recursive: true });
|
|
121
|
+
await fs.writeFile(path.join(dir, 'a', 'b.txt'), 'b');
|
|
122
|
+
|
|
123
|
+
const seen: ZipEntryInfo[] = [];
|
|
124
|
+
await zipDirectoryToUint8Array(dir, {
|
|
125
|
+
pathInclusionPredicate: (entry) => {
|
|
126
|
+
seen.push(entry);
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(seen).toEqual([
|
|
132
|
+
{ relPath: 'a', type: ZipEntryType.Directory },
|
|
133
|
+
{ relPath: 'a/b.txt', type: ZipEntryType.File },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('symlinks', () => {
|
|
139
|
+
it('throws when a symlink is encountered', async () => {
|
|
140
|
+
const dir = await createTempDir();
|
|
141
|
+
await fs.symlink('missing-target', path.join(dir, 'link.txt'));
|
|
142
|
+
|
|
143
|
+
await expect(zipDirectoryToUint8Array(dir)).rejects.toThrow(
|
|
144
|
+
'symlink entries are not supported: link.txt',
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
59
148
|
});
|