zyrk 0.3.7 → 0.3.9

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.
Files changed (189) hide show
  1. package/VERSION +1 -1
  2. package/bin/zyrk.js +1 -1
  3. package/dist/cli/main.d.ts +54 -0
  4. package/dist/cli/main.d.ts.map +1 -0
  5. package/dist/cli/main.js +473 -0
  6. package/dist/cli/npm-delegate.d.ts +38 -0
  7. package/dist/cli/npm-delegate.d.ts.map +1 -0
  8. package/{src/cli/npm-delegate.ts → dist/cli/npm-delegate.js} +26 -55
  9. package/dist/cli/parse.d.ts +40 -0
  10. package/dist/cli/parse.d.ts.map +1 -0
  11. package/dist/cli/parse.js +78 -0
  12. package/dist/commands/dependencies.d.ts +12 -0
  13. package/dist/commands/dependencies.d.ts.map +1 -0
  14. package/dist/commands/dependencies.js +118 -0
  15. package/dist/commands/deploy.d.ts +15 -0
  16. package/dist/commands/deploy.d.ts.map +1 -0
  17. package/dist/commands/deploy.js +506 -0
  18. package/dist/commands/maintenance.d.ts +27 -0
  19. package/dist/commands/maintenance.d.ts.map +1 -0
  20. package/dist/commands/maintenance.js +198 -0
  21. package/dist/commands/patch.d.ts +38 -0
  22. package/dist/commands/patch.d.ts.map +1 -0
  23. package/dist/commands/patch.js +336 -0
  24. package/dist/commands/project.d.ts +15 -0
  25. package/dist/commands/project.d.ts.map +1 -0
  26. package/dist/commands/project.js +396 -0
  27. package/dist/commands/store.d.ts +10 -0
  28. package/dist/commands/store.d.ts.map +1 -0
  29. package/dist/commands/store.js +551 -0
  30. package/dist/config/paths.d.ts +37 -0
  31. package/dist/config/paths.d.ts.map +1 -0
  32. package/dist/config/paths.js +66 -0
  33. package/dist/dlx/create.d.ts +23 -0
  34. package/dist/dlx/create.d.ts.map +1 -0
  35. package/dist/dlx/create.js +59 -0
  36. package/dist/dlx/run.d.ts +54 -0
  37. package/dist/dlx/run.d.ts.map +1 -0
  38. package/dist/dlx/run.js +274 -0
  39. package/dist/errors.d.ts +68 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +111 -0
  42. package/dist/extract/tarball.d.ts +82 -0
  43. package/dist/extract/tarball.d.ts.map +1 -0
  44. package/dist/extract/tarball.js +645 -0
  45. package/dist/fetch/all.d.ts +48 -0
  46. package/dist/fetch/all.d.ts.map +1 -0
  47. package/dist/fetch/all.js +127 -0
  48. package/dist/fetch/package.d.ts +45 -0
  49. package/dist/fetch/package.d.ts.map +1 -0
  50. package/dist/fetch/package.js +124 -0
  51. package/dist/fetch/registry.d.ts +109 -0
  52. package/dist/fetch/registry.d.ts.map +1 -0
  53. package/dist/fetch/registry.js +418 -0
  54. package/dist/install/import-npm.d.ts +4 -0
  55. package/dist/install/import-npm.d.ts.map +1 -0
  56. package/dist/install/import-npm.js +147 -0
  57. package/dist/install/lock.d.ts +8 -0
  58. package/dist/install/lock.d.ts.map +1 -0
  59. package/dist/install/lock.js +146 -0
  60. package/dist/install/metadata.d.ts +19 -0
  61. package/dist/install/metadata.d.ts.map +1 -0
  62. package/dist/install/metadata.js +79 -0
  63. package/dist/install/registry.d.ts +8 -0
  64. package/dist/install/registry.d.ts.map +1 -0
  65. package/dist/install/registry.js +59 -0
  66. package/dist/install/run.d.ts +32 -0
  67. package/dist/install/run.d.ts.map +1 -0
  68. package/dist/install/run.js +545 -0
  69. package/dist/lockfile/fingerprints.d.ts +35 -0
  70. package/dist/lockfile/fingerprints.d.ts.map +1 -0
  71. package/dist/lockfile/fingerprints.js +56 -0
  72. package/dist/lockfile/read.d.ts +27 -0
  73. package/dist/lockfile/read.d.ts.map +1 -0
  74. package/dist/lockfile/read.js +126 -0
  75. package/dist/lockfile/types.d.ts +74 -0
  76. package/dist/lockfile/types.d.ts.map +1 -0
  77. package/dist/lockfile/types.js +17 -0
  78. package/dist/lockfile/write.d.ts +39 -0
  79. package/dist/lockfile/write.d.ts.map +1 -0
  80. package/dist/lockfile/write.js +180 -0
  81. package/dist/log.d.ts +21 -0
  82. package/dist/log.d.ts.map +1 -0
  83. package/dist/log.js +79 -0
  84. package/dist/project/projection.d.ts +50 -0
  85. package/dist/project/projection.d.ts.map +1 -0
  86. package/dist/project/projection.js +276 -0
  87. package/dist/resolve/resolver.d.ts +75 -0
  88. package/dist/resolve/resolver.d.ts.map +1 -0
  89. package/dist/resolve/resolver.js +533 -0
  90. package/dist/resolve/spec.d.ts +26 -0
  91. package/dist/resolve/spec.d.ts.map +1 -0
  92. package/dist/resolve/spec.js +87 -0
  93. package/dist/store/db.d.ts +49 -0
  94. package/dist/store/db.d.ts.map +1 -0
  95. package/{src/store/db.ts → dist/store/db.js} +106 -206
  96. package/dist/store/graph.d.ts +60 -0
  97. package/dist/store/graph.d.ts.map +1 -0
  98. package/dist/store/graph.js +115 -0
  99. package/dist/store/layout.d.ts +71 -0
  100. package/dist/store/layout.d.ts.map +1 -0
  101. package/dist/store/layout.js +282 -0
  102. package/dist/store/manifests.d.ts +78 -0
  103. package/dist/store/manifests.d.ts.map +1 -0
  104. package/dist/store/manifests.js +238 -0
  105. package/dist/store/objects.d.ts +56 -0
  106. package/dist/store/objects.d.ts.map +1 -0
  107. package/dist/store/objects.js +157 -0
  108. package/dist/store/realize.d.ts +38 -0
  109. package/dist/store/realize.d.ts.map +1 -0
  110. package/dist/store/realize.js +227 -0
  111. package/dist/store/source.d.ts +42 -0
  112. package/dist/store/source.d.ts.map +1 -0
  113. package/dist/store/source.js +169 -0
  114. package/dist/store/txn.d.ts +37 -0
  115. package/dist/store/txn.d.ts.map +1 -0
  116. package/dist/store/txn.js +97 -0
  117. package/dist/store/verify.d.ts +40 -0
  118. package/dist/store/verify.d.ts.map +1 -0
  119. package/dist/store/verify.js +396 -0
  120. package/dist/util/fsx.d.ts +54 -0
  121. package/dist/util/fsx.d.ts.map +1 -0
  122. package/dist/util/fsx.js +259 -0
  123. package/dist/util/hash.d.ts +48 -0
  124. package/dist/util/hash.d.ts.map +1 -0
  125. package/dist/util/hash.js +80 -0
  126. package/dist/util/ids.d.ts +19 -0
  127. package/dist/util/ids.d.ts.map +1 -0
  128. package/dist/util/ids.js +38 -0
  129. package/dist/util/integrity.d.ts +97 -0
  130. package/dist/util/integrity.d.ts.map +1 -0
  131. package/dist/util/integrity.js +192 -0
  132. package/dist/util/json.d.ts +14 -0
  133. package/dist/util/json.d.ts.map +1 -0
  134. package/dist/util/json.js +78 -0
  135. package/dist/util/locks.d.ts +40 -0
  136. package/dist/util/locks.d.ts.map +1 -0
  137. package/dist/util/locks.js +186 -0
  138. package/dist/util/proc.d.ts +32 -0
  139. package/dist/util/proc.d.ts.map +1 -0
  140. package/dist/util/proc.js +84 -0
  141. package/dist/version.d.ts +27 -0
  142. package/dist/version.d.ts.map +1 -0
  143. package/{src/version.ts → dist/version.js} +0 -8
  144. package/package.json +5 -3
  145. package/src/cli/main.ts +0 -468
  146. package/src/cli/parse.ts +0 -106
  147. package/src/commands/dependencies.ts +0 -154
  148. package/src/commands/deploy.ts +0 -777
  149. package/src/commands/maintenance.ts +0 -260
  150. package/src/commands/patch.ts +0 -408
  151. package/src/commands/project.ts +0 -598
  152. package/src/commands/store.ts +0 -689
  153. package/src/config/paths.ts +0 -119
  154. package/src/dlx/create.ts +0 -69
  155. package/src/dlx/run.ts +0 -389
  156. package/src/errors.ts +0 -142
  157. package/src/extract/tarball.ts +0 -868
  158. package/src/fetch/all.ts +0 -206
  159. package/src/fetch/package.ts +0 -181
  160. package/src/fetch/registry.ts +0 -615
  161. package/src/install/import-npm.ts +0 -177
  162. package/src/install/lock.ts +0 -176
  163. package/src/install/metadata.ts +0 -122
  164. package/src/install/registry.ts +0 -88
  165. package/src/install/run.ts +0 -760
  166. package/src/lockfile/fingerprints.ts +0 -74
  167. package/src/lockfile/read.ts +0 -198
  168. package/src/lockfile/types.ts +0 -75
  169. package/src/lockfile/write.ts +0 -257
  170. package/src/log.ts +0 -100
  171. package/src/project/projection.ts +0 -390
  172. package/src/resolve/resolver.ts +0 -757
  173. package/src/resolve/spec.ts +0 -131
  174. package/src/store/graph.ts +0 -175
  175. package/src/store/layout.ts +0 -431
  176. package/src/store/manifests.ts +0 -347
  177. package/src/store/objects.ts +0 -229
  178. package/src/store/realize.ts +0 -322
  179. package/src/store/source.ts +0 -249
  180. package/src/store/txn.ts +0 -118
  181. package/src/store/verify.ts +0 -433
  182. package/src/types/vendor.d.ts +0 -120
  183. package/src/util/fsx.ts +0 -289
  184. package/src/util/hash.ts +0 -117
  185. package/src/util/ids.ts +0 -42
  186. package/src/util/integrity.ts +0 -269
  187. package/src/util/json.ts +0 -89
  188. package/src/util/locks.ts +0 -266
  189. package/src/util/proc.ts +0 -108
