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.
- package/VERSION +1 -1
- package/bin/zyrk.js +1 -1
- package/dist/cli/main.d.ts +54 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +473 -0
- package/dist/cli/npm-delegate.d.ts +38 -0
- package/dist/cli/npm-delegate.d.ts.map +1 -0
- package/{src/cli/npm-delegate.ts → dist/cli/npm-delegate.js} +26 -55
- package/dist/cli/parse.d.ts +40 -0
- package/dist/cli/parse.d.ts.map +1 -0
- package/dist/cli/parse.js +78 -0
- package/dist/commands/dependencies.d.ts +12 -0
- package/dist/commands/dependencies.d.ts.map +1 -0
- package/dist/commands/dependencies.js +118 -0
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +506 -0
- package/dist/commands/maintenance.d.ts +27 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +198 -0
- package/dist/commands/patch.d.ts +38 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +336 -0
- package/dist/commands/project.d.ts +15 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +396 -0
- package/dist/commands/store.d.ts +10 -0
- package/dist/commands/store.d.ts.map +1 -0
- package/dist/commands/store.js +551 -0
- package/dist/config/paths.d.ts +37 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +66 -0
- package/dist/dlx/create.d.ts +23 -0
- package/dist/dlx/create.d.ts.map +1 -0
- package/dist/dlx/create.js +59 -0
- package/dist/dlx/run.d.ts +54 -0
- package/dist/dlx/run.d.ts.map +1 -0
- package/dist/dlx/run.js +274 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/extract/tarball.d.ts +82 -0
- package/dist/extract/tarball.d.ts.map +1 -0
- package/dist/extract/tarball.js +645 -0
- package/dist/fetch/all.d.ts +48 -0
- package/dist/fetch/all.d.ts.map +1 -0
- package/dist/fetch/all.js +127 -0
- package/dist/fetch/package.d.ts +45 -0
- package/dist/fetch/package.d.ts.map +1 -0
- package/dist/fetch/package.js +124 -0
- package/dist/fetch/registry.d.ts +109 -0
- package/dist/fetch/registry.d.ts.map +1 -0
- package/dist/fetch/registry.js +418 -0
- package/dist/install/import-npm.d.ts +4 -0
- package/dist/install/import-npm.d.ts.map +1 -0
- package/dist/install/import-npm.js +147 -0
- package/dist/install/lock.d.ts +8 -0
- package/dist/install/lock.d.ts.map +1 -0
- package/dist/install/lock.js +146 -0
- package/dist/install/metadata.d.ts +19 -0
- package/dist/install/metadata.d.ts.map +1 -0
- package/dist/install/metadata.js +79 -0
- package/dist/install/registry.d.ts +8 -0
- package/dist/install/registry.d.ts.map +1 -0
- package/dist/install/registry.js +59 -0
- package/dist/install/run.d.ts +32 -0
- package/dist/install/run.d.ts.map +1 -0
- package/dist/install/run.js +545 -0
- package/dist/lockfile/fingerprints.d.ts +35 -0
- package/dist/lockfile/fingerprints.d.ts.map +1 -0
- package/dist/lockfile/fingerprints.js +56 -0
- package/dist/lockfile/read.d.ts +27 -0
- package/dist/lockfile/read.d.ts.map +1 -0
- package/dist/lockfile/read.js +126 -0
- package/dist/lockfile/types.d.ts +74 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/lockfile/types.js +17 -0
- package/dist/lockfile/write.d.ts +39 -0
- package/dist/lockfile/write.d.ts.map +1 -0
- package/dist/lockfile/write.js +180 -0
- package/dist/log.d.ts +21 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +79 -0
- package/dist/project/projection.d.ts +50 -0
- package/dist/project/projection.d.ts.map +1 -0
- package/dist/project/projection.js +276 -0
- package/dist/resolve/resolver.d.ts +75 -0
- package/dist/resolve/resolver.d.ts.map +1 -0
- package/dist/resolve/resolver.js +533 -0
- package/dist/resolve/spec.d.ts +26 -0
- package/dist/resolve/spec.d.ts.map +1 -0
- package/dist/resolve/spec.js +87 -0
- package/dist/store/db.d.ts +49 -0
- package/dist/store/db.d.ts.map +1 -0
- package/{src/store/db.ts → dist/store/db.js} +106 -206
- package/dist/store/graph.d.ts +60 -0
- package/dist/store/graph.d.ts.map +1 -0
- package/dist/store/graph.js +115 -0
- package/dist/store/layout.d.ts +71 -0
- package/dist/store/layout.d.ts.map +1 -0
- package/dist/store/layout.js +282 -0
- package/dist/store/manifests.d.ts +78 -0
- package/dist/store/manifests.d.ts.map +1 -0
- package/dist/store/manifests.js +238 -0
- package/dist/store/objects.d.ts +56 -0
- package/dist/store/objects.d.ts.map +1 -0
- package/dist/store/objects.js +157 -0
- package/dist/store/realize.d.ts +38 -0
- package/dist/store/realize.d.ts.map +1 -0
- package/dist/store/realize.js +227 -0
- package/dist/store/source.d.ts +42 -0
- package/dist/store/source.d.ts.map +1 -0
- package/dist/store/source.js +169 -0
- package/dist/store/txn.d.ts +37 -0
- package/dist/store/txn.d.ts.map +1 -0
- package/dist/store/txn.js +97 -0
- package/dist/store/verify.d.ts +40 -0
- package/dist/store/verify.d.ts.map +1 -0
- package/dist/store/verify.js +396 -0
- package/dist/util/fsx.d.ts +54 -0
- package/dist/util/fsx.d.ts.map +1 -0
- package/dist/util/fsx.js +259 -0
- package/dist/util/hash.d.ts +48 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +80 -0
- package/dist/util/ids.d.ts +19 -0
- package/dist/util/ids.d.ts.map +1 -0
- package/dist/util/ids.js +38 -0
- package/dist/util/integrity.d.ts +97 -0
- package/dist/util/integrity.d.ts.map +1 -0
- package/dist/util/integrity.js +192 -0
- package/dist/util/json.d.ts +14 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +78 -0
- package/dist/util/locks.d.ts +40 -0
- package/dist/util/locks.d.ts.map +1 -0
- package/dist/util/locks.js +186 -0
- package/dist/util/proc.d.ts +32 -0
- package/dist/util/proc.d.ts.map +1 -0
- package/dist/util/proc.js +84 -0
- package/dist/version.d.ts +27 -0
- package/dist/version.d.ts.map +1 -0
- package/{src/version.ts → dist/version.js} +0 -8
- package/package.json +5 -3
- package/src/cli/main.ts +0 -468
- package/src/cli/parse.ts +0 -106
- package/src/commands/dependencies.ts +0 -154
- package/src/commands/deploy.ts +0 -777
- package/src/commands/maintenance.ts +0 -260
- package/src/commands/patch.ts +0 -408
- package/src/commands/project.ts +0 -598
- package/src/commands/store.ts +0 -689
- package/src/config/paths.ts +0 -119
- package/src/dlx/create.ts +0 -69
- package/src/dlx/run.ts +0 -389
- package/src/errors.ts +0 -142
- package/src/extract/tarball.ts +0 -868
- package/src/fetch/all.ts +0 -206
- package/src/fetch/package.ts +0 -181
- package/src/fetch/registry.ts +0 -615
- package/src/install/import-npm.ts +0 -177
- package/src/install/lock.ts +0 -176
- package/src/install/metadata.ts +0 -122
- package/src/install/registry.ts +0 -88
- package/src/install/run.ts +0 -760
- package/src/lockfile/fingerprints.ts +0 -74
- package/src/lockfile/read.ts +0 -198
- package/src/lockfile/types.ts +0 -75
- package/src/lockfile/write.ts +0 -257
- package/src/log.ts +0 -100
- package/src/project/projection.ts +0 -390
- package/src/resolve/resolver.ts +0 -757
- package/src/resolve/spec.ts +0 -131
- package/src/store/graph.ts +0 -175
- package/src/store/layout.ts +0 -431
- package/src/store/manifests.ts +0 -347
- package/src/store/objects.ts +0 -229
- package/src/store/realize.ts +0 -322
- package/src/store/source.ts +0 -249
- package/src/store/txn.ts +0 -118
- package/src/store/verify.ts +0 -433
- package/src/types/vendor.d.ts +0 -120
- package/src/util/fsx.ts +0 -289
- package/src/util/hash.ts +0 -117
- package/src/util/ids.ts +0 -42
- package/src/util/integrity.ts +0 -269
- package/src/util/json.ts +0 -89
- package/src/util/locks.ts +0 -266
- 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"}
|