@fulmenhq/tsfulmen 0.3.2 → 0.4.0
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/CHANGELOG.md +65 -1
- package/README.md +30 -1
- package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
- package/dist/appidentity/index.d.ts +15 -5
- package/dist/appidentity/index.js +9 -2204
- package/dist/appidentity/index.js.map +1 -1
- package/dist/assets/index.d.ts +179 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/bin/prometheus-cli.js +15 -9134
- package/dist/bin/prometheus-cli.js.map +1 -1
- package/dist/bin/schema-cli.js +4 -6098
- package/dist/bin/schema-cli.js.map +1 -1
- package/dist/bin/signals-cli.js +15 -1910
- package/dist/bin/signals-cli.js.map +1 -1
- package/dist/chunk-3HISR2XI.js +36 -0
- package/dist/chunk-3HISR2XI.js.map +1 -0
- package/dist/chunk-5HZEJGZ5.js +725 -0
- package/dist/chunk-5HZEJGZ5.js.map +1 -0
- package/dist/chunk-7SVYOPJ3.js +9412 -0
- package/dist/chunk-7SVYOPJ3.js.map +1 -0
- package/dist/chunk-BCO3FUUO.js +682 -0
- package/dist/chunk-BCO3FUUO.js.map +1 -0
- package/dist/chunk-BJDL6JEX.js +208 -0
- package/dist/chunk-BJDL6JEX.js.map +1 -0
- package/dist/chunk-BTUS6PZZ.js +302 -0
- package/dist/chunk-BTUS6PZZ.js.map +1 -0
- package/dist/chunk-CW4CSNVF.js +410 -0
- package/dist/chunk-CW4CSNVF.js.map +1 -0
- package/dist/chunk-HIBZ2AY7.js +194 -0
- package/dist/chunk-HIBZ2AY7.js.map +1 -0
- package/dist/chunk-HS4F3BRR.js +985 -0
- package/dist/chunk-HS4F3BRR.js.map +1 -0
- package/dist/chunk-HY6JQOFN.js +32 -0
- package/dist/chunk-HY6JQOFN.js.map +1 -0
- package/dist/chunk-K2FYT43R.js +837 -0
- package/dist/chunk-K2FYT43R.js.map +1 -0
- package/dist/chunk-K7BMFGQP.js +43 -0
- package/dist/chunk-K7BMFGQP.js.map +1 -0
- package/dist/chunk-LWAH2UG7.js +103 -0
- package/dist/chunk-LWAH2UG7.js.map +1 -0
- package/dist/chunk-OHE2AOH4.js +273 -0
- package/dist/chunk-OHE2AOH4.js.map +1 -0
- package/dist/chunk-P3DNOZOG.js +1488 -0
- package/dist/chunk-P3DNOZOG.js.map +1 -0
- package/dist/chunk-PQ6TKPUN.js +27 -0
- package/dist/chunk-PQ6TKPUN.js.map +1 -0
- package/dist/chunk-QCCN6POA.js +211 -0
- package/dist/chunk-QCCN6POA.js.map +1 -0
- package/dist/chunk-RSLFR4TO.js +631 -0
- package/dist/chunk-RSLFR4TO.js.map +1 -0
- package/dist/chunk-UP2VWCW5.js +33 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/chunk-W6TQFTTY.js +130 -0
- package/dist/chunk-W6TQFTTY.js.map +1 -0
- package/dist/chunk-WSMVLWLB.js +269 -0
- package/dist/chunk-WSMVLWLB.js.map +1 -0
- package/dist/config/index.d.ts +69 -11
- package/dist/config/index.js +24 -1227
- package/dist/config/index.js.map +1 -1
- package/dist/crucible/fulpack/index.js +2 -25
- package/dist/crucible/fulpack/index.js.map +1 -1
- package/dist/crucible/index.js +13 -1129
- package/dist/crucible/index.js.map +1 -1
- package/dist/docscribe/index.js +2 -680
- package/dist/docscribe/index.js.map +1 -1
- package/dist/errors/index.js +5 -1831
- package/dist/errors/index.js.map +1 -1
- package/dist/errors-D7KXV3QW.js +4 -0
- package/dist/errors-D7KXV3QW.js.map +1 -0
- package/dist/errors-RBJX6ETJ.js +8 -0
- package/dist/errors-RBJX6ETJ.js.map +1 -0
- package/dist/export-BWRETGYS.js +6 -0
- package/dist/export-BWRETGYS.js.map +1 -0
- package/dist/foundry/index.d.ts +1 -1
- package/dist/foundry/index.js +340 -3949
- package/dist/foundry/index.js.map +1 -1
- package/dist/foundry/similarity/index.js +3 -134
- package/dist/foundry/similarity/index.js.map +1 -1
- package/dist/fulencode/index.js +3 -319
- package/dist/fulencode/index.js.map +1 -1
- package/dist/fulhash/index.js +3 -611
- package/dist/fulhash/index.js.map +1 -1
- package/dist/fulpack/index.js +3 -1515
- package/dist/fulpack/index.js.map +1 -1
- package/dist/hash-YOJYVOWH.js +4 -0
- package/dist/hash-YOJYVOWH.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -4390
- package/dist/index.js.map +1 -1
- package/dist/loader-H5K7HSO5.js +9 -0
- package/dist/loader-H5K7HSO5.js.map +1 -0
- package/dist/logging/index.js +2 -723
- package/dist/logging/index.js.map +1 -1
- package/dist/pathfinder/index.js +18 -2341
- package/dist/pathfinder/index.js.map +1 -1
- package/dist/prom-client-PNEBGD3G.js +4801 -0
- package/dist/prom-client-PNEBGD3G.js.map +1 -0
- package/dist/reports/license-inventory.csv +1 -1
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema/index.js +4 -6148
- package/dist/schema/index.js.map +1 -1
- package/dist/signals/index.d.ts +1 -1
- package/dist/signals/index.js +8 -2767
- package/dist/signals/index.js.map +1 -1
- package/dist/similarity/index.js +3 -134
- package/dist/similarity/index.js.map +1 -1
- package/dist/telemetry/http/index.js +9 -930
- package/dist/telemetry/http/index.js.map +1 -1
- package/dist/telemetry/index.js +4 -1561
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/prometheus/index.js +7 -8417
- package/dist/telemetry/prometheus/index.js.map +1 -1
- package/dist/{types-Dv5TERCM.d.ts → types-CHDvDRCf.d.ts} +17 -1
- package/dist/validator-U4CKD2Y3.js +6 -0
- package/dist/validator-U4CKD2Y3.js.map +1 -0
- package/package.json +5 -1
- package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
- package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/data-sensitivity-classification.md +1 -1
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
import { __require } from './chunk-UP2VWCW5.js';
|
|
2
|
+
import { existsSync, mkdirSync, statSync, createReadStream, createWriteStream, readdirSync, lstatSync } from 'fs';
|
|
3
|
+
import { dirname, basename, join } from 'path';
|
|
4
|
+
import { pipeline } from 'stream';
|
|
5
|
+
import { createGunzip, createGzip } from 'zlib';
|
|
6
|
+
import { TarArchive, ZipArchive } from 'archiver';
|
|
7
|
+
|
|
8
|
+
// src/fulpack/errors.ts
|
|
9
|
+
function createFulpackError(code, message, operation, context) {
|
|
10
|
+
return {
|
|
11
|
+
code,
|
|
12
|
+
message,
|
|
13
|
+
operation,
|
|
14
|
+
...context?.path && { path: context.path },
|
|
15
|
+
...context?.archive && { archive: context.archive },
|
|
16
|
+
...context?.details && { details: context.details }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
var ERROR_CODES = {
|
|
20
|
+
// Validation Errors
|
|
21
|
+
INVALID_ARCHIVE_FORMAT: "INVALID_ARCHIVE_FORMAT",
|
|
22
|
+
INVALID_PATH: "INVALID_PATH",
|
|
23
|
+
INVALID_OPTIONS: "INVALID_OPTIONS",
|
|
24
|
+
// Security Errors
|
|
25
|
+
PATH_TRAVERSAL: "PATH_TRAVERSAL",
|
|
26
|
+
ABSOLUTE_PATH: "ABSOLUTE_PATH",
|
|
27
|
+
SYMLINK_ESCAPE: "SYMLINK_ESCAPE",
|
|
28
|
+
DECOMPRESSION_BOMB: "DECOMPRESSION_BOMB",
|
|
29
|
+
CHECKSUM_MISMATCH: "CHECKSUM_MISMATCH",
|
|
30
|
+
// Runtime Errors
|
|
31
|
+
ARCHIVE_NOT_FOUND: "ARCHIVE_NOT_FOUND",
|
|
32
|
+
ARCHIVE_CORRUPT: "ARCHIVE_CORRUPT",
|
|
33
|
+
EXTRACTION_FAILED: "EXTRACTION_FAILED",
|
|
34
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
35
|
+
DISK_FULL: "DISK_FULL",
|
|
36
|
+
SOURCE_NOT_FOUND: "SOURCE_NOT_FOUND"
|
|
37
|
+
};
|
|
38
|
+
var FulpackOperationError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
operation;
|
|
41
|
+
path;
|
|
42
|
+
archive;
|
|
43
|
+
details;
|
|
44
|
+
constructor(error) {
|
|
45
|
+
super(error.message);
|
|
46
|
+
this.name = "FulpackOperationError";
|
|
47
|
+
this.code = error.code;
|
|
48
|
+
this.operation = error.operation;
|
|
49
|
+
this.path = error.path;
|
|
50
|
+
this.archive = error.archive;
|
|
51
|
+
this.details = error.details;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert back to canonical error format
|
|
55
|
+
*/
|
|
56
|
+
toCanonical() {
|
|
57
|
+
return {
|
|
58
|
+
code: this.code,
|
|
59
|
+
message: this.message,
|
|
60
|
+
operation: this.operation,
|
|
61
|
+
...this.path && { path: this.path },
|
|
62
|
+
...this.archive && { archive: this.archive },
|
|
63
|
+
...this.details && { details: this.details }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function hasPathTraversal(path) {
|
|
68
|
+
return path.includes("..") || path.includes("\\..");
|
|
69
|
+
}
|
|
70
|
+
function isAbsolutePath(path) {
|
|
71
|
+
return path.startsWith("/") || /^[a-zA-Z]:/.test(path);
|
|
72
|
+
}
|
|
73
|
+
function validatePath(path, operation, allowAbsolute = false) {
|
|
74
|
+
if (!allowAbsolute && isAbsolutePath(path)) {
|
|
75
|
+
return createFulpackError(
|
|
76
|
+
ERROR_CODES.ABSOLUTE_PATH,
|
|
77
|
+
`Absolute path not allowed: ${path}`,
|
|
78
|
+
operation,
|
|
79
|
+
{ path }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (hasPathTraversal(path)) {
|
|
83
|
+
return createFulpackError(
|
|
84
|
+
ERROR_CODES.PATH_TRAVERSAL,
|
|
85
|
+
`Path traversal detected: ${path}`,
|
|
86
|
+
operation,
|
|
87
|
+
{ path }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function checkDecompressionBomb(uncompressedSize, compressedSize, maxSize, maxEntries, currentEntries) {
|
|
93
|
+
if (uncompressedSize > maxSize) {
|
|
94
|
+
return createFulpackError(
|
|
95
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
96
|
+
`Archive exceeds maximum size limit`,
|
|
97
|
+
"extract" /* EXTRACT */,
|
|
98
|
+
{
|
|
99
|
+
details: {
|
|
100
|
+
actual_size: uncompressedSize,
|
|
101
|
+
max_size: maxSize
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (currentEntries > maxEntries) {
|
|
107
|
+
return createFulpackError(
|
|
108
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
109
|
+
`Archive exceeds maximum entry limit`,
|
|
110
|
+
"extract" /* EXTRACT */,
|
|
111
|
+
{
|
|
112
|
+
details: {
|
|
113
|
+
actual_size: currentEntries,
|
|
114
|
+
max_size: maxEntries
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (compressedSize > 0 && uncompressedSize / compressedSize > 100) {
|
|
120
|
+
return createFulpackError(
|
|
121
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
122
|
+
`Suspicious compression ratio detected`,
|
|
123
|
+
"extract" /* EXTRACT */,
|
|
124
|
+
{
|
|
125
|
+
details: {
|
|
126
|
+
compression_ratio: uncompressedSize / compressedSize
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/fulpack/core.ts
|
|
135
|
+
var DEFAULTS = {
|
|
136
|
+
CREATE: {
|
|
137
|
+
compression_level: 6,
|
|
138
|
+
checksum_algorithm: "sha256",
|
|
139
|
+
preserve_permissions: true,
|
|
140
|
+
follow_symlinks: false
|
|
141
|
+
},
|
|
142
|
+
EXTRACT: {
|
|
143
|
+
overwrite: "error",
|
|
144
|
+
verify_checksums: true,
|
|
145
|
+
preserve_permissions: true,
|
|
146
|
+
max_size: 1024 * 1024 * 1024,
|
|
147
|
+
// 1GB
|
|
148
|
+
max_entries: 1e5
|
|
149
|
+
},
|
|
150
|
+
SCAN: {
|
|
151
|
+
include_metadata: true,
|
|
152
|
+
max_entries: 1e5
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
async function create(source, output, format, options) {
|
|
156
|
+
const opts = { ...DEFAULTS.CREATE, ...options };
|
|
157
|
+
const sources = Array.isArray(source) ? source : [source];
|
|
158
|
+
if (format !== "tar" /* TAR */ && format !== "tar.gz" /* TAR_GZ */ && format !== "zip" /* ZIP */ && format !== "gzip" /* GZIP */) {
|
|
159
|
+
throw new FulpackOperationError(
|
|
160
|
+
createFulpackError(
|
|
161
|
+
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
162
|
+
`Format ${format} not yet supported`,
|
|
163
|
+
"create" /* CREATE */
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const outputDir = dirname(output);
|
|
168
|
+
if (!existsSync(outputDir)) {
|
|
169
|
+
mkdirSync(outputDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
if (format === "zip" /* ZIP */) {
|
|
172
|
+
return await createZipArchive(sources, output, opts);
|
|
173
|
+
} else if (format === "tar.gz" /* TAR_GZ */) {
|
|
174
|
+
return await createTarGzArchive(sources, output, opts);
|
|
175
|
+
} else if (format === "tar" /* TAR */) {
|
|
176
|
+
return await createTarArchive(sources, output, opts);
|
|
177
|
+
} else if (format === "gzip" /* GZIP */) {
|
|
178
|
+
return await createGzipFile(sources, output, opts);
|
|
179
|
+
} else {
|
|
180
|
+
throw new FulpackOperationError(
|
|
181
|
+
createFulpackError(
|
|
182
|
+
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
183
|
+
`Unsupported format for creation: ${format}`,
|
|
184
|
+
"create" /* CREATE */
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function extract(archive, destination, options) {
|
|
190
|
+
const opts = { ...DEFAULTS.EXTRACT, ...options };
|
|
191
|
+
if (!existsSync(archive)) {
|
|
192
|
+
throw new FulpackOperationError(
|
|
193
|
+
createFulpackError(
|
|
194
|
+
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
195
|
+
`Archive not found: ${archive}`,
|
|
196
|
+
"extract" /* EXTRACT */,
|
|
197
|
+
{ archive }
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!existsSync(destination)) {
|
|
202
|
+
mkdirSync(destination, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
if (archive.endsWith(".tar.gz") || archive.endsWith(".tgz")) {
|
|
205
|
+
return await extractTarGz(archive, destination, opts);
|
|
206
|
+
} else if (archive.endsWith(".tar")) {
|
|
207
|
+
return await extractTar(archive, destination, opts);
|
|
208
|
+
} else if (archive.endsWith(".zip")) {
|
|
209
|
+
return await extractZip(archive, destination, opts);
|
|
210
|
+
} else if (archive.endsWith(".gz")) {
|
|
211
|
+
return await extractGzipFile(archive, destination, opts);
|
|
212
|
+
} else {
|
|
213
|
+
throw new FulpackOperationError(
|
|
214
|
+
createFulpackError(
|
|
215
|
+
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
216
|
+
`Unsupported archive format for extraction: ${archive}`,
|
|
217
|
+
"extract" /* EXTRACT */,
|
|
218
|
+
{ archive }
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function scan(archive, options) {
|
|
224
|
+
const opts = { ...DEFAULTS.SCAN, ...options };
|
|
225
|
+
if (!existsSync(archive)) {
|
|
226
|
+
throw new FulpackOperationError(
|
|
227
|
+
createFulpackError(
|
|
228
|
+
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
229
|
+
`Archive not found: ${archive}`,
|
|
230
|
+
"scan" /* SCAN */,
|
|
231
|
+
{ archive }
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const filename = basename(archive);
|
|
236
|
+
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
237
|
+
return await scanTarGz(archive, opts);
|
|
238
|
+
} else if (filename.endsWith(".tar")) {
|
|
239
|
+
return await scanTar(archive, opts);
|
|
240
|
+
} else if (filename.endsWith(".zip")) {
|
|
241
|
+
return await scanZip(archive, opts);
|
|
242
|
+
} else if (filename.endsWith(".gz")) {
|
|
243
|
+
return await scanGzipFile(archive, opts);
|
|
244
|
+
} else {
|
|
245
|
+
throw new FulpackOperationError(
|
|
246
|
+
createFulpackError(
|
|
247
|
+
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
248
|
+
`Unsupported archive format: ${filename}`,
|
|
249
|
+
"scan" /* SCAN */,
|
|
250
|
+
{ archive }
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function verify(archive, _options) {
|
|
256
|
+
if (!existsSync(archive)) {
|
|
257
|
+
throw new FulpackOperationError(
|
|
258
|
+
createFulpackError(
|
|
259
|
+
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
260
|
+
`Archive not found: ${archive}`,
|
|
261
|
+
"verify" /* VERIFY */,
|
|
262
|
+
{ archive }
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const stats = statSync(archive);
|
|
267
|
+
const errors = [];
|
|
268
|
+
const warnings = [];
|
|
269
|
+
const checks_performed = [];
|
|
270
|
+
let entry_count = 0;
|
|
271
|
+
let checksums_verified = 0;
|
|
272
|
+
checks_performed.push("structure_valid");
|
|
273
|
+
if (stats.size === 0) {
|
|
274
|
+
errors.push(
|
|
275
|
+
createFulpackError(ERROR_CODES.ARCHIVE_CORRUPT, "Archive file is empty", "verify" /* VERIFY */, {
|
|
276
|
+
archive
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
return {
|
|
280
|
+
valid: false,
|
|
281
|
+
errors,
|
|
282
|
+
warnings,
|
|
283
|
+
entry_count: 0,
|
|
284
|
+
checksums_verified: 0,
|
|
285
|
+
checks_performed
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
let entries;
|
|
289
|
+
try {
|
|
290
|
+
entries = await scan(archive, { include_metadata: true });
|
|
291
|
+
entry_count = entries.length;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
errors.push(
|
|
294
|
+
createFulpackError(
|
|
295
|
+
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
296
|
+
`Failed to scan archive: ${error instanceof Error ? error.message : String(error)}`,
|
|
297
|
+
"verify" /* VERIFY */,
|
|
298
|
+
{ archive, details: { original_error: error } }
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
return {
|
|
302
|
+
valid: false,
|
|
303
|
+
errors,
|
|
304
|
+
warnings,
|
|
305
|
+
entry_count: 0,
|
|
306
|
+
checksums_verified: 0,
|
|
307
|
+
checks_performed
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
checks_performed.push("no_path_traversal");
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
const pathError = validatePath(entry.path, "verify" /* VERIFY */, true);
|
|
313
|
+
if (pathError) {
|
|
314
|
+
errors.push({ ...pathError, archive });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const symlinks = entries.filter((e) => e.type === "symlink");
|
|
318
|
+
if (symlinks.length > 0) {
|
|
319
|
+
checks_performed.push("symlinks_safe");
|
|
320
|
+
for (const symlink of symlinks) {
|
|
321
|
+
if (symlink.symlink_target) {
|
|
322
|
+
const targetError = validatePath(symlink.symlink_target, "verify" /* VERIFY */, true);
|
|
323
|
+
if (targetError) {
|
|
324
|
+
errors.push(
|
|
325
|
+
createFulpackError(
|
|
326
|
+
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
327
|
+
`Symlink '${symlink.path}' targets unsafe location: ${symlink.symlink_target}`,
|
|
328
|
+
"verify" /* VERIFY */,
|
|
329
|
+
{ archive, path: symlink.path, details: { symlink_target: symlink.symlink_target } }
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
checks_performed.push("no_decompression_bomb");
|
|
337
|
+
const total_size = entries.reduce((sum, entry) => sum + (entry.size || 0), 0);
|
|
338
|
+
const compressed_size = stats.size;
|
|
339
|
+
const compression_ratio = total_size > 0 && compressed_size > 0 ? total_size / compressed_size : 1;
|
|
340
|
+
if (compression_ratio > 100) {
|
|
341
|
+
warnings.push(
|
|
342
|
+
`High compression ratio detected: ${compression_ratio.toFixed(1)}:1 (threshold: 100:1)`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const MAX_SIZE = 1024 * 1024 * 1024;
|
|
346
|
+
if (total_size > MAX_SIZE) {
|
|
347
|
+
warnings.push(
|
|
348
|
+
`Total uncompressed size (${(total_size / (1024 * 1024)).toFixed(1)}MB) exceeds recommended limit (1GB)`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const MAX_ENTRIES = 1e5;
|
|
352
|
+
if (entry_count > MAX_ENTRIES) {
|
|
353
|
+
warnings.push(`Entry count (${entry_count}) exceeds recommended limit (${MAX_ENTRIES})`);
|
|
354
|
+
}
|
|
355
|
+
const entriesWithChecksums = entries.filter((e) => e.checksum !== void 0);
|
|
356
|
+
if (entriesWithChecksums.length > 0) {
|
|
357
|
+
checks_performed.push("checksums_verified");
|
|
358
|
+
checksums_verified = entriesWithChecksums.length;
|
|
359
|
+
} else if (entry_count > 0) {
|
|
360
|
+
warnings.push("Archive does not contain checksums for integrity verification");
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
valid: errors.length === 0,
|
|
364
|
+
errors,
|
|
365
|
+
warnings,
|
|
366
|
+
entry_count,
|
|
367
|
+
checksums_verified,
|
|
368
|
+
checks_performed
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function info(archive) {
|
|
372
|
+
if (!existsSync(archive)) {
|
|
373
|
+
throw new FulpackOperationError(
|
|
374
|
+
createFulpackError(
|
|
375
|
+
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
376
|
+
`Archive not found: ${archive}`,
|
|
377
|
+
"info" /* INFO */,
|
|
378
|
+
{ archive }
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const stats = statSync(archive);
|
|
383
|
+
const filename = basename(archive);
|
|
384
|
+
let format;
|
|
385
|
+
let compression = "none";
|
|
386
|
+
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
387
|
+
format = "tar.gz" /* TAR_GZ */;
|
|
388
|
+
compression = "gzip";
|
|
389
|
+
} else if (filename.endsWith(".zip")) {
|
|
390
|
+
format = "zip" /* ZIP */;
|
|
391
|
+
compression = "deflate";
|
|
392
|
+
} else if (filename.endsWith(".gz")) {
|
|
393
|
+
format = "gzip" /* GZIP */;
|
|
394
|
+
compression = "gzip";
|
|
395
|
+
} else if (filename.endsWith(".tar")) {
|
|
396
|
+
format = "tar" /* TAR */;
|
|
397
|
+
compression = "none";
|
|
398
|
+
} else {
|
|
399
|
+
throw new FulpackOperationError(
|
|
400
|
+
createFulpackError(
|
|
401
|
+
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
402
|
+
`Unsupported archive format: ${filename}`,
|
|
403
|
+
"info" /* INFO */,
|
|
404
|
+
{ archive }
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const entries = await scan(archive, { include_metadata: true });
|
|
409
|
+
const entry_count = entries.length;
|
|
410
|
+
const total_size = entries.reduce((sum, entry) => sum + (entry.size || 0), 0);
|
|
411
|
+
const compressed_size = stats.size;
|
|
412
|
+
const compression_ratio = total_size > 0 ? compressed_size / total_size : 1;
|
|
413
|
+
const has_checksums = entries.some((entry) => entry.checksum !== void 0);
|
|
414
|
+
return {
|
|
415
|
+
format,
|
|
416
|
+
compression,
|
|
417
|
+
entry_count,
|
|
418
|
+
total_size,
|
|
419
|
+
compressed_size,
|
|
420
|
+
compression_ratio,
|
|
421
|
+
has_checksums,
|
|
422
|
+
created: stats.mtime.toISOString()
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function extractGzipFile(archive, destination, options) {
|
|
426
|
+
const filename = basename(archive, ".gz");
|
|
427
|
+
const outputPath = join(destination, filename);
|
|
428
|
+
if (existsSync(outputPath)) {
|
|
429
|
+
if (options.overwrite === "error") {
|
|
430
|
+
throw new FulpackOperationError(
|
|
431
|
+
createFulpackError(
|
|
432
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
433
|
+
`Output file already exists: ${outputPath}`,
|
|
434
|
+
"extract" /* EXTRACT */,
|
|
435
|
+
{ path: outputPath, archive }
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
} else if (options.overwrite === "skip") {
|
|
439
|
+
return {
|
|
440
|
+
extracted_count: 0,
|
|
441
|
+
skipped_count: 1,
|
|
442
|
+
error_count: 0,
|
|
443
|
+
errors: [],
|
|
444
|
+
warnings: [`Skipped existing file: ${outputPath}`]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
const readStream = createReadStream(archive);
|
|
450
|
+
const writeStream = createWriteStream(outputPath);
|
|
451
|
+
const gunzip = createGunzip();
|
|
452
|
+
await new Promise((resolve, reject) => {
|
|
453
|
+
pipeline(readStream, gunzip, writeStream, (error) => {
|
|
454
|
+
if (error) {
|
|
455
|
+
reject(error);
|
|
456
|
+
} else {
|
|
457
|
+
resolve(void 0);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
extracted_count: 1,
|
|
463
|
+
skipped_count: 0,
|
|
464
|
+
error_count: 0,
|
|
465
|
+
errors: [],
|
|
466
|
+
warnings: []
|
|
467
|
+
};
|
|
468
|
+
} catch (error) {
|
|
469
|
+
throw new FulpackOperationError(
|
|
470
|
+
createFulpackError(
|
|
471
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
472
|
+
`Failed to extract gzip file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
473
|
+
"extract" /* EXTRACT */,
|
|
474
|
+
{ archive, details: { original_error: error } }
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function scanGzipFile(archive, options) {
|
|
480
|
+
const filename = basename(archive, ".gz");
|
|
481
|
+
const stats = statSync(archive);
|
|
482
|
+
const entry = {
|
|
483
|
+
path: filename,
|
|
484
|
+
type: "file" /* FILE */,
|
|
485
|
+
size: 0,
|
|
486
|
+
// Would need to decompress to get actual size
|
|
487
|
+
compressed_size: stats.size,
|
|
488
|
+
modified: stats.mtime.toISOString(),
|
|
489
|
+
...options.include_metadata && { mode: "0644" }
|
|
490
|
+
// Default for gzip files
|
|
491
|
+
};
|
|
492
|
+
return [entry];
|
|
493
|
+
}
|
|
494
|
+
async function createTarGzArchive(sources, output, options) {
|
|
495
|
+
const writeStream = createWriteStream(output);
|
|
496
|
+
const archive = new TarArchive({
|
|
497
|
+
gzip: true,
|
|
498
|
+
gzipOptions: {
|
|
499
|
+
level: options.compression_level
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
let entryCount = 0;
|
|
503
|
+
let totalSize = 0;
|
|
504
|
+
const writePromise = new Promise((resolve, reject) => {
|
|
505
|
+
writeStream.on("close", () => resolve());
|
|
506
|
+
writeStream.on("error", reject);
|
|
507
|
+
archive.on(
|
|
508
|
+
"error",
|
|
509
|
+
(error) => reject(
|
|
510
|
+
new FulpackOperationError(
|
|
511
|
+
createFulpackError(
|
|
512
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
513
|
+
`TAR.GZ creation failed: ${error.message}`,
|
|
514
|
+
"create" /* CREATE */,
|
|
515
|
+
{ details: { original_error: error } }
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
archive.pipe(writeStream);
|
|
522
|
+
for (const sourcePath of sources) {
|
|
523
|
+
if (!existsSync(sourcePath)) {
|
|
524
|
+
throw new FulpackOperationError(
|
|
525
|
+
createFulpackError(
|
|
526
|
+
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
527
|
+
`Source not found: ${sourcePath}`,
|
|
528
|
+
"create" /* CREATE */,
|
|
529
|
+
{ details: { source: sourcePath } }
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const stats = statSync(sourcePath);
|
|
534
|
+
if (stats.isDirectory()) {
|
|
535
|
+
await addDirectoryToTarGzArchive(archive, sourcePath, "", options);
|
|
536
|
+
} else {
|
|
537
|
+
const entryName = basename(sourcePath);
|
|
538
|
+
archive.file(sourcePath, {
|
|
539
|
+
name: entryName,
|
|
540
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
541
|
+
});
|
|
542
|
+
entryCount++;
|
|
543
|
+
totalSize += stats.size;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
archive.finalize().catch(() => {
|
|
547
|
+
});
|
|
548
|
+
await writePromise;
|
|
549
|
+
const outputStats = statSync(output);
|
|
550
|
+
return {
|
|
551
|
+
format: "tar.gz" /* TAR_GZ */,
|
|
552
|
+
compression: "gzip",
|
|
553
|
+
entry_count: entryCount,
|
|
554
|
+
total_size: totalSize,
|
|
555
|
+
compressed_size: outputStats.size,
|
|
556
|
+
compression_ratio: totalSize > 0 ? outputStats.size / totalSize : 0,
|
|
557
|
+
has_checksums: false,
|
|
558
|
+
// TODO: Add fulhash checksum integration
|
|
559
|
+
created: outputStats.mtime.toISOString()
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function createZipArchive(sources, output, options) {
|
|
563
|
+
const writeStream = createWriteStream(output);
|
|
564
|
+
const archive = new ZipArchive({
|
|
565
|
+
zlib: { level: options.compression_level }
|
|
566
|
+
});
|
|
567
|
+
let entryCount = 0;
|
|
568
|
+
let totalSize = 0;
|
|
569
|
+
const writePromise = new Promise((resolve, reject) => {
|
|
570
|
+
writeStream.on("close", () => resolve());
|
|
571
|
+
writeStream.on("error", reject);
|
|
572
|
+
archive.on(
|
|
573
|
+
"error",
|
|
574
|
+
(error) => reject(
|
|
575
|
+
new FulpackOperationError(
|
|
576
|
+
createFulpackError(
|
|
577
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
578
|
+
`ZIP creation failed: ${error.message}`,
|
|
579
|
+
"create" /* CREATE */,
|
|
580
|
+
{ details: { original_error: error } }
|
|
581
|
+
)
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
archive.pipe(writeStream);
|
|
587
|
+
for (const sourcePath of sources) {
|
|
588
|
+
if (!existsSync(sourcePath)) {
|
|
589
|
+
throw new FulpackOperationError(
|
|
590
|
+
createFulpackError(
|
|
591
|
+
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
592
|
+
`Source not found: ${sourcePath}`,
|
|
593
|
+
"create" /* CREATE */,
|
|
594
|
+
{ details: { source: sourcePath } }
|
|
595
|
+
)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const stats = statSync(sourcePath);
|
|
599
|
+
if (stats.isDirectory()) {
|
|
600
|
+
await addDirectoryToZipArchive(archive, sourcePath, "", options);
|
|
601
|
+
} else {
|
|
602
|
+
const entryName = basename(sourcePath);
|
|
603
|
+
archive.file(sourcePath, {
|
|
604
|
+
name: entryName,
|
|
605
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
606
|
+
});
|
|
607
|
+
entryCount++;
|
|
608
|
+
totalSize += stats.size;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
archive.finalize().catch(() => {
|
|
612
|
+
});
|
|
613
|
+
await writePromise;
|
|
614
|
+
const outputStats = statSync(output);
|
|
615
|
+
return {
|
|
616
|
+
format: "zip" /* ZIP */,
|
|
617
|
+
compression: "deflate",
|
|
618
|
+
entry_count: entryCount,
|
|
619
|
+
total_size: totalSize,
|
|
620
|
+
compressed_size: outputStats.size,
|
|
621
|
+
compression_ratio: totalSize > 0 ? outputStats.size / totalSize : 0,
|
|
622
|
+
has_checksums: false,
|
|
623
|
+
created: outputStats.mtime.toISOString()
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async function addDirectoryToTarGzArchive(archive, dirPath, archivePrefix, options) {
|
|
627
|
+
const entries = readdirSync(dirPath);
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
const fullPath = join(dirPath, entry);
|
|
630
|
+
const archivePath = join(archivePrefix, entry);
|
|
631
|
+
const stats = lstatSync(fullPath);
|
|
632
|
+
if (stats.isSymbolicLink()) {
|
|
633
|
+
if (!options.follow_symlinks) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const targetStats = statSync(fullPath);
|
|
637
|
+
if (targetStats.isDirectory()) {
|
|
638
|
+
await addDirectoryToTarGzArchive(archive, fullPath, archivePath, options);
|
|
639
|
+
} else {
|
|
640
|
+
archive.file(fullPath, {
|
|
641
|
+
name: archivePath,
|
|
642
|
+
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
} else if (stats.isDirectory()) {
|
|
646
|
+
await addDirectoryToTarGzArchive(archive, fullPath, archivePath, options);
|
|
647
|
+
} else {
|
|
648
|
+
archive.file(fullPath, {
|
|
649
|
+
name: archivePath,
|
|
650
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function createTarArchive(sources, output, options) {
|
|
656
|
+
const writeStream = createWriteStream(output);
|
|
657
|
+
const archive = new TarArchive({
|
|
658
|
+
gzip: false
|
|
659
|
+
// Uncompressed
|
|
660
|
+
});
|
|
661
|
+
let entryCount = 0;
|
|
662
|
+
let totalSize = 0;
|
|
663
|
+
const writePromise = new Promise((resolve, reject) => {
|
|
664
|
+
writeStream.on("close", () => resolve());
|
|
665
|
+
writeStream.on("error", reject);
|
|
666
|
+
archive.on(
|
|
667
|
+
"error",
|
|
668
|
+
(error) => reject(
|
|
669
|
+
new FulpackOperationError(
|
|
670
|
+
createFulpackError(
|
|
671
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
672
|
+
`TAR creation failed: ${error.message}`,
|
|
673
|
+
"create" /* CREATE */,
|
|
674
|
+
{ details: { original_error: error } }
|
|
675
|
+
)
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
archive.pipe(writeStream);
|
|
681
|
+
for (const sourcePath of sources) {
|
|
682
|
+
if (!existsSync(sourcePath)) {
|
|
683
|
+
throw new FulpackOperationError(
|
|
684
|
+
createFulpackError(
|
|
685
|
+
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
686
|
+
`Source not found: ${sourcePath}`,
|
|
687
|
+
"create" /* CREATE */,
|
|
688
|
+
{ details: { source: sourcePath } }
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const stats = statSync(sourcePath);
|
|
693
|
+
if (stats.isDirectory()) {
|
|
694
|
+
await addDirectoryToTarArchive(archive, sourcePath, "", options);
|
|
695
|
+
} else {
|
|
696
|
+
const entryName = basename(sourcePath);
|
|
697
|
+
archive.file(sourcePath, {
|
|
698
|
+
name: entryName,
|
|
699
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
700
|
+
});
|
|
701
|
+
entryCount++;
|
|
702
|
+
totalSize += stats.size;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
archive.finalize().catch(() => {
|
|
706
|
+
});
|
|
707
|
+
await writePromise;
|
|
708
|
+
const outputStats = statSync(output);
|
|
709
|
+
return {
|
|
710
|
+
format: "tar" /* TAR */,
|
|
711
|
+
compression: "none",
|
|
712
|
+
entry_count: entryCount,
|
|
713
|
+
total_size: totalSize,
|
|
714
|
+
compressed_size: outputStats.size,
|
|
715
|
+
compression_ratio: 1,
|
|
716
|
+
// Uncompressed
|
|
717
|
+
has_checksums: false,
|
|
718
|
+
// TODO: Add fulhash checksum integration
|
|
719
|
+
created: outputStats.mtime.toISOString()
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
async function addDirectoryToTarArchive(archive, dirPath, archivePrefix, options) {
|
|
723
|
+
const entries = readdirSync(dirPath);
|
|
724
|
+
for (const entry of entries) {
|
|
725
|
+
const fullPath = join(dirPath, entry);
|
|
726
|
+
const archivePath = join(archivePrefix, entry);
|
|
727
|
+
const stats = lstatSync(fullPath);
|
|
728
|
+
if (stats.isSymbolicLink()) {
|
|
729
|
+
if (!options.follow_symlinks) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const targetStats = statSync(fullPath);
|
|
733
|
+
if (targetStats.isDirectory()) {
|
|
734
|
+
await addDirectoryToTarArchive(archive, fullPath, archivePath, options);
|
|
735
|
+
} else {
|
|
736
|
+
archive.file(fullPath, {
|
|
737
|
+
name: archivePath,
|
|
738
|
+
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
} else if (stats.isDirectory()) {
|
|
742
|
+
await addDirectoryToTarArchive(archive, fullPath, archivePath, options);
|
|
743
|
+
} else {
|
|
744
|
+
archive.file(fullPath, {
|
|
745
|
+
name: archivePath,
|
|
746
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function createGzipFile(sources, output, options) {
|
|
752
|
+
if (sources.length !== 1) {
|
|
753
|
+
throw new FulpackOperationError(
|
|
754
|
+
createFulpackError(
|
|
755
|
+
ERROR_CODES.INVALID_OPTIONS,
|
|
756
|
+
`GZIP format requires exactly one source file, got ${sources.length}`,
|
|
757
|
+
"create" /* CREATE */
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const sourcePath = sources[0];
|
|
762
|
+
if (!existsSync(sourcePath)) {
|
|
763
|
+
throw new FulpackOperationError(
|
|
764
|
+
createFulpackError(
|
|
765
|
+
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
766
|
+
`Source not found: ${sourcePath}`,
|
|
767
|
+
"create" /* CREATE */,
|
|
768
|
+
{ details: { source: sourcePath } }
|
|
769
|
+
)
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const stats = statSync(sourcePath);
|
|
773
|
+
if (stats.isDirectory()) {
|
|
774
|
+
throw new FulpackOperationError(
|
|
775
|
+
createFulpackError(
|
|
776
|
+
ERROR_CODES.INVALID_OPTIONS,
|
|
777
|
+
"GZIP format does not support directories",
|
|
778
|
+
"create" /* CREATE */,
|
|
779
|
+
{ details: { source: sourcePath } }
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const readStream = createReadStream(sourcePath);
|
|
784
|
+
const writeStream = createWriteStream(output);
|
|
785
|
+
const gzip = createGzip({ level: options.compression_level });
|
|
786
|
+
await new Promise((resolve, reject) => {
|
|
787
|
+
pipeline(readStream, gzip, writeStream, (error) => {
|
|
788
|
+
if (error) {
|
|
789
|
+
reject(error);
|
|
790
|
+
} else {
|
|
791
|
+
resolve(void 0);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
const outputStats = statSync(output);
|
|
796
|
+
return {
|
|
797
|
+
format: "gzip" /* GZIP */,
|
|
798
|
+
compression: "gzip",
|
|
799
|
+
entry_count: 1,
|
|
800
|
+
total_size: stats.size,
|
|
801
|
+
compressed_size: outputStats.size,
|
|
802
|
+
compression_ratio: stats.size > 0 ? outputStats.size / stats.size : 0,
|
|
803
|
+
has_checksums: false,
|
|
804
|
+
created: outputStats.mtime.toISOString()
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async function addDirectoryToZipArchive(archive, dirPath, archivePrefix, options) {
|
|
808
|
+
const entries = readdirSync(dirPath);
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
const fullPath = join(dirPath, entry);
|
|
811
|
+
const archivePath = join(archivePrefix, entry);
|
|
812
|
+
const stats = lstatSync(fullPath);
|
|
813
|
+
if (stats.isSymbolicLink()) {
|
|
814
|
+
if (!options.follow_symlinks) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const targetStats = statSync(fullPath);
|
|
818
|
+
if (targetStats.isDirectory()) {
|
|
819
|
+
await addDirectoryToZipArchive(archive, fullPath, archivePath, options);
|
|
820
|
+
} else {
|
|
821
|
+
archive.file(fullPath, {
|
|
822
|
+
name: archivePath,
|
|
823
|
+
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
} else if (stats.isDirectory()) {
|
|
827
|
+
await addDirectoryToZipArchive(archive, fullPath, archivePath, options);
|
|
828
|
+
} else {
|
|
829
|
+
archive.file(fullPath, {
|
|
830
|
+
name: archivePath,
|
|
831
|
+
mode: options.preserve_permissions ? stats.mode : 420
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function extractTarGz(archive, destination, options) {
|
|
837
|
+
const tarStream = await import('tar-stream');
|
|
838
|
+
const extract2 = tarStream.extract();
|
|
839
|
+
let extractedCount = 0;
|
|
840
|
+
let skippedCount = 0;
|
|
841
|
+
let errorCount = 0;
|
|
842
|
+
const errors = [];
|
|
843
|
+
const warnings = [];
|
|
844
|
+
let totalExtractedSize = 0;
|
|
845
|
+
let entryCount = 0;
|
|
846
|
+
extract2.on("entry", async (header, stream, next) => {
|
|
847
|
+
entryCount++;
|
|
848
|
+
try {
|
|
849
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
850
|
+
errors.push(
|
|
851
|
+
createFulpackError(
|
|
852
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
853
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
854
|
+
"extract" /* EXTRACT */,
|
|
855
|
+
{
|
|
856
|
+
archive,
|
|
857
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
858
|
+
}
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
stream.resume();
|
|
862
|
+
next();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const pathError = validatePath(header.name, "extract" /* EXTRACT */, false);
|
|
866
|
+
if (pathError) {
|
|
867
|
+
errors.push({ ...pathError, archive });
|
|
868
|
+
errorCount++;
|
|
869
|
+
stream.resume();
|
|
870
|
+
next();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const outputPath = join(destination, header.name);
|
|
874
|
+
if (header.type === "directory") {
|
|
875
|
+
if (!existsSync(outputPath)) {
|
|
876
|
+
mkdirSync(outputPath, { recursive: true });
|
|
877
|
+
}
|
|
878
|
+
extractedCount++;
|
|
879
|
+
stream.resume();
|
|
880
|
+
next();
|
|
881
|
+
} else if (header.type === "file") {
|
|
882
|
+
if (existsSync(outputPath)) {
|
|
883
|
+
if (options.overwrite === "error") {
|
|
884
|
+
errors.push(
|
|
885
|
+
createFulpackError(
|
|
886
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
887
|
+
`Output file already exists: ${outputPath}`,
|
|
888
|
+
"extract" /* EXTRACT */,
|
|
889
|
+
{ path: header.name, archive }
|
|
890
|
+
)
|
|
891
|
+
);
|
|
892
|
+
errorCount++;
|
|
893
|
+
stream.resume();
|
|
894
|
+
next();
|
|
895
|
+
return;
|
|
896
|
+
} else if (options.overwrite === "skip") {
|
|
897
|
+
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
898
|
+
skippedCount++;
|
|
899
|
+
stream.resume();
|
|
900
|
+
next();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const parentDir = dirname(outputPath);
|
|
905
|
+
if (!existsSync(parentDir)) {
|
|
906
|
+
mkdirSync(parentDir, { recursive: true });
|
|
907
|
+
}
|
|
908
|
+
const writeStream = createWriteStream(outputPath);
|
|
909
|
+
stream.on("data", (chunk) => {
|
|
910
|
+
totalExtractedSize += chunk.length;
|
|
911
|
+
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
912
|
+
stream.destroy();
|
|
913
|
+
writeStream.destroy();
|
|
914
|
+
errors.push(
|
|
915
|
+
createFulpackError(
|
|
916
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
917
|
+
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
918
|
+
"extract" /* EXTRACT */,
|
|
919
|
+
{
|
|
920
|
+
archive,
|
|
921
|
+
path: header.name,
|
|
922
|
+
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
);
|
|
926
|
+
errorCount++;
|
|
927
|
+
next();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
stream.pipe(writeStream);
|
|
932
|
+
writeStream.on("finish", () => {
|
|
933
|
+
if (options.preserve_permissions && header.mode) {
|
|
934
|
+
try {
|
|
935
|
+
const { chmodSync } = __require("fs");
|
|
936
|
+
chmodSync(outputPath, header.mode);
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
extractedCount++;
|
|
941
|
+
next();
|
|
942
|
+
});
|
|
943
|
+
writeStream.on("error", (error) => {
|
|
944
|
+
errors.push(
|
|
945
|
+
createFulpackError(
|
|
946
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
947
|
+
`Failed to write file: ${error.message}`,
|
|
948
|
+
"extract" /* EXTRACT */,
|
|
949
|
+
{ path: header.name, archive, details: { original_error: error } }
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
errorCount++;
|
|
953
|
+
next();
|
|
954
|
+
});
|
|
955
|
+
} else {
|
|
956
|
+
warnings.push(`Skipped entry type ${header.type}: ${header.name}`);
|
|
957
|
+
stream.resume();
|
|
958
|
+
next();
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
errors.push(
|
|
962
|
+
createFulpackError(
|
|
963
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
964
|
+
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
965
|
+
"extract" /* EXTRACT */,
|
|
966
|
+
{ path: header.name, archive, details: { original_error: error } }
|
|
967
|
+
)
|
|
968
|
+
);
|
|
969
|
+
errorCount++;
|
|
970
|
+
stream.resume();
|
|
971
|
+
next();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
const readStream = createReadStream(archive);
|
|
975
|
+
const gunzip = createGunzip();
|
|
976
|
+
return new Promise((resolve, reject) => {
|
|
977
|
+
extract2.on("finish", () => {
|
|
978
|
+
resolve({
|
|
979
|
+
extracted_count: extractedCount,
|
|
980
|
+
skipped_count: skippedCount,
|
|
981
|
+
error_count: errorCount,
|
|
982
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
983
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
984
|
+
total_bytes: totalExtractedSize
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
extract2.on("error", (error) => {
|
|
988
|
+
reject(
|
|
989
|
+
new FulpackOperationError(
|
|
990
|
+
createFulpackError(
|
|
991
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
992
|
+
`TAR.GZ extraction failed: ${error.message}`,
|
|
993
|
+
"extract" /* EXTRACT */,
|
|
994
|
+
{ archive, details: { original_error: error } }
|
|
995
|
+
)
|
|
996
|
+
)
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
readStream.pipe(gunzip).pipe(extract2);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
async function extractTar(archive, destination, options) {
|
|
1003
|
+
const tarStream = await import('tar-stream');
|
|
1004
|
+
const extract2 = tarStream.extract();
|
|
1005
|
+
let extractedCount = 0;
|
|
1006
|
+
let skippedCount = 0;
|
|
1007
|
+
let errorCount = 0;
|
|
1008
|
+
const errors = [];
|
|
1009
|
+
const warnings = [];
|
|
1010
|
+
let totalExtractedSize = 0;
|
|
1011
|
+
let entryCount = 0;
|
|
1012
|
+
extract2.on("entry", async (header, stream, next) => {
|
|
1013
|
+
entryCount++;
|
|
1014
|
+
try {
|
|
1015
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
1016
|
+
errors.push(
|
|
1017
|
+
createFulpackError(
|
|
1018
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1019
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
1020
|
+
"extract" /* EXTRACT */,
|
|
1021
|
+
{
|
|
1022
|
+
archive,
|
|
1023
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
1024
|
+
}
|
|
1025
|
+
)
|
|
1026
|
+
);
|
|
1027
|
+
stream.resume();
|
|
1028
|
+
next();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const pathError = validatePath(header.name, "extract" /* EXTRACT */, false);
|
|
1032
|
+
if (pathError) {
|
|
1033
|
+
errors.push({ ...pathError, archive });
|
|
1034
|
+
errorCount++;
|
|
1035
|
+
stream.resume();
|
|
1036
|
+
next();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const outputPath = join(destination, header.name);
|
|
1040
|
+
if (header.type === "directory") {
|
|
1041
|
+
if (!existsSync(outputPath)) {
|
|
1042
|
+
mkdirSync(outputPath, { recursive: true });
|
|
1043
|
+
}
|
|
1044
|
+
extractedCount++;
|
|
1045
|
+
stream.resume();
|
|
1046
|
+
next();
|
|
1047
|
+
} else if (header.type === "file") {
|
|
1048
|
+
if (existsSync(outputPath)) {
|
|
1049
|
+
if (options.overwrite === "error") {
|
|
1050
|
+
errors.push(
|
|
1051
|
+
createFulpackError(
|
|
1052
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1053
|
+
`Output file already exists: ${outputPath}`,
|
|
1054
|
+
"extract" /* EXTRACT */,
|
|
1055
|
+
{ path: header.name, archive }
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
errorCount++;
|
|
1059
|
+
stream.resume();
|
|
1060
|
+
next();
|
|
1061
|
+
return;
|
|
1062
|
+
} else if (options.overwrite === "skip") {
|
|
1063
|
+
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
1064
|
+
skippedCount++;
|
|
1065
|
+
stream.resume();
|
|
1066
|
+
next();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const parentDir = dirname(outputPath);
|
|
1071
|
+
if (!existsSync(parentDir)) {
|
|
1072
|
+
mkdirSync(parentDir, { recursive: true });
|
|
1073
|
+
}
|
|
1074
|
+
const writeStream = createWriteStream(outputPath);
|
|
1075
|
+
stream.on("data", (chunk) => {
|
|
1076
|
+
totalExtractedSize += chunk.length;
|
|
1077
|
+
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
1078
|
+
stream.destroy();
|
|
1079
|
+
writeStream.destroy();
|
|
1080
|
+
errors.push(
|
|
1081
|
+
createFulpackError(
|
|
1082
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1083
|
+
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
1084
|
+
"extract" /* EXTRACT */,
|
|
1085
|
+
{
|
|
1086
|
+
archive,
|
|
1087
|
+
path: header.name,
|
|
1088
|
+
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
1091
|
+
);
|
|
1092
|
+
errorCount++;
|
|
1093
|
+
next();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
stream.pipe(writeStream);
|
|
1098
|
+
writeStream.on("finish", () => {
|
|
1099
|
+
if (options.preserve_permissions && header.mode) {
|
|
1100
|
+
try {
|
|
1101
|
+
const { chmodSync } = __require("fs");
|
|
1102
|
+
chmodSync(outputPath, header.mode);
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
extractedCount++;
|
|
1107
|
+
next();
|
|
1108
|
+
});
|
|
1109
|
+
writeStream.on("error", (error) => {
|
|
1110
|
+
errors.push(
|
|
1111
|
+
createFulpackError(
|
|
1112
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1113
|
+
`Failed to write file: ${error.message}`,
|
|
1114
|
+
"extract" /* EXTRACT */,
|
|
1115
|
+
{ path: header.name, archive, details: { original_error: error } }
|
|
1116
|
+
)
|
|
1117
|
+
);
|
|
1118
|
+
errorCount++;
|
|
1119
|
+
next();
|
|
1120
|
+
});
|
|
1121
|
+
} else {
|
|
1122
|
+
warnings.push(`Skipped entry type ${header.type}: ${header.name}`);
|
|
1123
|
+
stream.resume();
|
|
1124
|
+
next();
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
errors.push(
|
|
1128
|
+
createFulpackError(
|
|
1129
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1130
|
+
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1131
|
+
"extract" /* EXTRACT */,
|
|
1132
|
+
{ path: header.name, archive, details: { original_error: error } }
|
|
1133
|
+
)
|
|
1134
|
+
);
|
|
1135
|
+
errorCount++;
|
|
1136
|
+
stream.resume();
|
|
1137
|
+
next();
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
const readStream = createReadStream(archive);
|
|
1141
|
+
return new Promise((resolve, reject) => {
|
|
1142
|
+
extract2.on("finish", () => {
|
|
1143
|
+
resolve({
|
|
1144
|
+
extracted_count: extractedCount,
|
|
1145
|
+
skipped_count: skippedCount,
|
|
1146
|
+
error_count: errorCount,
|
|
1147
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1148
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
1149
|
+
total_bytes: totalExtractedSize
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
extract2.on("error", (error) => {
|
|
1153
|
+
reject(
|
|
1154
|
+
new FulpackOperationError(
|
|
1155
|
+
createFulpackError(
|
|
1156
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1157
|
+
`TAR extraction failed: ${error.message}`,
|
|
1158
|
+
"extract" /* EXTRACT */,
|
|
1159
|
+
{ archive, details: { original_error: error } }
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
});
|
|
1164
|
+
readStream.pipe(extract2);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
async function extractZip(archive, destination, options) {
|
|
1168
|
+
const unzipper = await import('unzipper');
|
|
1169
|
+
let extractedCount = 0;
|
|
1170
|
+
let skippedCount = 0;
|
|
1171
|
+
let errorCount = 0;
|
|
1172
|
+
const errors = [];
|
|
1173
|
+
const warnings = [];
|
|
1174
|
+
let totalExtractedSize = 0;
|
|
1175
|
+
let entryCount = 0;
|
|
1176
|
+
return new Promise((resolve, reject) => {
|
|
1177
|
+
const readStream = createReadStream(archive);
|
|
1178
|
+
readStream.pipe(unzipper.Parse()).on("entry", async (entry) => {
|
|
1179
|
+
entryCount++;
|
|
1180
|
+
try {
|
|
1181
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
1182
|
+
errors.push(
|
|
1183
|
+
createFulpackError(
|
|
1184
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1185
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
1186
|
+
"extract" /* EXTRACT */,
|
|
1187
|
+
{
|
|
1188
|
+
archive,
|
|
1189
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
1190
|
+
}
|
|
1191
|
+
)
|
|
1192
|
+
);
|
|
1193
|
+
entry.autodrain();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const pathError = validatePath(entry.path, "extract" /* EXTRACT */, false);
|
|
1197
|
+
if (pathError) {
|
|
1198
|
+
errors.push({ ...pathError, archive });
|
|
1199
|
+
errorCount++;
|
|
1200
|
+
entry.autodrain();
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const outputPath = join(destination, entry.path);
|
|
1204
|
+
if (entry.type === "Directory") {
|
|
1205
|
+
if (!existsSync(outputPath)) {
|
|
1206
|
+
mkdirSync(outputPath, { recursive: true });
|
|
1207
|
+
}
|
|
1208
|
+
extractedCount++;
|
|
1209
|
+
entry.autodrain();
|
|
1210
|
+
} else if (entry.type === "File") {
|
|
1211
|
+
if (existsSync(outputPath)) {
|
|
1212
|
+
if (options.overwrite === "error") {
|
|
1213
|
+
errors.push(
|
|
1214
|
+
createFulpackError(
|
|
1215
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1216
|
+
`Output file already exists: ${outputPath}`,
|
|
1217
|
+
"extract" /* EXTRACT */,
|
|
1218
|
+
{ path: entry.path, archive }
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
errorCount++;
|
|
1222
|
+
entry.autodrain();
|
|
1223
|
+
return;
|
|
1224
|
+
} else if (options.overwrite === "skip") {
|
|
1225
|
+
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
1226
|
+
skippedCount++;
|
|
1227
|
+
entry.autodrain();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const parentDir = dirname(outputPath);
|
|
1232
|
+
if (!existsSync(parentDir)) {
|
|
1233
|
+
mkdirSync(parentDir, { recursive: true });
|
|
1234
|
+
}
|
|
1235
|
+
const writeStream = createWriteStream(outputPath);
|
|
1236
|
+
entry.on("data", (chunk) => {
|
|
1237
|
+
totalExtractedSize += chunk.length;
|
|
1238
|
+
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
1239
|
+
entry.destroy();
|
|
1240
|
+
writeStream.destroy();
|
|
1241
|
+
errors.push(
|
|
1242
|
+
createFulpackError(
|
|
1243
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1244
|
+
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
1245
|
+
"extract" /* EXTRACT */,
|
|
1246
|
+
{
|
|
1247
|
+
archive,
|
|
1248
|
+
path: entry.path,
|
|
1249
|
+
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
errorCount++;
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
entry.pipe(writeStream);
|
|
1258
|
+
writeStream.on("finish", () => {
|
|
1259
|
+
extractedCount++;
|
|
1260
|
+
});
|
|
1261
|
+
writeStream.on("error", (error) => {
|
|
1262
|
+
errors.push(
|
|
1263
|
+
createFulpackError(
|
|
1264
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1265
|
+
`Failed to write file: ${error.message}`,
|
|
1266
|
+
"extract" /* EXTRACT */,
|
|
1267
|
+
{ path: entry.path, archive, details: { original_error: error } }
|
|
1268
|
+
)
|
|
1269
|
+
);
|
|
1270
|
+
errorCount++;
|
|
1271
|
+
});
|
|
1272
|
+
} else {
|
|
1273
|
+
warnings.push(`Skipped entry type ${entry.type}: ${entry.path}`);
|
|
1274
|
+
entry.autodrain();
|
|
1275
|
+
}
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
errors.push(
|
|
1278
|
+
createFulpackError(
|
|
1279
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1280
|
+
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1281
|
+
"extract" /* EXTRACT */,
|
|
1282
|
+
{ path: entry.path, archive, details: { original_error: error } }
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1285
|
+
errorCount++;
|
|
1286
|
+
entry.autodrain();
|
|
1287
|
+
}
|
|
1288
|
+
}).on("close", () => {
|
|
1289
|
+
resolve({
|
|
1290
|
+
extracted_count: extractedCount,
|
|
1291
|
+
skipped_count: skippedCount,
|
|
1292
|
+
error_count: errorCount,
|
|
1293
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1294
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
1295
|
+
total_bytes: totalExtractedSize
|
|
1296
|
+
});
|
|
1297
|
+
}).on("error", (error) => {
|
|
1298
|
+
reject(
|
|
1299
|
+
new FulpackOperationError(
|
|
1300
|
+
createFulpackError(
|
|
1301
|
+
ERROR_CODES.EXTRACTION_FAILED,
|
|
1302
|
+
`ZIP extraction failed: ${error.message}`,
|
|
1303
|
+
"extract" /* EXTRACT */,
|
|
1304
|
+
{ archive, details: { original_error: error } }
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
async function scanTar(archive, options) {
|
|
1312
|
+
const tarStream = await import('tar-stream');
|
|
1313
|
+
const extract2 = tarStream.extract();
|
|
1314
|
+
const entries = [];
|
|
1315
|
+
let entryCount = 0;
|
|
1316
|
+
return new Promise((resolve, reject) => {
|
|
1317
|
+
extract2.on("entry", async (header, stream, next) => {
|
|
1318
|
+
entryCount++;
|
|
1319
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
1320
|
+
stream.resume();
|
|
1321
|
+
next();
|
|
1322
|
+
reject(
|
|
1323
|
+
new FulpackOperationError(
|
|
1324
|
+
createFulpackError(
|
|
1325
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1326
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
1327
|
+
"scan" /* SCAN */,
|
|
1328
|
+
{
|
|
1329
|
+
archive,
|
|
1330
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
1331
|
+
}
|
|
1332
|
+
)
|
|
1333
|
+
)
|
|
1334
|
+
);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const entry = {
|
|
1338
|
+
path: header.name,
|
|
1339
|
+
type: header.type === "directory" ? "directory" /* DIRECTORY */ : header.type === "symlink" ? "symlink" /* SYMLINK */ : "file" /* FILE */,
|
|
1340
|
+
size: header.size || 0,
|
|
1341
|
+
modified: header.mtime ? header.mtime.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1342
|
+
...options.include_metadata && header.mode !== void 0 && header.mode !== null ? { mode: `0${(header.mode & 511).toString(8)}` } : {},
|
|
1343
|
+
...options.include_metadata && header.type === "symlink" && header.linkname ? { symlink_target: header.linkname } : {}
|
|
1344
|
+
};
|
|
1345
|
+
entries.push(entry);
|
|
1346
|
+
stream.resume();
|
|
1347
|
+
next();
|
|
1348
|
+
});
|
|
1349
|
+
extract2.on("finish", () => {
|
|
1350
|
+
resolve(entries);
|
|
1351
|
+
});
|
|
1352
|
+
extract2.on("error", (error) => {
|
|
1353
|
+
reject(
|
|
1354
|
+
new FulpackOperationError(
|
|
1355
|
+
createFulpackError(
|
|
1356
|
+
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
1357
|
+
`TAR scanning failed: ${error.message}`,
|
|
1358
|
+
"scan" /* SCAN */,
|
|
1359
|
+
{ archive, details: { original_error: error } }
|
|
1360
|
+
)
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
});
|
|
1364
|
+
const readStream = createReadStream(archive);
|
|
1365
|
+
readStream.pipe(extract2);
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
async function scanTarGz(archive, options) {
|
|
1369
|
+
const tarStream = await import('tar-stream');
|
|
1370
|
+
const extract2 = tarStream.extract();
|
|
1371
|
+
const entries = [];
|
|
1372
|
+
let entryCount = 0;
|
|
1373
|
+
return new Promise((resolve, reject) => {
|
|
1374
|
+
extract2.on("entry", async (header, stream, next) => {
|
|
1375
|
+
entryCount++;
|
|
1376
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
1377
|
+
stream.resume();
|
|
1378
|
+
next();
|
|
1379
|
+
reject(
|
|
1380
|
+
new FulpackOperationError(
|
|
1381
|
+
createFulpackError(
|
|
1382
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1383
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
1384
|
+
"scan" /* SCAN */,
|
|
1385
|
+
{
|
|
1386
|
+
archive,
|
|
1387
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
1388
|
+
}
|
|
1389
|
+
)
|
|
1390
|
+
)
|
|
1391
|
+
);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const entry = {
|
|
1395
|
+
path: header.name,
|
|
1396
|
+
type: header.type === "directory" ? "directory" /* DIRECTORY */ : header.type === "symlink" ? "symlink" /* SYMLINK */ : "file" /* FILE */,
|
|
1397
|
+
size: header.size || 0,
|
|
1398
|
+
modified: header.mtime ? header.mtime.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1399
|
+
...options.include_metadata && header.mode !== void 0 && header.mode !== null ? { mode: `0${(header.mode & 511).toString(8)}` } : {},
|
|
1400
|
+
...options.include_metadata && header.type === "symlink" && header.linkname ? { symlink_target: header.linkname } : {}
|
|
1401
|
+
};
|
|
1402
|
+
entries.push(entry);
|
|
1403
|
+
stream.resume();
|
|
1404
|
+
next();
|
|
1405
|
+
});
|
|
1406
|
+
extract2.on("finish", () => {
|
|
1407
|
+
resolve(entries);
|
|
1408
|
+
});
|
|
1409
|
+
extract2.on("error", (error) => {
|
|
1410
|
+
reject(
|
|
1411
|
+
new FulpackOperationError(
|
|
1412
|
+
createFulpackError(
|
|
1413
|
+
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
1414
|
+
`TAR.GZ scanning failed: ${error.message}`,
|
|
1415
|
+
"scan" /* SCAN */,
|
|
1416
|
+
{ archive, details: { original_error: error } }
|
|
1417
|
+
)
|
|
1418
|
+
)
|
|
1419
|
+
);
|
|
1420
|
+
});
|
|
1421
|
+
const readStream = createReadStream(archive);
|
|
1422
|
+
const gunzip = createGunzip();
|
|
1423
|
+
readStream.pipe(gunzip).pipe(extract2);
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
async function scanZip(archive, options) {
|
|
1427
|
+
const unzipper = await import('unzipper');
|
|
1428
|
+
const entries = [];
|
|
1429
|
+
let entryCount = 0;
|
|
1430
|
+
return new Promise((resolve, reject) => {
|
|
1431
|
+
createReadStream(archive).pipe(unzipper.Parse()).on("entry", (entry) => {
|
|
1432
|
+
const typedEntry = entry;
|
|
1433
|
+
entryCount++;
|
|
1434
|
+
if (options.max_entries && entryCount > options.max_entries) {
|
|
1435
|
+
typedEntry.autodrain();
|
|
1436
|
+
reject(
|
|
1437
|
+
new FulpackOperationError(
|
|
1438
|
+
createFulpackError(
|
|
1439
|
+
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
1440
|
+
`Entry count exceeds maximum (${options.max_entries})`,
|
|
1441
|
+
"scan" /* SCAN */,
|
|
1442
|
+
{
|
|
1443
|
+
archive,
|
|
1444
|
+
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
1445
|
+
}
|
|
1446
|
+
)
|
|
1447
|
+
)
|
|
1448
|
+
);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const uncompressedSize = typedEntry.vars?.uncompressedSize || typedEntry.props?.size || 0;
|
|
1452
|
+
const compressedSize = typedEntry.vars?.compressedSize || typedEntry.props?.compressedSize || 0;
|
|
1453
|
+
const archiveEntry = {
|
|
1454
|
+
path: typedEntry.path,
|
|
1455
|
+
type: typedEntry.type === "Directory" ? "directory" /* DIRECTORY */ : "file" /* FILE */,
|
|
1456
|
+
size: uncompressedSize,
|
|
1457
|
+
modified: typedEntry.vars?.lastModifiedTime ? new Date(typedEntry.vars.lastModifiedTime).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1458
|
+
...options.include_metadata ? {
|
|
1459
|
+
compressed_size: compressedSize,
|
|
1460
|
+
mode: "0644"
|
|
1461
|
+
// ZIP format doesn't preserve Unix permissions consistently
|
|
1462
|
+
} : {}
|
|
1463
|
+
};
|
|
1464
|
+
entries.push(archiveEntry);
|
|
1465
|
+
typedEntry.autodrain();
|
|
1466
|
+
}).on("error", (error) => {
|
|
1467
|
+
reject(
|
|
1468
|
+
new FulpackOperationError(
|
|
1469
|
+
createFulpackError(
|
|
1470
|
+
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
1471
|
+
`ZIP scanning failed: ${error.message}`,
|
|
1472
|
+
"scan" /* SCAN */,
|
|
1473
|
+
{ archive, details: { original_error: error } }
|
|
1474
|
+
)
|
|
1475
|
+
)
|
|
1476
|
+
);
|
|
1477
|
+
}).on("finish", () => {
|
|
1478
|
+
resolve(entries);
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/fulpack/index.ts
|
|
1484
|
+
var FULPACK_VERSION = "1.0.0";
|
|
1485
|
+
|
|
1486
|
+
export { ERROR_CODES, FULPACK_VERSION, FulpackOperationError, checkDecompressionBomb, create, createFulpackError, extract, hasPathTraversal, info, isAbsolutePath, scan, validatePath, verify };
|
|
1487
|
+
//# sourceMappingURL=chunk-P3DNOZOG.js.map
|
|
1488
|
+
//# sourceMappingURL=chunk-P3DNOZOG.js.map
|