@@ -0,0 +1,645 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { chmod, link as fsLink, lstat as fsLstat, mkdir as fsMkdir, open, rm, symlink as fsSymlink, } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { Parser } from "tar";
5
+ import { ZyrkError } from "../errors.js";
6
+ import { Digest, modeClassOf } from "../util/hash.js";
7
+ import { ensureDirWithMode, isErrno } from "../util/fsx.js";
8
+ export const DEFAULT_EXTRACTION_LIMITS = {
9
+ maxEntries: 200_000,
10
+ maxTotalBytes: 4 * 1024 * 1024 * 1024,
11
+ maxPathLength: 4096,
12
+ maxComponentLength: 255,
13
+ maxSymlinkTargetLength: 4096,
14
+ };
15
+ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
16
+ const WINDOWS_INVALID_CHARS = /[<>:"|?*\u0000-\u001f\u007f]/;
17
+ export async function extractTarball(options) {
18
+ const limits = {
19
+ ...DEFAULT_EXTRACTION_LIMITS,
20
+ ...options.limits,
21
+ };
22
+ await ensureDirWithMode(options.destDir, 0o700);
23
+ const destRoot = path.resolve(options.destDir);
24
+ // ---- Pass 1: validate and plan (writes nothing) ----
25
+ const plans = [];
26
+ const observations = [];
27
+ let observedReserved = 0;
28
+ let observedWindowsInvalid = 0;
29
+ await runParser(options.tarballPath, async (entry, index) => {
30
+ if (plans.length >= limits.maxEntries) {
31
+ throw unsafe(`archive has more than ${limits.maxEntries} entries`, {
32
+ maxEntries: limits.maxEntries,
33
+ });
34
+ }
35
+ const size = entry.size ?? 0;
36
+ if (size > limits.maxTotalBytes) {
37
+ throw unsafe(`archive entry declares ${size} bytes, beyond the limit of ${limits.maxTotalBytes}`, {
38
+ entryPath: entry.path,
39
+ });
40
+ }
41
+ const check = normalizeArchivePath(entry.path ?? "", limits);
42
+ if (check.reserved)
43
+ observedReserved += 1;
44
+ if (check.invalidWindows)
45
+ observedWindowsInvalid += 1;
46
+ plans.push({
47
+ index,
48
+ rawPath: check.normalized,
49
+ effectivePath: check.normalized,
50
+ type: planType(entry),
51
+ size,
52
+ mode: typeof entry.mode === "number" ? entry.mode : 0o644,
53
+ linkpath: typeof entry.linkpath === "string" && entry.linkpath !== ""
54
+ ? entry.linkpath
55
+ : null,
56
+ });
57
+ entry.resume();
58
+ });
59
+ assertNoDuplicateRawPaths(plans);
60
+ // A single wrapping root directory (npm's conventional `package/`) is
61
+ // stripped so manifests and realizations are stable regardless of the
62
+ // publisher's chosen wrapper name.
63
+ const commonRoot = findCommonRoot(plans);
64
+ const finalPlans = [];
65
+ for (const plan of plans) {
66
+ plan.effectivePath =
67
+ commonRoot === null
68
+ ? plan.rawPath
69
+ : stripPrefix(plan.rawPath, commonRoot);
70
+ if (plan.effectivePath !== "")
71
+ finalPlans.push(plan);
72
+ }
73
+ // Validate effective paths, duplicates, and byte limits.
74
+ const effectiveByPath = new Map();
75
+ const foldedSeen = new Map();
76
+ let totalBytes = 0;
77
+ for (const plan of finalPlans) {
78
+ const check = normalizeArchivePath(plan.effectivePath, limits);
79
+ if (check.normalized !== plan.effectivePath) {
80
+ throw unsafe(`entry path changes meaning when normalized: ${plan.rawPath}`, { entryPath: plan.rawPath });
81
+ }
82
+ if (effectiveByPath.has(plan.effectivePath)) {
83
+ throw unsafe(`archive contains ${JSON.stringify(plan.effectivePath)} more than once`, {
84
+ entryPath: plan.effectivePath,
85
+ });
86
+ }
87
+ effectiveByPath.set(plan.effectivePath, plan);
88
+ const folded = casefold(plan.effectivePath);
89
+ const clash = foldedSeen.get(folded);
90
+ if (clash !== undefined) {
91
+ throw unsafe(`case-folding collision between ${JSON.stringify(clash)} and ${JSON.stringify(plan.effectivePath)}`, {
92
+ pathA: clash,
93
+ pathB: plan.effectivePath,
94
+ });
95
+ }
96
+ foldedSeen.set(folded, plan.effectivePath);
97
+ if (plan.type === "file" || plan.type === "hardlink") {
98
+ totalBytes += plan.size;
99
+ if (totalBytes > limits.maxTotalBytes) {
100
+ throw unsafe(`archive expands to more than ${limits.maxTotalBytes} bytes`, {
101
+ maxTotalBytes: limits.maxTotalBytes,
102
+ });
103
+ }
104
+ }
105
+ }
106
+ // Resolve hard-link targets against the extracted tree.
107
+ for (const plan of finalPlans) {
108
+ if (plan.type !== "hardlink")
109
+ continue;
110
+ if (plan.linkpath === null) {
111
+ throw unsafe(`hard link without a target: ${plan.effectivePath}`, {
112
+ entryPath: plan.effectivePath,
113
+ });
114
+ }
115
+ const targetCheck = normalizeArchivePath(plan.linkpath, limits);
116
+ const target = commonRoot === null
117
+ ? targetCheck.normalized
118
+ : stripPrefix(targetCheck.normalized, commonRoot);
119
+ const targetPlan = target === "" ? undefined : effectiveByPath.get(target);
120
+ if (targetPlan === undefined || targetPlan.type !== "file") {
121
+ throw unsafe(`hard link target is not an extracted regular file: ${plan.effectivePath} -> ${plan.linkpath}`, {
122
+ entryPath: plan.effectivePath,
123
+ target: plan.linkpath,
124
+ });
125
+ }
126
+ plan.linkpath = target;
127
+ }
128
+ // Validate symlink targets lexically before writing anything.
129
+ for (const plan of finalPlans) {
130
+ if (plan.type !== "symlink")
131
+ continue;
132
+ const target = plan.linkpath;
133
+ if (target === null || target === "") {
134
+ throw unsafe(`symlink without a target: ${plan.effectivePath}`, {
135
+ entryPath: plan.effectivePath,
136
+ });
137
+ }
138
+ if (target.length > limits.maxSymlinkTargetLength) {
139
+ throw unsafe(`symlink target longer than ${limits.maxSymlinkTargetLength} characters: ${plan.effectivePath}`, {
140
+ entryPath: plan.effectivePath,
141
+ });
142
+ }
143
+ if (target.includes("\\")) {
144
+ throw unsafe(`symlink target contains a backslash: ${plan.effectivePath}`, { entryPath: plan.effectivePath });
145
+ }
146
+ if (target.startsWith("/") || /^[A-Za-z]:/.test(target)) {
147
+ throw unsafe(`symlink target is absolute: ${plan.effectivePath}`, {
148
+ entryPath: plan.effectivePath,
149
+ target,
150
+ });
151
+ }
152
+ assertTargetStaysInside(plan.effectivePath, target);
153
+ }
154
+ if (observedReserved > 0) {
155
+ observations.push(`${observedReserved} path(s) use names reserved on Windows; deployment to Windows requires review`);
156
+ }
157
+ if (observedWindowsInvalid > 0) {
158
+ observations.push(`${observedWindowsInvalid} path(s) contain characters invalid on Windows; deployment to Windows requires review`);
159
+ }
160
+ // ---- Pass 2: materialize ----
161
+ const planByIndex = new Map(finalPlans.map((plan) => [plan.index, plan]));
162
+ const symlinks = new Map();
163
+ const dirs = new Set();
164
+ const fileEntries = new Map();
165
+ const entries = [];
166
+ let fileCount = 0;
167
+ let dirCount = 0;
168
+ let symlinkCount = 0;
169
+ let logicalBytes = 0;
170
+ await runParser(options.tarballPath, async (entry, index) => {
171
+ const plan = planByIndex.get(index);
172
+ if (plan === undefined) {
173
+ // Entries removed by common-root stripping still must be drained.
174
+ entry.resume();
175
+ return;
176
+ }
177
+ switch (plan.type) {
178
+ case "dir": {
179
+ await ensureDirsFor(rootRelative(destRoot, plan.effectivePath), destRoot, symlinks);
180
+ dirs.add(plan.effectivePath);
181
+ dirCount += 1;
182
+ entries.push({ path: plan.effectivePath, type: "dir" });
183
+ entry.resume();
184
+ return;
185
+ }
186
+ case "file": {
187
+ await ensureDirsFor(rootRelative(destRoot, path.dirname(plan.effectivePath)), destRoot, symlinks);
188
+ const observed = await writeEntryFile(entry, destRoot, plan.effectivePath);
189
+ if (observed.size !== plan.size) {
190
+ await rm(observed.stagingPath, { force: true }).catch(() => { });
191
+ throw unsafe(`file size mismatch while extracting ${plan.effectivePath}`, {
192
+ entryPath: plan.effectivePath,
193
+ declared: plan.size,
194
+ actual: observed.size,
195
+ });
196
+ }
197
+ fileCount += 1;
198
+ logicalBytes += observed.size;
199
+ const fileEntry = {
200
+ path: plan.effectivePath,
201
+ type: "file",
202
+ mode: observed.mode,
203
+ size: observed.size,
204
+ innerDigest: observed.innerDigest,
205
+ stagingPath: observed.stagingPath,
206
+ };
207
+ fileEntries.set(plan.effectivePath, fileEntry);
208
+ entries.push(fileEntry);
209
+ return;
210
+ }
211
+ case "symlink": {
212
+ const target = plan.linkpath;
213
+ await ensureDirsFor(rootRelative(destRoot, path.dirname(plan.effectivePath)), destRoot, symlinks);
214
+ await fsSymlink(target, rootRelative(destRoot, plan.effectivePath));
215
+ symlinks.set(plan.effectivePath, target);
216
+ symlinkCount += 1;
217
+ entries.push({ path: plan.effectivePath, type: "symlink", target });
218
+ entry.resume();
219
+ return;
220
+ }
221
+ case "hardlink": {
222
+ const target = plan.linkpath;
223
+ const targetEntry = fileEntries.get(target);
224
+ if (targetEntry === undefined) {
225
+ throw unsafe(`hard link target was not extracted: ${plan.effectivePath} -> ${target}`, {
226
+ entryPath: plan.effectivePath,
227
+ target,
228
+ });
229
+ }
230
+ await ensureDirsFor(rootRelative(destRoot, path.dirname(plan.effectivePath)), destRoot, symlinks);
231
+ const dest = rootRelative(destRoot, plan.effectivePath);
232
+ try {
233
+ await fsLink(rootRelative(destRoot, target), dest);
234
+ }
235
+ catch (error) {
236
+ throw unsafe(`could not create hard link ${plan.effectivePath}`, {
237
+ entryPath: plan.effectivePath,
238
+ errno: error.code,
239
+ });
240
+ }
241
+ fileCount += 1;
242
+ logicalBytes += targetEntry.size;
243
+ const fileEntry = {
244
+ ...targetEntry,
245
+ path: plan.effectivePath,
246
+ stagingPath: dest,
247
+ };
248
+ fileEntries.set(plan.effectivePath, fileEntry);
249
+ entries.push(fileEntry);
250
+ entry.resume();
251
+ return;
252
+ }
253
+ }
254
+ });
255
+ // Post-pass: chase every symlink chain so escapes through composed links
256
+ // are detected even if each individual link looked safe.
257
+ let dangling = 0;
258
+ let cyclic = 0;
259
+ for (const [linkPath, target] of symlinks) {
260
+ const chased = chaseSymlinkChain(linkPath, target, symlinks);
261
+ if (chased.escaped) {
262
+ throw unsafe(`symlink chain escapes the package root: ${linkPath}`, {
263
+ entryPath: linkPath,
264
+ target,
265
+ });
266
+ }
267
+ if (chased.cyclic) {
268
+ cyclic += 1;
269
+ continue;
270
+ }
271
+ if (!fileEntries.has(chased.finalPath) &&
272
+ !dirs.has(chased.finalPath) &&
273
+ !symlinks.has(chased.finalPath)) {
274
+ dangling += 1;
275
+ }
276
+ }
277
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
278
+ const outcomes = new Set([
279
+ "traversal-checked",
280
+ "absolute-paths-rejected",
281
+ "case-collisions-rejected",
282
+ "modes-normalized",
283
+ "exclusive-creation",
284
+ "symlink-closure-validated",
285
+ ]);
286
+ if (symlinkCount > 0)
287
+ outcomes.add("symlinks-present");
288
+ if (finalPlans.some((plan) => plan.type === "hardlink"))
289
+ outcomes.add("hardlinks-present");
290
+ if (dangling > 0)
291
+ outcomes.add(`internal-dangling-symlinks:${dangling}`);
292
+ if (cyclic > 0)
293
+ outcomes.add(`cyclic-symlinks:${cyclic}`);
294
+ if (commonRoot !== null)
295
+ outcomes.add(`common-root-stripped:${commonRoot}`);
296
+ if (observedReserved > 0)
297
+ outcomes.add(`reserved-windows-names:${observedReserved}`);
298
+ if (observedWindowsInvalid > 0)
299
+ outcomes.add(`windows-invalid-characters:${observedWindowsInvalid}`);
300
+ return {
301
+ entries,
302
+ totals: {
303
+ files: fileCount,
304
+ dirs: dirCount,
305
+ symlinks: symlinkCount,
306
+ logicalBytes,
307
+ },
308
+ outcomes: [...outcomes].sort(),
309
+ commonRoot,
310
+ observations,
311
+ };
312
+ }
313
+ /** Normalize and validate an archive path; anything that can escape fails. */
314
+ export function normalizeArchivePath(rawInput, limits) {
315
+ const raw = rawInput.replace(/\/+$/, "");
316
+ if (raw === "") {
317
+ throw unsafe("archive contains an entry with an empty path", {
318
+ entryPath: rawInput,
319
+ });
320
+ }
321
+ if (raw.includes("\0")) {
322
+ throw unsafe("path contains a NUL byte", { entryPath: rawInput });
323
+ }
324
+ if (raw.includes("\\")) {
325
+ throw unsafe("path contains a backslash", { entryPath: rawInput });
326
+ }
327
+ if (raw.startsWith("/")) {
328
+ throw unsafe("path is absolute", { entryPath: rawInput });
329
+ }
330
+ if (/^[A-Za-z]:/.test(raw)) {
331
+ throw unsafe("path contains a drive letter", { entryPath: rawInput });
332
+ }
333
+ const segments = [];
334
+ let reserved = false;
335
+ let invalidWindows = false;
336
+ for (const segment of raw.split("/")) {
337
+ if (segment === "" || segment === ".")
338
+ continue;
339
+ if (segment === "..") {
340
+ throw unsafe("path traverses upwards with '..'", { entryPath: rawInput });
341
+ }
342
+ if (Buffer.byteLength(segment, "utf8") > limits.maxComponentLength) {
343
+ throw unsafe(`path component longer than ${limits.maxComponentLength} bytes`, { entryPath: rawInput });
344
+ }
345
+ if (WINDOWS_RESERVED.test(segment) || /[. ]$/.test(segment))
346
+ reserved = true;
347
+ if (WINDOWS_INVALID_CHARS.test(segment))
348
+ invalidWindows = true;
349
+ segments.push(segment);
350
+ }
351
+ if (segments.length === 0) {
352
+ throw unsafe("path has no usable segments", { entryPath: rawInput });
353
+ }
354
+ const normalized = segments.join("/");
355
+ if (normalized.length > limits.maxPathLength) {
356
+ throw unsafe(`path longer than ${limits.maxPathLength} characters`, {
357
+ entryPath: rawInput,
358
+ });
359
+ }
360
+ return { normalized, reserved, invalidWindows };
361
+ }
362
+ function assertTargetStaysInside(linkPath, target) {
363
+ let level = linkPath.split("/").length - 1;
364
+ for (const segment of target.split("/")) {
365
+ if (segment === "" || segment === ".")
366
+ continue;
367
+ if (segment === "..") {
368
+ level -= 1;
369
+ if (level < 0) {
370
+ throw unsafe(`link target escapes the package root: ${linkPath} -> ${target}`, {
371
+ entryPath: linkPath,
372
+ target,
373
+ });
374
+ }
375
+ continue;
376
+ }
377
+ level += 1;
378
+ }
379
+ }
380
+ function findCommonRoot(plans) {
381
+ let root = null;
382
+ for (const plan of plans) {
383
+ const first = plan.rawPath.split("/")[0];
384
+ if (first === undefined)
385
+ return null;
386
+ if (plan.rawPath === first) {
387
+ // An entry that *is* the candidate root must itself be a directory.
388
+ if (plan.type !== "dir")
389
+ return null;
390
+ }
391
+ if (root === null)
392
+ root = first;
393
+ else if (root !== first)
394
+ return null;
395
+ }
396
+ return root;
397
+ }
398
+ function stripPrefix(entryPath, prefix) {
399
+ if (entryPath === prefix)
400
+ return "";
401
+ if (entryPath.startsWith(`${prefix}/`))
402
+ return entryPath.slice(prefix.length + 1);
403
+ return entryPath;
404
+ }
405
+ function assertNoDuplicateRawPaths(plans) {
406
+ const seen = new Set();
407
+ for (const plan of plans) {
408
+ if (seen.has(plan.rawPath)) {
409
+ throw unsafe(`archive contains ${JSON.stringify(plan.rawPath)} more than once`, { entryPath: plan.rawPath });
410
+ }
411
+ seen.add(plan.rawPath);
412
+ }
413
+ }
414
+ function casefold(value) {
415
+ return value.toLowerCase();
416
+ }
417
+ // ---------------------------------------------------------------------------
418
+ // Entry planning and materialization
419
+ // ---------------------------------------------------------------------------
420
+ function planType(entry) {
421
+ switch (entry.type) {
422
+ case "File":
423
+ case "OldFile":
424
+ case "ContiguousFile":
425
+ return "file";
426
+ case "Directory":
427
+ case "GNUDumpDir":
428
+ return "dir";
429
+ case "SymbolicLink":
430
+ return "symlink";
431
+ case "Link":
432
+ return "hardlink";
433
+ default:
434
+ throw unsafe(`unsupported archive entry type ${entry.type} at ${entry.path}`, {
435
+ entryPath: entry.path,
436
+ entryType: entry.type,
437
+ });
438
+ }
439
+ }
440
+ /** Root-relative path -> absolute path, rejecting escapes lexically once more. */
441
+ function rootRelative(destRoot, relative) {
442
+ const resolved = path.resolve(destRoot, relative);
443
+ if (resolved !== destRoot && !resolved.startsWith(`${destRoot}${path.sep}`)) {
444
+ throw unsafe(`path escapes the extraction root: ${relative}`, {
445
+ entryPath: relative,
446
+ });
447
+ }
448
+ return resolved;
449
+ }
450
+ /**
451
+ * Create the directory chain for an entry. Every ancestor is checked against
452
+ * the set of symlinks created earlier in this extraction, so nothing is ever
453
+ * written through a symlink (SPEC 13.4); directory entries themselves are
454
+ * created exclusively.
455
+ */
456
+ async function ensureDirsFor(absDir, destRoot, symlinks) {
457
+ if (absDir === destRoot)
458
+ return;
459
+ const relative = path.relative(destRoot, absDir);
460
+ const segments = relative.split(path.sep);
461
+ let current = "";
462
+ for (const segment of segments) {
463
+ current = current === "" ? segment : `${current}/${segment}`;
464
+ if (symlinks.has(current)) {
465
+ throw unsafe(`refusing to write through a symlink directory: ${current}`, { entryPath: current });
466
+ }
467
+ }
468
+ try {
469
+ await fsMkdir(absDir, { recursive: true, mode: 0o755 });
470
+ }
471
+ catch (error) {
472
+ if (!isErrno(error, "EEXIST")) {
473
+ if (isErrno(error, "ENOTDIR")) {
474
+ throw unsafe(`an ancestor of ${relative} is not a directory`, {
475
+ entryPath: relative,
476
+ });
477
+ }
478
+ throw error;
479
+ }
480
+ }
481
+ const stat = await fsLstat(absDir);
482
+ if (stat.isSymbolicLink()) {
483
+ throw unsafe(`refusing to use a symlink as a directory: ${relative}`, {
484
+ entryPath: relative,
485
+ });
486
+ }
487
+ if (!stat.isDirectory()) {
488
+ throw unsafe(`an entry collides with an existing file: ${relative}`, {
489
+ entryPath: relative,
490
+ });
491
+ }
492
+ }
493
+ async function writeEntryFile(entry, destRoot, relative) {
494
+ const dest = rootRelative(destRoot, relative);
495
+ const declaredMode = typeof entry.mode === "number" ? entry.mode : 0o644;
496
+ const modeClass = modeClassOf(declaredMode);
497
+ const fileMode = modeClass === "exec" ? 0o755 : 0o644;
498
+ // 'wx' never follows an existing symlink; any existing entry is an error.
499
+ let handle;
500
+ try {
501
+ handle = await open(dest, "wx", fileMode);
502
+ }
503
+ catch (error) {
504
+ if (isErrno(error, "EEXIST")) {
505
+ throw unsafe(`refusing to overwrite an existing entry: ${relative}`, {
506
+ entryPath: relative,
507
+ });
508
+ }
509
+ throw error;
510
+ }
511
+ const digest = new Digest();
512
+ let size = 0;
513
+ try {
514
+ for await (const chunk of entry) {
515
+ const buffer = Buffer.isBuffer(chunk)
516
+ ? chunk
517
+ : Buffer.from(chunk);
518
+ size += buffer.length;
519
+ digest.update(buffer);
520
+ await handle.write(buffer);
521
+ }
522
+ await handle.sync();
523
+ }
524
+ catch (error) {
525
+ await handle.close().catch(() => { });
526
+ await rm(dest, { force: true }).catch(() => { });
527
+ throw error;
528
+ }
529
+ await handle.close();
530
+ await chmod(dest, fileMode); // archive modes are advisory; bits are normalized
531
+ return {
532
+ mode: modeClass,
533
+ size,
534
+ innerDigest: digest.hex(),
535
+ stagingPath: dest,
536
+ };
537
+ }
538
+ // ---------------------------------------------------------------------------
539
+ // Symlink chain chasing (defense in depth, bounded)
540
+ // ---------------------------------------------------------------------------
541
+ const MAX_CHASE_HOPS = 40;
542
+ export function chaseSymlinkChain(linkPath, target, symlinks) {
543
+ const out = [];
544
+ let rest = [...linkPath.split("/").slice(0, -1), ...target.split("/")];
545
+ let hops = 0;
546
+ while (rest.length > 0) {
547
+ const segment = rest.shift();
548
+ if (segment === "" || segment === ".")
549
+ continue;
550
+ if (segment === "..") {
551
+ if (out.length === 0)
552
+ return { finalPath: "", escaped: true, cyclic: false };
553
+ out.pop();
554
+ continue;
555
+ }
556
+ out.push(segment);
557
+ const candidate = out.join("/");
558
+ const hopTarget = symlinks.get(candidate);
559
+ if (hopTarget !== undefined) {
560
+ hops += 1;
561
+ if (hops > MAX_CHASE_HOPS) {
562
+ return { finalPath: candidate, escaped: false, cyclic: true };
563
+ }
564
+ out.pop();
565
+ rest = [...hopTarget.split("/"), ...rest];
566
+ }
567
+ }
568
+ return { finalPath: out.join("/"), escaped: false, cyclic: false };
569
+ }
570
+ // ---------------------------------------------------------------------------
571
+ // Parser driver
572
+ // ---------------------------------------------------------------------------
573
+ /**
574
+ * Run the tar parser over the archive, invoking `handler` for each data
575
+ * entry. Entries are processed strictly serially: the parser does not read
576
+ * the next entry until the handler has finished with (or resumed) the
577
+ * current one, which keeps memory bounded on large archives.
578
+ */
579
+ async function runParser(tarballPath, handler) {
580
+ await new Promise((resolve, reject) => {
581
+ let index = 0;
582
+ let chain = Promise.resolve();
583
+ let failed = false;
584
+ const stream = createReadStream(tarballPath);
585
+ const finish = (error) => {
586
+ if (error !== undefined) {
587
+ failed = true;
588
+ stream.destroy();
589
+ reject(error);
590
+ }
591
+ else {
592
+ resolve();
593
+ }
594
+ };
595
+ // tar v7 delivers entries through the `entry` event; each entry starts
596
+ // paused and the handler must resume/consume it, which the parser's flow
597
+ // control (and therefore our bounded memory) depends on.
598
+ const parser = new Parser({
599
+ strict: true,
600
+ maxMetaEntrySize: 1024 * 1024,
601
+ maxDecompressionRatio: 2000,
602
+ });
603
+ parser.on("entry", (entry) => {
604
+ if (entry.meta === true) {
605
+ entry.resume();
606
+ return;
607
+ }
608
+ const entryIndex = index++;
609
+ chain = chain
610
+ .then(() => {
611
+ if (failed) {
612
+ entry.resume();
613
+ return;
614
+ }
615
+ return handler(entry, entryIndex).catch((error) => {
616
+ failed = true;
617
+ parser.abort(error instanceof Error ? error : new Error(String(error)));
618
+ throw error;
619
+ });
620
+ })
621
+ // Handler failures reach the caller through the parser's error event
622
+ // (abort always emits one); swallowing here only keeps the serial
623
+ // chain from becoming an unhandled rejection after the abort.
624
+ .catch(() => { });
625
+ });
626
+ parser.on("error", (error) => {
627
+ finish(error instanceof ZyrkError
628
+ ? error
629
+ : unsafe(`archive could not be parsed: ${error.message}`, {
630
+ archiveError: error.message,
631
+ }));
632
+ });
633
+ parser.on("end", () => {
634
+ chain.then(() => finish()).catch(() => { });
635
+ });
636
+ stream.on("error", (error) => finish(error));
637
+ stream.pipe(parser);
638
+ });
639
+ }
640
+ function unsafe(message, detail) {
641
+ return new ZyrkError("ZYRK_EXTRACTION_UNSAFE", message, {
642
+ detail,
643
+ remedy: "review the package contents or report the package; zyrk will not extract archives that violate the extraction safety rules",
644
+ });
645
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Bounded-concurrency source fetch (SPEC 17.4).
3
+ *
4
+ * One slow package must not serialize the whole closure, but unbounded
5
+ * parallelism exhausts file descriptors and memory on small machines — so
6
+ * downloads stream through a fixed-size worker pool while results stay in
7
+ * deterministic key order. Every request settles before the outcome is
8
+ * decided; when several fail, the smallest key's failure is reported so
9
+ * diagnostics are reproducible and never depend on completion order.
10
+ */
11
+ import { Store } from "../store/layout.ts";
12
+ import { Transaction } from "../store/txn.ts";
13
+ import type { StoreDb } from "../store/db.ts";
14
+ import type { Logger } from "../log.ts";
15
+ import { type FetchedSource, type FetchSourceOptions } from "./package.ts";
16
+ import type { RegistryClient } from "./registry.ts";
17
+ export declare const DEFAULT_FETCH_CONCURRENCY = 8;
18
+ export type SourceFetchRequest = {
19
+ /** Deterministic identity used for ordering and error selection. */
20
+ key: string;
21
+ name: string;
22
+ version: string;
23
+ };
24
+ export type FetchSourcesOptions = {
25
+ store: Store;
26
+ registry: RegistryClient;
27
+ txn: Transaction;
28
+ requests: SourceFetchRequest[];
29
+ /** Worker count; defaults to 8, always at least 1. */
30
+ concurrency?: number;
31
+ db?: StoreDb;
32
+ logger?: Logger;
33
+ limits?: FetchSourceOptions["limits"];
34
+ allowWeakIntegrity?: boolean;
35
+ keepTarball?: boolean;
36
+ fetchOne?: (request: SourceFetchRequest, context: {
37
+ store: Store;
38
+ registry: RegistryClient;
39
+ txn: Transaction;
40
+ db?: StoreDb;
41
+ logger?: Logger;
42
+ limits?: FetchSourceOptions["limits"];
43
+ allowWeakIntegrity?: boolean;
44
+ keepTarball?: boolean;
45
+ }) => Promise<FetchedSource>;
46
+ };
47
+ export declare function fetchSources(options: FetchSourcesOptions): Promise<Map<string, FetchedSource>>;
48
+ //# sourceMappingURL=all.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"all.d.ts","sourceRoot":"","sources":["../../src/fetch/all.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,CACT,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE;QACP,KAAK,EAAE,KAAK,CAAC;QACb,QAAQ,EAAE,cAAc,CAAC;QACzB,GAAG,EAAE,WAAW,CAAC;QACjB,EAAE,CAAC,EAAE,OAAO,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,KACE,OAAO,CAAC,aAAa,CAAC,CAAC;CAC7B,CAAC;AAiCF,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CA6GrC"}