@vltpkg/tar 1.0.0-rc.23 → 1.0.0-rc.25

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.
@@ -0,0 +1 @@
1
+ export declare const findTarDir: (path: string | undefined, tarDir?: string) => string | undefined;
@@ -0,0 +1,22 @@
1
+ // usually this will be 'package/', but could also be anything
2
+ // eg, github tarballs are ${user}-${project}-${committish}
3
+ // if it starts with `./` then all entries must as well.
4
+ export const findTarDir = (path, tarDir) => {
5
+ if (tarDir !== undefined)
6
+ return tarDir;
7
+ if (!path)
8
+ return undefined;
9
+ const i = path.indexOf('/', path.startsWith('./') ? 2 : 0);
10
+ if (i === -1)
11
+ return undefined;
12
+ const chomp = path.substring(0, i);
13
+ if (chomp === '.' ||
14
+ chomp === '..' ||
15
+ chomp === '' ||
16
+ chomp === './.' ||
17
+ chomp === './..' ||
18
+ chomp === './') {
19
+ return undefined;
20
+ }
21
+ return chomp + '/';
22
+ };
@@ -0,0 +1,3 @@
1
+ export * from './unpack.ts';
2
+ export * from './pool.ts';
3
+ export * from './unpack-request.ts';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./unpack.js";
2
+ export * from "./pool.js";
3
+ export * from "./unpack-request.js";
package/dist/pool.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { UnpackRequest } from './unpack-request.ts';
2
+ import { Worker } from './worker.ts';
3
+ export * from './worker.ts';
4
+ /**
5
+ * Automatically expanding/contracting set of workers to maximize parallelism
6
+ * of unpack operations up to 1 less than the number of CPUs (or 1).
7
+ *
8
+ * `pool.unpack(tarData, target)` will perform the unpack operation
9
+ * synchronously, in one of these workers, and returns a promise when the
10
+ * worker has confirmed completion of the task.
11
+ */
12
+ export declare class Pool {
13
+ #private;
14
+ /**
15
+ * Number of workers to emplly. Defaults to 1 less than the number of
16
+ * CPUs, or 1.
17
+ */
18
+ jobs: number;
19
+ /**
20
+ * Set of currently active worker threads
21
+ */
22
+ workers: Set<Worker>;
23
+ /**
24
+ * Queue of requests awaiting an available worker
25
+ */
26
+ queue: UnpackRequest[];
27
+ /**
28
+ * Requests that have been assigned to a worker, but have not yet
29
+ * been confirmed completed.
30
+ */
31
+ pending: Map<number, UnpackRequest>;
32
+ /**
33
+ * Provide the tardata to be unpacked, and the location where it's to be
34
+ * placed. Will create a new worker up to the `jobs` value, and then start
35
+ * pushing in the queue for workers to pick up as they become available.
36
+ *
37
+ * Returned promise resolves when the provided tarball has been extracted.
38
+ */
39
+ unpack(tarData: Buffer, target: string): Promise<void>;
40
+ }
package/dist/pool.js ADDED
@@ -0,0 +1,87 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { asError } from '@vltpkg/types';
3
+ import os from 'node:os';
4
+ import { UnpackRequest } from "./unpack-request.js";
5
+ import { isResponseOK, Worker } from "./worker.js";
6
+ export * from "./worker.js";
7
+ /**
8
+ * Automatically expanding/contracting set of workers to maximize parallelism
9
+ * of unpack operations up to 1 less than the number of CPUs (or 1).
10
+ *
11
+ * `pool.unpack(tarData, target)` will perform the unpack operation
12
+ * synchronously, in one of these workers, and returns a promise when the
13
+ * worker has confirmed completion of the task.
14
+ */
15
+ export class Pool {
16
+ /**
17
+ * Number of workers to emplly. Defaults to 1 less than the number of
18
+ * CPUs, or 1.
19
+ */
20
+ /* c8 ignore next */
21
+ jobs = 8 * (Math.max(os.availableParallelism(), 2) - 1);
22
+ /**
23
+ * Set of currently active worker threads
24
+ */
25
+ workers = new Set();
26
+ /**
27
+ * Queue of requests awaiting an available worker
28
+ */
29
+ queue = [];
30
+ /**
31
+ * Requests that have been assigned to a worker, but have not yet
32
+ * been confirmed completed.
33
+ */
34
+ pending = new Map();
35
+ // handle a message from the worker
36
+ #onMessage(w, m) {
37
+ const { id } = m;
38
+ // a request has been met or failed, report and either
39
+ // pick up the next item in the queue, or terminate worker
40
+ const ur = this.pending.get(id);
41
+ /* c8 ignore next */
42
+ if (!ur)
43
+ return;
44
+ if (isResponseOK(m)) {
45
+ ur.resolve();
46
+ /* c8 ignore start - nearly impossible in normal circumstances */
47
+ }
48
+ else {
49
+ ur.reject(error(asError(m.error, 'failed without error message').message, {
50
+ found: m,
51
+ cause: m.error,
52
+ }));
53
+ }
54
+ /* c8 ignore stop */
55
+ const next = this.queue.shift();
56
+ if (!next) {
57
+ this.workers.delete(w);
58
+ }
59
+ else {
60
+ void w.process(next);
61
+ }
62
+ }
63
+ // create a new worker
64
+ #createWorker(req) {
65
+ const w = new Worker((m) => this.#onMessage(w, m));
66
+ this.workers.add(w);
67
+ void w.process(req);
68
+ }
69
+ /**
70
+ * Provide the tardata to be unpacked, and the location where it's to be
71
+ * placed. Will create a new worker up to the `jobs` value, and then start
72
+ * pushing in the queue for workers to pick up as they become available.
73
+ *
74
+ * Returned promise resolves when the provided tarball has been extracted.
75
+ */
76
+ async unpack(tarData, target) {
77
+ const ur = new UnpackRequest(tarData, target);
78
+ this.pending.set(ur.id, ur);
79
+ if (this.workers.size < this.jobs) {
80
+ this.#createWorker(ur);
81
+ }
82
+ else {
83
+ this.queue.push(ur);
84
+ }
85
+ return ur.promise;
86
+ }
87
+ }
@@ -0,0 +1,9 @@
1
+ export declare class UnpackRequest {
2
+ id: number;
3
+ tarData: Buffer;
4
+ target: string;
5
+ resolve: () => void;
6
+ reject: (reason?: any) => void;
7
+ promise: Promise<void>;
8
+ constructor(tarData: Buffer, target: string);
9
+ }
@@ -0,0 +1,16 @@
1
+ let ID = 1;
2
+ export class UnpackRequest {
3
+ id = ID++;
4
+ tarData;
5
+ target;
6
+ resolve;
7
+ reject;
8
+ promise = new Promise((res, rej) => {
9
+ this.resolve = res;
10
+ this.reject = rej;
11
+ });
12
+ constructor(tarData, target) {
13
+ this.tarData = tarData;
14
+ this.target = target;
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export declare const unpack: (tarData: Buffer, target: string) => Promise<void>;
package/dist/unpack.js ADDED
@@ -0,0 +1,175 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { lstat, mkdir, rename, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, resolve, sep } from 'node:path';
5
+ import { rimraf } from 'rimraf';
6
+ import { Header } from 'tar/header';
7
+ import { Pax } from 'tar/pax';
8
+ import { unzip as unzipCB } from 'node:zlib';
9
+ import { findTarDir } from "./find-tar-dir.js";
10
+ const unzip = async (input) => new Promise((res, rej) =>
11
+ /* c8 ignore start */
12
+ unzipCB(input, (er, result) => (er ? rej(er) : res(result))));
13
+ const exists = async (path) => {
14
+ try {
15
+ await lstat(path);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
22
+ let id = 1;
23
+ const tmp = randomBytes(6).toString('hex') + '.';
24
+ const tmpSuffix = () => tmp + String(id++);
25
+ const checkFs = (h, tarDir, target) => {
26
+ /* c8 ignore start - impossible */
27
+ if (!h.path)
28
+ return false;
29
+ if (!tarDir)
30
+ return false;
31
+ /* c8 ignore stop */
32
+ h.path = h.path.replace(/[\\/]+/g, '/');
33
+ // packages should always be in a 'package' tarDir in the archive
34
+ if (!h.path.startsWith(tarDir))
35
+ return false;
36
+ // Package root
37
+ const absoluteBasePath = target;
38
+ const itemAbsolutePath = resolve(target, h.path.slice(tarDir.length));
39
+ if (!itemAbsolutePath.startsWith(absoluteBasePath)) {
40
+ return false;
41
+ }
42
+ return true;
43
+ };
44
+ const write = async (path, body, executable = false) => {
45
+ await mkdirp(dirname(path));
46
+ // if the mode is world-executable, then make it executable
47
+ // this is needed for some packages that have a file that is
48
+ // not a declared bin, but still used as a cli executable.
49
+ await writeFile(path, body, {
50
+ mode: executable ? 0o777 : 0o666,
51
+ });
52
+ };
53
+ const made = new Set();
54
+ const making = new Map();
55
+ const mkdirp = async (d) => {
56
+ if (!made.has(d)) {
57
+ const m = making.get(d) ??
58
+ mkdir(d, { recursive: true, mode: 0o777 }).then(() => making.delete(d));
59
+ making.set(d, m);
60
+ await m;
61
+ made.add(d);
62
+ }
63
+ };
64
+ export const unpack = async (tarData, target) => {
65
+ const isGzip = tarData[0] === 0x1f && tarData[1] === 0x8b;
66
+ await unpackUnzipped(isGzip ? await unzip(tarData) : tarData, target);
67
+ };
68
+ const unpackUnzipped = async (buffer, target) => {
69
+ /* c8 ignore start */
70
+ const isGzip = buffer[0] === 0x1f && buffer[1] === 0x8b;
71
+ if (isGzip) {
72
+ throw error('still gzipped after unzipping', {
73
+ found: isGzip,
74
+ wanted: false,
75
+ });
76
+ }
77
+ /* c8 ignore stop */
78
+ // another real quick gutcheck before we get started
79
+ if (buffer.length % 512 !== 0) {
80
+ throw error('Invalid tarball: length not divisible by 512', {
81
+ found: buffer.length,
82
+ });
83
+ }
84
+ if (buffer.length < 1024) {
85
+ throw error('Invalid tarball: not terminated by 1024 null bytes', { found: buffer.length });
86
+ }
87
+ // make sure the last kb is all zeros
88
+ for (let i = buffer.length - 1024; i < buffer.length; i++) {
89
+ if (buffer[i] !== 0) {
90
+ throw error('Invalid tarball: not terminated by 1024 null bytes', { found: buffer.subarray(i, i + 10) });
91
+ }
92
+ }
93
+ const tmp = dirname(target) + sep + '.' + basename(target) + '.' + tmpSuffix();
94
+ const og = tmp + '.ORIGINAL';
95
+ await Promise.all([rimraf(tmp), rimraf(og)]);
96
+ let succeeded = false;
97
+ try {
98
+ let tarDir = undefined;
99
+ let offset = 0;
100
+ let h;
101
+ let ex = undefined;
102
+ let gex = undefined;
103
+ while (offset < buffer.length &&
104
+ !(h = new Header(buffer, offset, ex, gex)).nullBlock) {
105
+ offset += 512;
106
+ ex = undefined;
107
+ gex = undefined;
108
+ const size = h.size ?? 0;
109
+ const body = buffer.subarray(offset, offset + size);
110
+ // skip invalid headers
111
+ if (!h.cksumValid)
112
+ continue;
113
+ offset += 512 * Math.ceil(size / 512);
114
+ // TODO: tarDir might not be named "package/"
115
+ // find the first tarDir in the first entry, and use that.
116
+ switch (h.type) {
117
+ case 'File':
118
+ if (!tarDir)
119
+ tarDir = findTarDir(h.path, tarDir);
120
+ /* c8 ignore next */
121
+ if (!tarDir)
122
+ continue;
123
+ if (!checkFs(h, tarDir, tmp))
124
+ continue;
125
+ await write(resolve(tmp, h.path.substring(tarDir.length)), body,
126
+ // if it's world-executable, it's an executable
127
+ // otherwise, make it read-only.
128
+ 1 === ((h.mode ?? 0x666) & 1));
129
+ break;
130
+ case 'Directory':
131
+ /* c8 ignore next 2 */
132
+ if (!tarDir)
133
+ tarDir = findTarDir(h.path, tarDir);
134
+ if (!tarDir)
135
+ continue;
136
+ if (!checkFs(h, tarDir, tmp))
137
+ continue;
138
+ await mkdirp(resolve(tmp, h.path.substring(tarDir.length)));
139
+ break;
140
+ case 'GlobalExtendedHeader':
141
+ gex = Pax.parse(body.toString(), gex, true);
142
+ break;
143
+ case 'ExtendedHeader':
144
+ case 'OldExtendedHeader':
145
+ ex = Pax.parse(body.toString(), ex, false);
146
+ break;
147
+ case 'NextFileHasLongPath':
148
+ case 'OldGnuLongPath':
149
+ ex ??= Object.create(null);
150
+ ex.path = body.toString().replace(/\0.*/, '');
151
+ break;
152
+ }
153
+ }
154
+ const targetExists = await exists(target);
155
+ if (targetExists)
156
+ await rename(target, og);
157
+ await rename(tmp, target);
158
+ if (targetExists)
159
+ await rimraf(og);
160
+ succeeded = true;
161
+ }
162
+ finally {
163
+ // do not handle error or obscure throw site, just do the cleanup
164
+ // if it didn't complete successfully.
165
+ if (!succeeded) {
166
+ /* c8 ignore start */
167
+ if (await exists(og)) {
168
+ await rimraf(target);
169
+ await rename(og, target);
170
+ }
171
+ /* c8 ignore stop */
172
+ await rimraf(tmp);
173
+ }
174
+ }
175
+ };
@@ -0,0 +1,19 @@
1
+ import type { UnpackRequest } from './unpack-request.ts';
2
+ export type ResponseError = {
3
+ id: number;
4
+ error: unknown;
5
+ };
6
+ export type ResponseOK = {
7
+ id: number;
8
+ ok: true;
9
+ };
10
+ export declare const isResponseOK: (o: unknown) => o is ResponseOK;
11
+ /**
12
+ * Basically just a queue of unpack requests,
13
+ * to keep them throttled to a reasonable amount of parallelism
14
+ */
15
+ export declare class Worker {
16
+ onMessage: (m: ResponseError | ResponseOK) => void;
17
+ constructor(onMessage: (m: ResponseError | ResponseOK) => void);
18
+ process(req: UnpackRequest): Promise<void>;
19
+ }
package/dist/worker.js ADDED
@@ -0,0 +1,25 @@
1
+ import { unpack } from "./unpack.js";
2
+ const isObj = (o) => !!o && typeof o === 'object';
3
+ export const isResponseOK = (o) => isObj(o) && typeof o.id === 'number' && o.ok === true;
4
+ /**
5
+ * Basically just a queue of unpack requests,
6
+ * to keep them throttled to a reasonable amount of parallelism
7
+ */
8
+ export class Worker {
9
+ onMessage;
10
+ constructor(onMessage) {
11
+ this.onMessage = onMessage;
12
+ }
13
+ async process(req) {
14
+ const { target, tarData, id } = req;
15
+ try {
16
+ await unpack(tarData, target);
17
+ const m = { id, ok: true };
18
+ this.onMessage(m);
19
+ }
20
+ catch (error) {
21
+ const m = { id, error };
22
+ this.onMessage(m);
23
+ }
24
+ }
25
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/tar",
3
3
  "description": "An extremely limited and very fast tar extractor",
4
- "version": "1.0.0-rc.23",
4
+ "version": "1.0.0-rc.25",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -12,8 +12,8 @@
12
12
  "email": "support@vlt.sh"
13
13
  },
14
14
  "dependencies": {
15
- "@vltpkg/error-cause": "1.0.0-rc.23",
16
- "@vltpkg/types": "1.0.0-rc.23",
15
+ "@vltpkg/error-cause": "1.0.0-rc.25",
16
+ "@vltpkg/types": "1.0.0-rc.25",
17
17
  "rimraf": "^6.1.2",
18
18
  "tar": "^7.5.2"
19
19
  },