@uipath/maestro-builder-sdk 6.4.0 → 6.5.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/dist/cli/commands/registry.js +68 -12
- package/dist/registry/archive/entry-policy.d.ts +36 -0
- package/dist/registry/archive/entry-policy.js +60 -0
- package/dist/registry/archive/extract.d.ts +31 -0
- package/dist/registry/archive/extract.js +92 -0
- package/dist/registry/archive/zip-reader.d.ts +38 -0
- package/dist/registry/archive/zip-reader.js +256 -0
- package/dist/registry/codecs/pyjson.d.ts +62 -0
- package/dist/registry/codecs/pyjson.js +208 -0
- package/dist/registry/compat/compare.d.ts +18 -0
- package/dist/registry/compat/compare.js +32 -0
- package/dist/registry/compat/paths.d.ts +4 -0
- package/dist/registry/compat/paths.js +17 -0
- package/dist/registry/compat/pystr.d.ts +50 -0
- package/dist/registry/compat/pystr.js +221 -0
- package/dist/registry/concurrency.d.ts +10 -0
- package/dist/registry/concurrency.js +34 -0
- package/dist/registry/curated-overlay.d.ts +36 -4
- package/dist/registry/curated-overlay.js +87 -14
- package/dist/registry/errors.d.ts +26 -0
- package/dist/registry/errors.js +38 -0
- package/dist/registry/extractor.d.ts +9 -10
- package/dist/registry/extractor.js +5 -15
- package/dist/registry/files.d.ts +26 -0
- package/dist/registry/files.js +160 -0
- package/dist/registry/generators/descriptors.d.ts +81 -0
- package/dist/registry/generators/descriptors.js +405 -0
- package/dist/registry/generators/fields.d.ts +46 -0
- package/dist/registry/generators/fields.js +111 -0
- package/dist/registry/generators/markdown/common.d.ts +21 -0
- package/dist/registry/generators/markdown/common.js +55 -0
- package/dist/registry/generators/markdown/index.d.ts +33 -0
- package/dist/registry/generators/markdown/index.js +37 -0
- package/dist/registry/generators/markdown/page.d.ts +8 -0
- package/dist/registry/generators/markdown/page.js +52 -0
- package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
- package/dist/registry/generators/markdown/pyvalue.js +106 -0
- package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
- package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
- package/dist/registry/generators/markdown/sections/example.js +295 -0
- package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/filtering.js +76 -0
- package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/header.js +37 -0
- package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
- package/dist/registry/generators/markdown/sections/inputs.js +151 -0
- package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
- package/dist/registry/generators/markdown/sections/lookups.js +150 -0
- package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
- package/dist/registry/generators/markdown/sections/outputs.js +9 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
- package/dist/registry/generators/materialize.d.ts +30 -0
- package/dist/registry/generators/materialize.js +132 -0
- package/dist/registry/generators/md-symbols.d.ts +22 -0
- package/dist/registry/generators/md-symbols.js +58 -0
- package/dist/registry/generators/triggers.d.ts +15 -0
- package/dist/registry/generators/triggers.js +85 -0
- package/dist/registry/generators/type-registry.d.ts +50 -0
- package/dist/registry/generators/type-registry.js +154 -0
- package/dist/registry/generators/type-tree.d.ts +65 -0
- package/dist/registry/generators/type-tree.js +179 -0
- package/dist/registry/library-build/build.d.ts +57 -0
- package/dist/registry/library-build/build.js +286 -0
- package/dist/registry/library-build/canonical.d.ts +119 -0
- package/dist/registry/library-build/canonical.js +512 -0
- package/dist/registry/library-build/cli.d.ts +45 -0
- package/dist/registry/library-build/cli.js +182 -0
- package/dist/registry/library-build/context.d.ts +107 -0
- package/dist/registry/library-build/context.js +267 -0
- package/dist/registry/library-build/enrich.d.ts +36 -0
- package/dist/registry/library-build/enrich.js +158 -0
- package/dist/registry/library-build/events.d.ts +108 -0
- package/dist/registry/library-build/events.js +476 -0
- package/dist/registry/library-build/generic-expand.d.ts +77 -0
- package/dist/registry/library-build/generic-expand.js +363 -0
- package/dist/registry/library-build/plan.d.ts +71 -0
- package/dist/registry/library-build/plan.js +270 -0
- package/dist/registry/library-build/py.d.ts +45 -0
- package/dist/registry/library-build/py.js +167 -0
- package/dist/registry/library-build/schema-action.d.ts +38 -0
- package/dist/registry/library-build/schema-action.js +107 -0
- package/dist/registry/library-build/write.d.ts +6 -0
- package/dist/registry/library-build/write.js +26 -0
- package/dist/registry/library-reader.d.ts +20 -0
- package/dist/registry/library-reader.js +50 -0
- package/dist/registry/lookup-specs.d.ts +55 -0
- package/dist/registry/lookup-specs.js +140 -0
- package/dist/registry/materialize-cli.js +28 -14
- package/dist/registry/materializer.d.ts +25 -3
- package/dist/registry/materializer.js +45 -29
- package/dist/registry/model/library.d.ts +254 -0
- package/dist/registry/model/library.js +23 -0
- package/dist/registry/naming.d.ts +54 -0
- package/dist/registry/naming.js +142 -0
- package/dist/registry/orphans.d.ts +4 -0
- package/dist/registry/orphans.js +86 -0
- package/dist/registry/prepare/actions.d.ts +26 -0
- package/dist/registry/prepare/actions.js +94 -0
- package/dist/registry/prepare/connections.d.ts +181 -0
- package/dist/registry/prepare/connections.js +399 -0
- package/dist/registry/prepare/event.d.ts +96 -0
- package/dist/registry/prepare/event.js +268 -0
- package/dist/registry/prepare/handoff.d.ts +21 -0
- package/dist/registry/prepare/handoff.js +91 -0
- package/dist/registry/prepare/ledger.d.ts +104 -0
- package/dist/registry/prepare/ledger.js +216 -0
- package/dist/registry/prepare/messages.d.ts +190 -0
- package/dist/registry/prepare/messages.js +472 -0
- package/dist/registry/prepare/objects.d.ts +130 -0
- package/dist/registry/prepare/objects.js +318 -0
- package/dist/registry/prepare/overlay.d.ts +149 -0
- package/dist/registry/prepare/overlay.js +487 -0
- package/dist/registry/prepare/parents.d.ts +41 -0
- package/dist/registry/prepare/parents.js +122 -0
- package/dist/registry/prepare/pipeline.d.ts +91 -0
- package/dist/registry/prepare/pipeline.js +376 -0
- package/dist/registry/prepare/regenerate.d.ts +9 -0
- package/dist/registry/prepare/regenerate.js +50 -0
- package/dist/registry/prepare/resolve.d.ts +199 -0
- package/dist/registry/prepare/resolve.js +470 -0
- package/dist/registry/prepare/retry.d.ts +83 -0
- package/dist/registry/prepare/retry.js +109 -0
- package/dist/registry/prepare/source-import.d.ts +54 -0
- package/dist/registry/prepare/source-import.js +192 -0
- package/dist/registry/prepare/support.d.ts +41 -0
- package/dist/registry/prepare/support.js +178 -0
- package/dist/registry/prepare-cli.d.ts +16 -5
- package/dist/registry/prepare-cli.js +60 -53
- package/dist/registry/pull-cli.js +8 -7
- package/dist/registry/reporter.d.ts +26 -0
- package/dist/registry/reporter.js +41 -0
- package/dist/registry/uip/client.d.ts +99 -0
- package/dist/registry/uip/client.js +226 -0
- package/dist/registry/uip/commands.d.ts +40 -0
- package/dist/registry/uip/commands.js +80 -0
- package/dist/registry/uip/envelope.d.ts +37 -0
- package/dist/registry/uip/envelope.js +84 -0
- package/dist/registry/uip/response-cache.d.ts +28 -0
- package/dist/registry/uip/response-cache.js +57 -0
- package/lib/README.md +10 -8
- package/package.json +3 -3
- package/dist/generators/_connections.py +0 -331
- package/dist/generators/_events.py +0 -355
- package/dist/generators/_lookups.py +0 -114
- package/dist/generators/_naming.py +0 -185
- package/dist/generators/_objects.py +0 -299
- package/dist/generators/_resolve.py +0 -319
- package/dist/generators/convert_library_to_md.py +0 -1441
- package/dist/generators/extract_library.py +0 -125
- package/dist/generators/generate_connectors_ts.py +0 -936
- package/dist/generators/generate_library_json.py +0 -2527
- package/dist/generators/generate_triggers_ts.py +0 -110
- package/dist/generators/materialize_registry.py +0 -155
- package/dist/generators/prepare_connector.py +0 -1860
- package/dist/registry/python.d.ts +0 -26
- package/dist/registry/python.js +0 -46
|
@@ -27,10 +27,10 @@ export function registerRegistryCommands(program) {
|
|
|
27
27
|
.option('--force', 're-download and regenerate even if the archive is cached')
|
|
28
28
|
.option('--import <specifier>', 'module specifier for generated descriptors')
|
|
29
29
|
.option('--triggers <path>', 'registry-triggers.json for trigger descriptors')
|
|
30
|
-
.option('--python <executable>', 'Python interpreter to run the generators with')
|
|
31
30
|
.option('--root <prefix>', 'only extract archive entries under this prefix')
|
|
32
|
-
.option('--
|
|
33
|
-
.option('--
|
|
31
|
+
.option('--python <executable>', '(deprecated, ignored) the generators run in-process')
|
|
32
|
+
.option('--extractor <path>', '(deprecated, ignored) the extractor is part of the package')
|
|
33
|
+
.option('--generator <path>', '(deprecated, ignored) the generators are part of the package')
|
|
34
34
|
.action(async (options) => {
|
|
35
35
|
const argv = [];
|
|
36
36
|
addValue(argv, '--library', options.library);
|
|
@@ -54,12 +54,16 @@ export function registerRegistryCommands(program) {
|
|
|
54
54
|
family.command('materialize')
|
|
55
55
|
.description('Generate library-md and connectors/ from an extracted library-json.')
|
|
56
56
|
.requiredOption('--library-json <dir>', 'extracted library-json directory')
|
|
57
|
-
.
|
|
58
|
-
.
|
|
57
|
+
.option('--library-md <dir>', 'markdown output directory (omit to skip the Markdown)')
|
|
58
|
+
.option('--connectors <dir>', 'TypeScript descriptor output directory (omit to skip descriptors)')
|
|
59
59
|
.option('--import <specifier>', 'module specifier for generated descriptors')
|
|
60
60
|
.option('--triggers <path>', 'registry-triggers.json for trigger descriptors')
|
|
61
|
-
.option('--
|
|
62
|
-
.option('--
|
|
61
|
+
.option('--connector-subset <keys>', 'comma-separated connector keys to emit instead of all')
|
|
62
|
+
.option('--include-generic-expanded', 'also emit descriptors for GenericExpanded entries')
|
|
63
|
+
.option('--check-orphans', 'fail when an entry file has no index.json row (the image build asks for this)')
|
|
64
|
+
.option('--check-symbols', 'fail when a Markdown page cites a symbol the descriptors do not export')
|
|
65
|
+
.option('--python <executable>', '(deprecated, ignored) the generators run in-process')
|
|
66
|
+
.option('--generator <path>', '(deprecated, ignored) the generators are part of the package')
|
|
63
67
|
.action(async (options) => {
|
|
64
68
|
const argv = [];
|
|
65
69
|
addValue(argv, '--library-json', options.libraryJson);
|
|
@@ -67,10 +71,56 @@ export function registerRegistryCommands(program) {
|
|
|
67
71
|
addValue(argv, '--connectors', options.connectors);
|
|
68
72
|
addValue(argv, '--import', options.import);
|
|
69
73
|
addValue(argv, '--triggers', options.triggers);
|
|
74
|
+
addValue(argv, '--connector-subset', options.connectorSubset);
|
|
75
|
+
if (options.includeGenericExpanded)
|
|
76
|
+
argv.push('--include-generic-expanded');
|
|
77
|
+
if (options.checkSymbols)
|
|
78
|
+
argv.push('--check-symbols');
|
|
79
|
+
if (options.checkOrphans)
|
|
80
|
+
argv.push('--check-orphans');
|
|
70
81
|
addValue(argv, '--python', options.python);
|
|
71
82
|
addValue(argv, '--generator', options.generator);
|
|
72
83
|
await invoke('registry materialize', () => loadTool(import.meta.url, '../../registry/materialize-cli.js'), argv);
|
|
73
84
|
});
|
|
85
|
+
// The pipeline's generator, `generate_library_json.py`, flag for flag: the
|
|
86
|
+
// Azure YAML that calls it should only have to swap the executable.
|
|
87
|
+
family.command('build-library')
|
|
88
|
+
.description('Generate the canonical connector library from the UiPath Flow registry.')
|
|
89
|
+
.option('--output-dir <dir>', 'output directory (default: ./library)')
|
|
90
|
+
.option('--cache-dir <dir>', 'cache for `flow registry get` JSONs (default: ./.registry-cache)')
|
|
91
|
+
.option('--is-cache-dir <dir>', 'cache for `is resources describe` JSONs (default: ./.is-cache)')
|
|
92
|
+
.option('--connectors-cache-dir <dir>', 'cache for `is connectors metadata` JSONs (default: ./.is-connectors-cache)')
|
|
93
|
+
.option('--skip-enrichment', 'skip the `is resources describe` enrichment pass')
|
|
94
|
+
.option('--skip-connector-enrichment', 'skip the `is connectors metadata` connector-level pass')
|
|
95
|
+
.option('--skip-generic-expansion', 'keep Generic CRUD entries as templates instead of one entry per object/operation')
|
|
96
|
+
.option('--triggers-output <path>', 'trigger side-file path (default: <parent of --output-dir>/registry-triggers.json)')
|
|
97
|
+
.option('--keep-temp', 'keep the cache directories after the run')
|
|
98
|
+
.option('--offline', 'never run `uip`; treat an uncached call as a failed one (for replaying a build from kept caches)')
|
|
99
|
+
.option('--connectors <keys>', 'comma-separated connector keys to build as a curated subset (default: all)')
|
|
100
|
+
.option('--catalog-scope <scope>', "connector catalog scope: 'all', 'tier1' or 'tier2' (default: all)")
|
|
101
|
+
.option('--concurrency <n>', 'parallel remote CLI calls (default: 10)')
|
|
102
|
+
.action(async (options) => {
|
|
103
|
+
const argv = [];
|
|
104
|
+
addValue(argv, '--output-dir', options.outputDir);
|
|
105
|
+
addValue(argv, '--cache-dir', options.cacheDir);
|
|
106
|
+
addValue(argv, '--is-cache-dir', options.isCacheDir);
|
|
107
|
+
addValue(argv, '--connectors-cache-dir', options.connectorsCacheDir);
|
|
108
|
+
if (options.skipEnrichment)
|
|
109
|
+
argv.push('--skip-enrichment');
|
|
110
|
+
if (options.skipConnectorEnrichment)
|
|
111
|
+
argv.push('--skip-connector-enrichment');
|
|
112
|
+
if (options.skipGenericExpansion)
|
|
113
|
+
argv.push('--skip-generic-expansion');
|
|
114
|
+
addValue(argv, '--triggers-output', options.triggersOutput);
|
|
115
|
+
if (options.keepTemp)
|
|
116
|
+
argv.push('--keep-temp');
|
|
117
|
+
if (options.offline)
|
|
118
|
+
argv.push('--offline');
|
|
119
|
+
addValue(argv, '--connectors', options.connectors);
|
|
120
|
+
addValue(argv, '--catalog-scope', options.catalogScope);
|
|
121
|
+
addValue(argv, '--concurrency', options.concurrency);
|
|
122
|
+
await invoke('registry build-library', () => loadTool(import.meta.url, '../../registry/library-build/cli.js'), argv);
|
|
123
|
+
});
|
|
74
124
|
family.command('prepare')
|
|
75
125
|
.description("Resolve one operation's schema against a live connection into ./connectors-local.")
|
|
76
126
|
.argument('<connector-key>', 'connector key, e.g. uipath-salesforce')
|
|
@@ -82,7 +132,11 @@ export function registerRegistryCommands(program) {
|
|
|
82
132
|
.option('--bindings <file>', 'bindings file to update (default: ./bindings.json)')
|
|
83
133
|
.option('--bind-connection <name>', 'symbolic connection name (default: the connector suffix)')
|
|
84
134
|
.option('--bind-folder <name>', 'symbolic folder name (default: shared)')
|
|
85
|
-
|
|
135
|
+
// A SEPARATE key, not commander's negation of `--bindings`: sharing one key
|
|
136
|
+
// made the result order-dependent, so `--no-bindings --bindings b.json` and
|
|
137
|
+
// the reverse disagreed. argparse had two independent options.
|
|
138
|
+
.option('--skip-bindings', 'discover the connection but leave bindings.json alone')
|
|
139
|
+
.option('--no-bindings', 'alias of --skip-bindings')
|
|
86
140
|
.option('--object <name>', 'one connection-discovered object')
|
|
87
141
|
.option('--all-objects', 'every connection-discovered object')
|
|
88
142
|
.option('-f, --field <name=value>', 'parent field that selects the schema (repeatable)', collect, [])
|
|
@@ -92,8 +146,8 @@ export function registerRegistryCommands(program) {
|
|
|
92
146
|
.option('--connector-version <version>', 'connector version to resolve against')
|
|
93
147
|
.option('--import <specifier>', 'module specifier for generated descriptors')
|
|
94
148
|
.option('--json-only', 'write the JSON overlay without generating TypeScript')
|
|
95
|
-
.option('--python <executable>', '
|
|
96
|
-
.option('--generator <path>', '
|
|
149
|
+
.option('--python <executable>', '(deprecated, ignored once prepare runs in-process)')
|
|
150
|
+
.option('--generator <path>', '(deprecated, ignored: prepare runs in-process and no script is packaged)')
|
|
97
151
|
// Commander declares the surface; the inner CLI only ever sees what is
|
|
98
152
|
// forwarded below. Adding `--resolve` to prepare-cli.ts without adding it
|
|
99
153
|
// HERE made the flag unreachable through the shipped command, while the
|
|
@@ -111,8 +165,10 @@ export function registerRegistryCommands(program) {
|
|
|
111
165
|
addValue(argv, '--bindings', options.bindings);
|
|
112
166
|
addValue(argv, '--bind-connection', options.bindConnection);
|
|
113
167
|
addValue(argv, '--bind-folder', options.bindFolder);
|
|
114
|
-
// commander sets `bindings` false for `--no-bindings
|
|
115
|
-
|
|
168
|
+
// commander sets `bindings` false for `--no-bindings`; `--skip-bindings`
|
|
169
|
+
// arrives under its own key, so neither spelling can be lost to the
|
|
170
|
+
// order the two flags appear in.
|
|
171
|
+
if (options.bindings === false || options.skipBindings)
|
|
116
172
|
argv.push('--no-bindings');
|
|
117
173
|
addValue(argv, '--object', options.object);
|
|
118
174
|
if (options.allObjects)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which archive entries the extractor is willing to write, and where.
|
|
3
|
+
*
|
|
4
|
+
* A zip carries whatever paths its author put in it, so entry names are
|
|
5
|
+
* validated rather than joined. This is `safe_relative_path` / `is_symlink`
|
|
6
|
+
* from `integrations/scripts/extract_library.py`, kept rule for rule so both
|
|
7
|
+
* extractors agree on what a hostile entry looks like:
|
|
8
|
+
*
|
|
9
|
+
* * only entries under the root are considered, and the prefix is stripped,
|
|
10
|
+
* so the tree flattens to the destination;
|
|
11
|
+
* * a segment that is empty, `.` or `..` aborts the whole extraction. Not
|
|
12
|
+
* skips it — aborts. An archive containing `../../etc/foo` is not an
|
|
13
|
+
* archive with one bad file, it is an archive we should not be unpacking;
|
|
14
|
+
* * absolute names and Windows drive letters are refused for the same reason;
|
|
15
|
+
* * symlink entries are refused. Nothing in the library needs one, and a
|
|
16
|
+
* symlink written during extraction turns a later write into a write
|
|
17
|
+
* somewhere else.
|
|
18
|
+
*/
|
|
19
|
+
/** The archive asked for something outside the destination. */
|
|
20
|
+
export declare class UnsafeArchive extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The destination-relative path for `entryName`, or `undefined` to skip it.
|
|
25
|
+
*
|
|
26
|
+
* Throws {@link UnsafeArchive} when the name is one we refuse outright.
|
|
27
|
+
*/
|
|
28
|
+
export declare function safeRelativePath(entryName: string, root: string): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a central-directory `externalAttr` marks a symlink.
|
|
31
|
+
*
|
|
32
|
+
* The unix mode lives in the top 16 bits of the external attributes. Zips
|
|
33
|
+
* written on Windows leave it zero, which reads as "not a symlink" — correct,
|
|
34
|
+
* since those cannot carry one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSymlinkEntry(externalAttr: number): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which archive entries the extractor is willing to write, and where.
|
|
3
|
+
*
|
|
4
|
+
* A zip carries whatever paths its author put in it, so entry names are
|
|
5
|
+
* validated rather than joined. This is `safe_relative_path` / `is_symlink`
|
|
6
|
+
* from `integrations/scripts/extract_library.py`, kept rule for rule so both
|
|
7
|
+
* extractors agree on what a hostile entry looks like:
|
|
8
|
+
*
|
|
9
|
+
* * only entries under the root are considered, and the prefix is stripped,
|
|
10
|
+
* so the tree flattens to the destination;
|
|
11
|
+
* * a segment that is empty, `.` or `..` aborts the whole extraction. Not
|
|
12
|
+
* skips it — aborts. An archive containing `../../etc/foo` is not an
|
|
13
|
+
* archive with one bad file, it is an archive we should not be unpacking;
|
|
14
|
+
* * absolute names and Windows drive letters are refused for the same reason;
|
|
15
|
+
* * symlink entries are refused. Nothing in the library needs one, and a
|
|
16
|
+
* symlink written during extraction turns a later write into a write
|
|
17
|
+
* somewhere else.
|
|
18
|
+
*/
|
|
19
|
+
/** The archive asked for something outside the destination. */
|
|
20
|
+
export class UnsafeArchive extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "UnsafeArchive";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The destination-relative path for `entryName`, or `undefined` to skip it.
|
|
28
|
+
*
|
|
29
|
+
* Throws {@link UnsafeArchive} when the name is one we refuse outright.
|
|
30
|
+
*/
|
|
31
|
+
export function safeRelativePath(entryName, root) {
|
|
32
|
+
const normalized = entryName.replace(/\\/g, "/");
|
|
33
|
+
if (!normalized.startsWith(root))
|
|
34
|
+
return undefined;
|
|
35
|
+
const relative = normalized.slice(root.length);
|
|
36
|
+
if (relative === "" || relative.endsWith("/"))
|
|
37
|
+
return undefined; // the directory entry itself
|
|
38
|
+
const parts = relative.split("/");
|
|
39
|
+
if (relative.startsWith("/") || parts[0].includes(":")) {
|
|
40
|
+
throw new UnsafeArchive(`absolute or drive-qualified entry: ${entryName}`);
|
|
41
|
+
}
|
|
42
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
43
|
+
throw new UnsafeArchive(`unsafe entry: ${entryName}`);
|
|
44
|
+
}
|
|
45
|
+
return relative;
|
|
46
|
+
}
|
|
47
|
+
/** `S_IFMT` and `S_IFLNK` from `<sys/stat.h>`; `stat.S_ISLNK(mode)` is `(mode & S_IFMT) === S_IFLNK`. */
|
|
48
|
+
const S_IFMT = 0o170000;
|
|
49
|
+
const S_IFLNK = 0o120000;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a central-directory `externalAttr` marks a symlink.
|
|
52
|
+
*
|
|
53
|
+
* The unix mode lives in the top 16 bits of the external attributes. Zips
|
|
54
|
+
* written on Windows leave it zero, which reads as "not a symlink" — correct,
|
|
55
|
+
* since those cannot carry one.
|
|
56
|
+
*/
|
|
57
|
+
export function isSymlinkEntry(externalAttr) {
|
|
58
|
+
const mode = (externalAttr >>> 16) & 0xffff;
|
|
59
|
+
return (mode & S_IFMT) === S_IFLNK;
|
|
60
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { UnsafeArchive } from "./entry-policy.js";
|
|
2
|
+
export { NotAZip, UnsupportedArchive } from "./zip-reader.js";
|
|
3
|
+
/** Only entries under this prefix are the library; the prefix is stripped on the way out. */
|
|
4
|
+
export declare const DEFAULT_ROOT = "integrations/library-json/";
|
|
5
|
+
export interface ExtractArchiveOptions {
|
|
6
|
+
/** The downloaded `.zip`. */
|
|
7
|
+
readonly archivePath: string;
|
|
8
|
+
/** Where the flattened `library-json/` tree lands; created if missing. */
|
|
9
|
+
readonly destination: string;
|
|
10
|
+
/** Only extract entries under this prefix (default {@link DEFAULT_ROOT}). */
|
|
11
|
+
readonly root?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ExtractArchiveResult {
|
|
14
|
+
/** Files written — directory entries and entries outside the root do not count. */
|
|
15
|
+
readonly written: number;
|
|
16
|
+
}
|
|
17
|
+
export type ExtractLibraryErrorCode = "NO_SUCH_ARCHIVE" | "UNSAFE_ARCHIVE" | "NOT_A_ZIP" | "UNSUPPORTED_ARCHIVE" | "NO_INDEX";
|
|
18
|
+
/**
|
|
19
|
+
* Extraction stopped. `message` is exactly what the Python printed to stderr
|
|
20
|
+
* (`extract-library: …`), `code` says which way it failed, and `cause` carries
|
|
21
|
+
* the underlying {@link UnsafeArchive} / {@link NotAZip} /
|
|
22
|
+
* {@link UnsupportedArchive} when there is one.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ExtractLibraryError extends Error {
|
|
25
|
+
readonly code: ExtractLibraryErrorCode;
|
|
26
|
+
readonly exitCode: 1;
|
|
27
|
+
constructor(message: string, code: ExtractLibraryErrorCode, options?: {
|
|
28
|
+
cause?: unknown;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export declare function extractArchive(options: ExtractArchiveOptions): ExtractArchiveResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the connector library out of a downloaded zip, safely.
|
|
3
|
+
*
|
|
4
|
+
* This is `integrations/scripts/extract_library.py` in TypeScript, so the SDK
|
|
5
|
+
* no longer needs Python for the one step where `zipfile` was the reason. The
|
|
6
|
+
* caller has already fetched the archive and checked its sha256; this only
|
|
7
|
+
* unpacks it, trusting nothing in it:
|
|
8
|
+
*
|
|
9
|
+
* * entry names go through {@link safeRelativePath}, which flattens the tree
|
|
10
|
+
* to the destination and aborts the WHOLE extraction on traversal — the
|
|
11
|
+
* Python raised mid-loop and left what it had written, and so does this;
|
|
12
|
+
* * symlink entries are refused;
|
|
13
|
+
* * `index.json` has to land, or the archive was not the library and the
|
|
14
|
+
* caller should hear about it rather than get an empty directory.
|
|
15
|
+
*
|
|
16
|
+
* Every failure is an {@link ExtractLibraryError} whose message is the line the
|
|
17
|
+
* Python printed to stderr; the caller prints it and exits 1.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { join, resolve, sep } from "node:path";
|
|
21
|
+
import { isFile, writeBytes, ensureDir } from "../files.js";
|
|
22
|
+
import { UnsafeArchive, isSymlinkEntry, safeRelativePath } from "./entry-policy.js";
|
|
23
|
+
import { NotAZip, UnsupportedArchive, entryData, readZipEntries } from "./zip-reader.js";
|
|
24
|
+
export { UnsafeArchive } from "./entry-policy.js";
|
|
25
|
+
export { NotAZip, UnsupportedArchive } from "./zip-reader.js";
|
|
26
|
+
/** Only entries under this prefix are the library; the prefix is stripped on the way out. */
|
|
27
|
+
export const DEFAULT_ROOT = "integrations/library-json/";
|
|
28
|
+
/**
|
|
29
|
+
* Extraction stopped. `message` is exactly what the Python printed to stderr
|
|
30
|
+
* (`extract-library: …`), `code` says which way it failed, and `cause` carries
|
|
31
|
+
* the underlying {@link UnsafeArchive} / {@link NotAZip} /
|
|
32
|
+
* {@link UnsupportedArchive} when there is one.
|
|
33
|
+
*/
|
|
34
|
+
export class ExtractLibraryError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
exitCode = 1;
|
|
37
|
+
constructor(message, code, options) {
|
|
38
|
+
super(message, options);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = "ExtractLibraryError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** `child` is `parent` or lives beneath it — both already `path.resolve`d. */
|
|
44
|
+
function isWithin(parent, child) {
|
|
45
|
+
return child === parent || child.startsWith(parent.endsWith(sep) ? parent : parent + sep);
|
|
46
|
+
}
|
|
47
|
+
export function extractArchive(options) {
|
|
48
|
+
const { archivePath, destination } = options;
|
|
49
|
+
const root = options.root ?? DEFAULT_ROOT;
|
|
50
|
+
if (!isFile(archivePath)) {
|
|
51
|
+
throw new ExtractLibraryError(`extract-library: no such archive: ${archivePath}`, "NO_SUCH_ARCHIVE");
|
|
52
|
+
}
|
|
53
|
+
ensureDir(destination);
|
|
54
|
+
const destinationRoot = resolve(destination);
|
|
55
|
+
let written = 0;
|
|
56
|
+
try {
|
|
57
|
+
const buffer = readFileSync(archivePath);
|
|
58
|
+
for (const entry of readZipEntries(buffer)) {
|
|
59
|
+
const relative = safeRelativePath(entry.name, root);
|
|
60
|
+
if (relative === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
if (isSymlinkEntry(entry.externalAttr)) {
|
|
63
|
+
throw new UnsafeArchive(`symlink entry: ${entry.name}`);
|
|
64
|
+
}
|
|
65
|
+
const target = resolve(destinationRoot, ...relative.split("/"));
|
|
66
|
+
// Defence in depth: the policy above already guarantees this, so it can
|
|
67
|
+
// never fire — but a second, independent check on the resolved path
|
|
68
|
+
// costs nothing and does not depend on the policy being right.
|
|
69
|
+
if (!isWithin(destinationRoot, target)) {
|
|
70
|
+
throw new UnsafeArchive(`entry escapes the destination: ${entry.name}`);
|
|
71
|
+
}
|
|
72
|
+
writeBytes(target, entryData(buffer, entry));
|
|
73
|
+
written += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof UnsafeArchive) {
|
|
78
|
+
throw new ExtractLibraryError(`extract-library: ${error.message}`, "UNSAFE_ARCHIVE", { cause: error });
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof NotAZip) {
|
|
81
|
+
throw new ExtractLibraryError(`extract-library: ${archivePath} is not a zip: ${error.message}`, "NOT_A_ZIP", { cause: error });
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof UnsupportedArchive) {
|
|
84
|
+
throw new ExtractLibraryError(`extract-library: ${archivePath} uses a zip feature this reader refuses: ${error.message}`, "UNSUPPORTED_ARCHIVE", { cause: error });
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (written === 0 || !isFile(join(destination, "index.json"))) {
|
|
89
|
+
throw new ExtractLibraryError(`extract-library: ${archivePath} carries no ${root}index.json`, "NO_INDEX");
|
|
90
|
+
}
|
|
91
|
+
return { written };
|
|
92
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The bytes are not a zip we can read: no end record, bad signatures, corrupt data. */
|
|
2
|
+
export declare class NotAZip extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** A well-formed zip that uses a feature this reader refuses (zip64, encryption, …). */
|
|
6
|
+
export declare class UnsupportedArchive extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface ZipEntry {
|
|
10
|
+
/** The entry name as stored, `/`-separated by convention (not enforced here). */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** 0 = stored, 8 = deflate. Anything else has already been refused. */
|
|
13
|
+
readonly method: number;
|
|
14
|
+
/** General purpose bit flags from the central directory. */
|
|
15
|
+
readonly flags: number;
|
|
16
|
+
readonly crc32: number;
|
|
17
|
+
readonly compressedSize: number;
|
|
18
|
+
readonly uncompressedSize: number;
|
|
19
|
+
readonly localHeaderOffset: number;
|
|
20
|
+
/** The high 16 bits carry the unix mode when the archive was made on unix. */
|
|
21
|
+
readonly externalAttr: number;
|
|
22
|
+
/** A directory entry: its name ends with `/`. */
|
|
23
|
+
readonly isDirectory: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Every entry in the archive, in central-directory order, with the features we
|
|
27
|
+
* refuse already rejected. Throws {@link NotAZip} when the bytes are not a zip
|
|
28
|
+
* and {@link UnsupportedArchive} for one we decline to read.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readZipEntries(buffer: Uint8Array): ZipEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* The uncompressed bytes of one entry, CRC-checked.
|
|
33
|
+
*
|
|
34
|
+
* The local header's name and extra lengths are read from the local header
|
|
35
|
+
* itself: a writer may store a different extra field there than in the central
|
|
36
|
+
* directory, so the directory's lengths cannot be used to find the data.
|
|
37
|
+
*/
|
|
38
|
+
export declare function entryData(buffer: Uint8Array, entry: ZipEntry): Uint8Array;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zip reader for exactly one archive: the connector library `registry pull`
|
|
3
|
+
* downloads.
|
|
4
|
+
*
|
|
5
|
+
* The SDK has no archive dependency and this keeps it that way. Node 22's
|
|
6
|
+
* `node:zlib` carries `inflateRawSync` and `crc32`, which is everything a
|
|
7
|
+
* reader needs for a zip that stores or deflates. The archive is self-produced
|
|
8
|
+
* by the pipeline (`zip -X` over a sorted file list): compression methods 0 and
|
|
9
|
+
* 8 only, no zip64, no data descriptors, no encryption, ASCII names. Everything
|
|
10
|
+
* outside that is refused rather than guessed at — this is a reader for the
|
|
11
|
+
* archive we make, not a general one.
|
|
12
|
+
*
|
|
13
|
+
* Layout, for the reader who has not seen a zip lately: every file's bytes sit
|
|
14
|
+
* behind a LOCAL FILE HEADER; after all of them comes the CENTRAL DIRECTORY,
|
|
15
|
+
* one header per entry pointing back at its local header; and the file ends
|
|
16
|
+
* with the END OF CENTRAL DIRECTORY record, which locates the directory and may
|
|
17
|
+
* be followed by an archive comment. Readers start at the end.
|
|
18
|
+
*/
|
|
19
|
+
import { crc32, inflateRawSync } from "node:zlib";
|
|
20
|
+
/** The bytes are not a zip we can read: no end record, bad signatures, corrupt data. */
|
|
21
|
+
export class NotAZip extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "NotAZip";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** A well-formed zip that uses a feature this reader refuses (zip64, encryption, …). */
|
|
28
|
+
export class UnsupportedArchive extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "UnsupportedArchive";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const SIG_LOCAL_HEADER = 0x04034b50;
|
|
35
|
+
const SIG_CENTRAL_HEADER = 0x02014b50;
|
|
36
|
+
const SIG_EOCD = 0x06054b50;
|
|
37
|
+
const SIG_ZIP64_LOCATOR = 0x07064b50;
|
|
38
|
+
const LOCAL_HEADER_SIZE = 30;
|
|
39
|
+
const CENTRAL_HEADER_SIZE = 46;
|
|
40
|
+
const EOCD_SIZE = 22;
|
|
41
|
+
const ZIP64_LOCATOR_SIZE = 20;
|
|
42
|
+
const MAX_COMMENT = 0xffff;
|
|
43
|
+
const METHOD_STORED = 0;
|
|
44
|
+
const METHOD_DEFLATE = 8;
|
|
45
|
+
const FLAG_ENCRYPTED = 1 << 0;
|
|
46
|
+
const FLAG_DATA_DESCRIPTOR = 1 << 3;
|
|
47
|
+
const FLAG_UTF8_NAMES = 1 << 11;
|
|
48
|
+
/** Bits 1–2 parameterise deflate and bit 11 names the encoding; nothing else is expected. */
|
|
49
|
+
const FLAGS_UNDERSTOOD = (1 << 1) | (1 << 2) | FLAG_UTF8_NAMES;
|
|
50
|
+
const ZIP64_EXTRA_ID = 0x0001;
|
|
51
|
+
function view(buffer) {
|
|
52
|
+
return new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
53
|
+
}
|
|
54
|
+
/** Decode an entry name: UTF-8 when the archive says so, otherwise ASCII only. */
|
|
55
|
+
function decodeName(bytes, flags) {
|
|
56
|
+
if (flags & FLAG_UTF8_NAMES) {
|
|
57
|
+
try {
|
|
58
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new NotAZip("entry name is not valid UTF-8");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Without bit 11 the name is IBM437, which Node cannot decode and this archive
|
|
65
|
+
// never needs: its names are ASCII, where the two agree.
|
|
66
|
+
let name = "";
|
|
67
|
+
for (const byte of bytes) {
|
|
68
|
+
if (byte > 0x7f)
|
|
69
|
+
throw new UnsupportedArchive("non-ASCII entry name without the UTF-8 flag");
|
|
70
|
+
name += String.fromCharCode(byte);
|
|
71
|
+
}
|
|
72
|
+
return name;
|
|
73
|
+
}
|
|
74
|
+
/** Refuse the features this reader does not implement, before any byte is inflated. */
|
|
75
|
+
function assertSupported(entry, extra) {
|
|
76
|
+
if (entry.flags & FLAG_ENCRYPTED) {
|
|
77
|
+
throw new UnsupportedArchive(`encrypted entry: ${entry.name}`);
|
|
78
|
+
}
|
|
79
|
+
// Python's zipfile tolerates bit 3 (sizes and CRC trail the data in a
|
|
80
|
+
// descriptor, the header carries zeros). This archive never sets it — the
|
|
81
|
+
// central directory always has the real values — so a set bit means a
|
|
82
|
+
// different producer, and refusing is the safer contract than trusting a
|
|
83
|
+
// header we would otherwise have to reconcile with the trailer.
|
|
84
|
+
if (entry.flags & FLAG_DATA_DESCRIPTOR) {
|
|
85
|
+
throw new UnsupportedArchive(`data descriptor entry: ${entry.name}`);
|
|
86
|
+
}
|
|
87
|
+
if (entry.flags & ~FLAGS_UNDERSTOOD & 0xffff) {
|
|
88
|
+
throw new UnsupportedArchive(`unrecognised general purpose flags 0x${entry.flags.toString(16)}: ${entry.name}`);
|
|
89
|
+
}
|
|
90
|
+
if (entry.method !== METHOD_STORED && entry.method !== METHOD_DEFLATE) {
|
|
91
|
+
throw new UnsupportedArchive(`compression method ${entry.method}: ${entry.name}`);
|
|
92
|
+
}
|
|
93
|
+
if (entry.compressedSize === 0xffffffff ||
|
|
94
|
+
entry.uncompressedSize === 0xffffffff ||
|
|
95
|
+
entry.localHeaderOffset === 0xffffffff) {
|
|
96
|
+
throw new UnsupportedArchive(`zip64 entry: ${entry.name}`);
|
|
97
|
+
}
|
|
98
|
+
// Extra fields are (id u16, size u16, data) records. Only the zip64 one matters.
|
|
99
|
+
const dv = view(extra);
|
|
100
|
+
let pos = 0;
|
|
101
|
+
while (pos + 4 <= extra.length) {
|
|
102
|
+
const id = dv.getUint16(pos, true);
|
|
103
|
+
const size = dv.getUint16(pos + 2, true);
|
|
104
|
+
if (id === ZIP64_EXTRA_ID)
|
|
105
|
+
throw new UnsupportedArchive(`zip64 entry: ${entry.name}`);
|
|
106
|
+
pos += 4 + size;
|
|
107
|
+
}
|
|
108
|
+
if (pos !== extra.length)
|
|
109
|
+
throw new NotAZip(`corrupt extra field: ${entry.name}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Find the End Of Central Directory record.
|
|
113
|
+
*
|
|
114
|
+
* It is the last 22 bytes when the archive has no comment; with one, it sits up
|
|
115
|
+
* to 65535 bytes earlier. We scan backwards for the signature and accept the
|
|
116
|
+
* first candidate whose comment length reaches exactly the end of the file, so
|
|
117
|
+
* a stray signature inside a comment cannot be mistaken for the record.
|
|
118
|
+
*/
|
|
119
|
+
function readEndRecord(buffer) {
|
|
120
|
+
const dv = view(buffer);
|
|
121
|
+
const floor = Math.max(0, buffer.length - EOCD_SIZE - MAX_COMMENT);
|
|
122
|
+
for (let pos = buffer.length - EOCD_SIZE; pos >= floor; pos -= 1) {
|
|
123
|
+
if (dv.getUint32(pos, true) !== SIG_EOCD)
|
|
124
|
+
continue;
|
|
125
|
+
const commentLength = dv.getUint16(pos + 20, true);
|
|
126
|
+
if (pos + EOCD_SIZE + commentLength !== buffer.length)
|
|
127
|
+
continue;
|
|
128
|
+
const thisDisk = dv.getUint16(pos + 4, true);
|
|
129
|
+
const directoryDisk = dv.getUint16(pos + 6, true);
|
|
130
|
+
const entriesOnDisk = dv.getUint16(pos + 8, true);
|
|
131
|
+
const totalEntries = dv.getUint16(pos + 10, true);
|
|
132
|
+
const directorySize = dv.getUint32(pos + 12, true);
|
|
133
|
+
const directoryOffset = dv.getUint32(pos + 16, true);
|
|
134
|
+
if (totalEntries === 0xffff ||
|
|
135
|
+
directorySize === 0xffffffff ||
|
|
136
|
+
directoryOffset === 0xffffffff ||
|
|
137
|
+
(pos >= ZIP64_LOCATOR_SIZE && dv.getUint32(pos - ZIP64_LOCATOR_SIZE, true) === SIG_ZIP64_LOCATOR)) {
|
|
138
|
+
throw new UnsupportedArchive("zip64 archive");
|
|
139
|
+
}
|
|
140
|
+
if (thisDisk !== 0 || directoryDisk !== 0 || entriesOnDisk !== totalEntries) {
|
|
141
|
+
throw new UnsupportedArchive("multi-disk archive");
|
|
142
|
+
}
|
|
143
|
+
if (directoryOffset + directorySize > pos) {
|
|
144
|
+
throw new NotAZip("central directory overruns the end record");
|
|
145
|
+
}
|
|
146
|
+
return { totalEntries, directorySize, directoryOffset };
|
|
147
|
+
}
|
|
148
|
+
throw new NotAZip("no end of central directory record found");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Every entry in the archive, in central-directory order, with the features we
|
|
152
|
+
* refuse already rejected. Throws {@link NotAZip} when the bytes are not a zip
|
|
153
|
+
* and {@link UnsupportedArchive} for one we decline to read.
|
|
154
|
+
*/
|
|
155
|
+
export function readZipEntries(buffer) {
|
|
156
|
+
const end = readEndRecord(buffer);
|
|
157
|
+
const dv = view(buffer);
|
|
158
|
+
const directoryEnd = end.directoryOffset + end.directorySize;
|
|
159
|
+
const entries = [];
|
|
160
|
+
let pos = end.directoryOffset;
|
|
161
|
+
for (let index = 0; index < end.totalEntries; index += 1) {
|
|
162
|
+
if (pos + CENTRAL_HEADER_SIZE > directoryEnd) {
|
|
163
|
+
throw new NotAZip(`central directory truncated at entry ${index}`);
|
|
164
|
+
}
|
|
165
|
+
if (dv.getUint32(pos, true) !== SIG_CENTRAL_HEADER) {
|
|
166
|
+
throw new NotAZip(`bad central directory signature at entry ${index}`);
|
|
167
|
+
}
|
|
168
|
+
const flags = dv.getUint16(pos + 8, true);
|
|
169
|
+
const method = dv.getUint16(pos + 10, true);
|
|
170
|
+
const crc = dv.getUint32(pos + 16, true);
|
|
171
|
+
const compressedSize = dv.getUint32(pos + 20, true);
|
|
172
|
+
const uncompressedSize = dv.getUint32(pos + 24, true);
|
|
173
|
+
const nameLength = dv.getUint16(pos + 28, true);
|
|
174
|
+
const extraLength = dv.getUint16(pos + 30, true);
|
|
175
|
+
const commentLength = dv.getUint16(pos + 32, true);
|
|
176
|
+
const externalAttr = dv.getUint32(pos + 38, true);
|
|
177
|
+
const localHeaderOffset = dv.getUint32(pos + 42, true);
|
|
178
|
+
const nameStart = pos + CENTRAL_HEADER_SIZE;
|
|
179
|
+
const extraStart = nameStart + nameLength;
|
|
180
|
+
const next = extraStart + extraLength + commentLength;
|
|
181
|
+
if (next > directoryEnd) {
|
|
182
|
+
throw new NotAZip(`central directory truncated at entry ${index}`);
|
|
183
|
+
}
|
|
184
|
+
const name = decodeName(buffer.subarray(nameStart, extraStart), flags);
|
|
185
|
+
const entry = {
|
|
186
|
+
name,
|
|
187
|
+
method,
|
|
188
|
+
flags,
|
|
189
|
+
crc32: crc,
|
|
190
|
+
compressedSize,
|
|
191
|
+
uncompressedSize,
|
|
192
|
+
localHeaderOffset,
|
|
193
|
+
externalAttr,
|
|
194
|
+
isDirectory: name.endsWith("/"),
|
|
195
|
+
};
|
|
196
|
+
assertSupported(entry, buffer.subarray(extraStart, extraStart + extraLength));
|
|
197
|
+
entries.push(entry);
|
|
198
|
+
pos = next;
|
|
199
|
+
}
|
|
200
|
+
return entries;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The uncompressed bytes of one entry, CRC-checked.
|
|
204
|
+
*
|
|
205
|
+
* The local header's name and extra lengths are read from the local header
|
|
206
|
+
* itself: a writer may store a different extra field there than in the central
|
|
207
|
+
* directory, so the directory's lengths cannot be used to find the data.
|
|
208
|
+
*/
|
|
209
|
+
export function entryData(buffer, entry) {
|
|
210
|
+
assertSupported(entry, new Uint8Array(0));
|
|
211
|
+
const dv = view(buffer);
|
|
212
|
+
const pos = entry.localHeaderOffset;
|
|
213
|
+
if (pos + LOCAL_HEADER_SIZE > buffer.length) {
|
|
214
|
+
throw new NotAZip(`local header out of range: ${entry.name}`);
|
|
215
|
+
}
|
|
216
|
+
if (dv.getUint32(pos, true) !== SIG_LOCAL_HEADER) {
|
|
217
|
+
throw new NotAZip(`bad local header signature: ${entry.name}`);
|
|
218
|
+
}
|
|
219
|
+
const nameLength = dv.getUint16(pos + 26, true);
|
|
220
|
+
const extraLength = dv.getUint16(pos + 28, true);
|
|
221
|
+
const nameStart = pos + LOCAL_HEADER_SIZE;
|
|
222
|
+
const dataStart = nameStart + nameLength + extraLength;
|
|
223
|
+
const dataEnd = dataStart + entry.compressedSize;
|
|
224
|
+
if (dataEnd > buffer.length) {
|
|
225
|
+
throw new NotAZip(`entry data out of range: ${entry.name}`);
|
|
226
|
+
}
|
|
227
|
+
// Python's zipfile makes this check too: the two names must agree, or the
|
|
228
|
+
// directory is pointing at somebody else's bytes.
|
|
229
|
+
const localName = decodeName(buffer.subarray(nameStart, nameStart + nameLength), entry.flags);
|
|
230
|
+
if (localName !== entry.name) {
|
|
231
|
+
throw new NotAZip(`file name in directory '${entry.name}' and header '${localName}' differ`);
|
|
232
|
+
}
|
|
233
|
+
const compressed = buffer.subarray(dataStart, dataEnd);
|
|
234
|
+
let data;
|
|
235
|
+
if (entry.method === METHOD_STORED) {
|
|
236
|
+
if (entry.compressedSize !== entry.uncompressedSize) {
|
|
237
|
+
throw new NotAZip(`stored entry sizes disagree: ${entry.name}`);
|
|
238
|
+
}
|
|
239
|
+
data = compressed;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
try {
|
|
243
|
+
data = inflateRawSync(compressed);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw new NotAZip(`bad deflate data for file '${entry.name}': ${error.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (data.length !== entry.uncompressedSize) {
|
|
250
|
+
throw new NotAZip(`size mismatch for file '${entry.name}': expected ${entry.uncompressedSize}, got ${data.length}`);
|
|
251
|
+
}
|
|
252
|
+
if (crc32(data) !== entry.crc32) {
|
|
253
|
+
throw new NotAZip(`bad CRC-32 for file '${entry.name}'`);
|
|
254
|
+
}
|
|
255
|
+
return data;
|
|
256
|
+
}
|