codekingpro/portable-devtools
115k
1import fs from 'fs';
2import { Minipass } from 'minipass';
3import path from 'path';
4import { Header } from './header.js';
5import { modeFix } from './mode-fix.js';
6import { normalizeWindowsPath } from './normalize-windows-path.js';
7import { dealias } from './options.js';
8import { Pax } from './pax.js';
9import { stripAbsolutePath } from './strip-absolute-path.js';
10import { stripTrailingSlashes } from './strip-trailing-slashes.js';
11import { warnMethod } from './warn-method.js';
12import * as winchars from './winchars.js';
13const prefixPath = (path, prefix) => {
14 if (!prefix) {
15 return normalizeWindowsPath(path);
16 }
17 path = normalizeWindowsPath(path).replace(/^\.(\/|$)/, '');
18 return stripTrailingSlashes(prefix) + '/' + path;
19};
20const maxReadSize = 16 * 1024 * 1024;
21const PROCESS = Symbol('process');
22const FILE = Symbol('file');
23const DIRECTORY = Symbol('directory');
24const SYMLINK = Symbol('symlink');
25const HARDLINK = Symbol('hardlink');
26const HEADER = Symbol('header');
27const READ = Symbol('read');
28const LSTAT = Symbol('lstat');
29const ONLSTAT = Symbol('onlstat');
30const ONREAD = Symbol('onread');
31const ONREADLINK = Symbol('onreadlink');
32const OPENFILE = Symbol('openfile');
33const ONOPENFILE = Symbol('onopenfile');
34const CLOSE = Symbol('close');
35const MODE = Symbol('mode');
36const AWAITDRAIN = Symbol('awaitDrain');
37const ONDRAIN = Symbol('ondrain');
38const PREFIX = Symbol('prefix');
39export class WriteEntry extends Minipass {
40 path;
41 portable;
42 myuid = (process.getuid && process.getuid()) || 0;
43 // until node has builtin pwnam functions, this'll have to do
44 myuser = process.env.USER || '';
45 maxReadSize;
46 linkCache;
47 statCache;
48 preservePaths;
49 cwd;
50 strict;
51 mtime;
52 noPax;
53 noMtime;
54 prefix;
55 fd;
56 blockLen = 0;
57 blockRemain = 0;
58 buf;
59 pos = 0;
60 remain = 0;
61 length = 0;
62 offset = 0;
63 win32;
64 absolute;
65 header;
66 type;
67 linkpath;
68 stat;
69 onWriteEntry;
70 #hadError = false;
71 constructor(p, opt_ = {}) {
72 const opt = dealias(opt_);
73 super();
74 this.path = normalizeWindowsPath(p);
75 // suppress atime, ctime, uid, gid, uname, gname
76 this.portable = !!opt.portable;
77 this.maxReadSize = opt.maxReadSize || maxReadSize;
78 this.linkCache = opt.linkCache || new Map();
79 this.statCache = opt.statCache || new Map();
80 this.preservePaths = !!opt.preservePaths;
81 this.cwd = normalizeWindowsPath(opt.cwd || process.cwd());
82 this.strict = !!opt.strict;
83 this.noPax = !!opt.noPax;
84 this.noMtime = !!opt.noMtime;
85 this.mtime = opt.mtime;
86 this.prefix = opt.prefix ? normalizeWindowsPath(opt.prefix) : undefined;
87 this.onWriteEntry = opt.onWriteEntry;
88 if (typeof opt.onwarn === 'function') {
89 this.on('warn', opt.onwarn);
90 }
91 let pathWarn = false;
92 if (!this.preservePaths) {
93 const [root, stripped] = stripAbsolutePath(this.path);
94 if (root && typeof stripped === 'string') {
95 this.path = stripped;
96 pathWarn = root;
97 }
98 }
99 this.win32 = !!opt.win32 || process.platform === 'win32';
100 if (this.win32) {
101 // force the \ to / normalization, since we might not *actually*
102 // be on windows, but want \ to be considered a path separator.
103 this.path = winchars.decode(this.path.replaceAll(/\\/g, '/'));
104 p = p.replaceAll(/\\/g, '/');
105 }
106 this.absolute = normalizeWindowsPath(opt.absolute || path.resolve(this.cwd, p));
107 if (this.path === '') {
108 this.path = './';
109 }
110 if (pathWarn) {
111 this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
112 entry: this,
113 path: pathWarn + this.path,
114 });
115 }
116 const cs = this.statCache.get(this.absolute);
117 if (cs) {
118 this[ONLSTAT](cs);
119 }
120 else {
121 this[LSTAT]();
122 }
123 }
124 warn(code, message, data = {}) {
125 return warnMethod(this, code, message, data);
126 }
127 emit(ev, ...data) {
128 if (ev === 'error') {
129 this.#hadError = true;
130 }
131 return super.emit(ev, ...data);
132 }
133 [LSTAT]() {
134 fs.lstat(this.absolute, (er, stat) => {
135 if (er) {
136 return this.emit('error', er);
137 }
138 this[ONLSTAT](stat);
139 });
140 }
141 [ONLSTAT](stat) {
142 this.statCache.set(this.absolute, stat);
143 this.stat = stat;
144 if (!stat.isFile()) {
145 stat.size = 0;
146 }
147 this.type = getType(stat);
148 this.emit('stat', stat);
149 this[PROCESS]();
150 }
151 [PROCESS]() {
152 switch (this.type) {
153 case 'File':
154 return this[FILE]();
155 case 'Directory':
156 return this[DIRECTORY]();
157 case 'SymbolicLink':
158 return this[SYMLINK]();
159 // unsupported types are ignored.
160 default:
161 return this.end();
162 }
163 }
164 [MODE](mode) {
165 return modeFix(mode, this.type === 'Directory', this.portable);
166 }
167 [PREFIX](path) {
168 return prefixPath(path, this.prefix);
169 }
170 [HEADER]() {
171 /* c8 ignore start */
172 if (!this.stat) {
173 throw new Error('cannot write header before stat');
174 }
175 /* c8 ignore stop */
176 if (this.type === 'Directory' && this.portable) {
177 this.noMtime = true;
178 }
179 this.onWriteEntry?.(this);
180 this.header = new Header({
181 path: this[PREFIX](this.path),
182 // only apply the prefix to hard links.
183 linkpath: this.type === 'Link' && this.linkpath !== undefined ?
184 this[PREFIX](this.linkpath)
185 : this.linkpath,
186 // only the permissions and setuid/setgid/sticky bitflags
187 // not the higher-order bits that specify file type
188 mode: this[MODE](this.stat.mode),
189 uid: this.portable ? undefined : this.stat.uid,
190 gid: this.portable ? undefined : this.stat.gid,
191 size: this.stat.size,
192 mtime: this.noMtime ? undefined : this.mtime || this.stat.mtime,
193 /* c8 ignore next */
194 type: this.type === 'Unsupported' ? undefined : this.type,
195 uname: this.portable ? undefined
196 : this.stat.uid === this.myuid ? this.myuser
197 : '',
198 atime: this.portable ? undefined : this.stat.atime,
199 ctime: this.portable ? undefined : this.stat.ctime,
200 });
201 if (this.header.encode() && !this.noPax) {
202 super.write(new Pax({
203 atime: this.portable ? undefined : this.header.atime,
204 ctime: this.portable ? undefined : this.header.ctime,
205 gid: this.portable ? undefined : this.header.gid,
206 mtime: this.noMtime ? undefined : this.mtime || this.header.mtime,
207 path: this[PREFIX](this.path),
208 linkpath: this.type === 'Link' && this.linkpath !== undefined ?
209 this[PREFIX](this.linkpath)
210 : this.linkpath,
211 size: this.header.size,
212 uid: this.portable ? undefined : this.header.uid,
213 uname: this.portable ? undefined : this.header.uname,
214 dev: this.portable ? undefined : this.stat.dev,
215 ino: this.portable ? undefined : this.stat.ino,
216 nlink: this.portable ? undefined : this.stat.nlink,
217 }).encode());
218 }
219 const block = this.header?.block;
220 /* c8 ignore start */
221 if (!block) {
222 throw new Error('failed to encode header');
223 }
224 /* c8 ignore stop */
225 super.write(block);
226 }
227 [DIRECTORY]() {
228 /* c8 ignore start */
229 if (!this.stat) {
230 throw new Error('cannot create directory entry without stat');
231 }
232 /* c8 ignore stop */
233 if (this.path.slice(-1) !== '/') {
234 this.path += '/';
235 }
236 this.stat.size = 0;
237 this[HEADER]();
238 this.end();
239 }
240 [SYMLINK]() {
241 fs.readlink(this.absolute, (er, linkpath) => {
242 if (er) {
243 return this.emit('error', er);
244 }
245 this[ONREADLINK](linkpath);
246 });
247 }
248 [ONREADLINK](linkpath) {
249 this.linkpath = normalizeWindowsPath(linkpath);
250 this[HEADER]();
251 this.end();
252 }
253 [HARDLINK](linkpath) {
254 /* c8 ignore start */
255 if (!this.stat) {
256 throw new Error('cannot create link entry without stat');
257 }
258 /* c8 ignore stop */
259 this.type = 'Link';
260 this.linkpath = normalizeWindowsPath(path.relative(this.cwd, linkpath));
261 this.stat.size = 0;
262 this[HEADER]();
263 this.end();
264 }
265 [FILE]() {
266 /* c8 ignore start */
267 if (!this.stat) {
268 throw new Error('cannot create file entry without stat');
269 }
270 /* c8 ignore stop */
271 if (this.stat.nlink > 1) {
272 const linkKey = `${this.stat.dev}:${this.stat.ino}`;
273 const linkpath = this.linkCache.get(linkKey);
274 if (linkpath?.indexOf(this.cwd) === 0) {
275 return this[HARDLINK](linkpath);
276 }
277 this.linkCache.set(linkKey, this.absolute);
278 }
279 this[HEADER]();
280 if (this.stat.size === 0) {
281 return this.end();
282 }
283 this[OPENFILE]();
284 }
285 [OPENFILE]() {
286 fs.open(this.absolute, 'r', (er, fd) => {
287 if (er) {
288 return this.emit('error', er);
289 }
290 this[ONOPENFILE](fd);
291 });
292 }
293 [ONOPENFILE](fd) {
294 this.fd = fd;
295 if (this.#hadError) {
296 return this[CLOSE]();
297 }
298 /* c8 ignore start */
299 if (!this.stat) {
300 throw new Error('should stat before calling onopenfile');
301 }
302 /* c8 ignore start */
303 this.blockLen = 512 * Math.ceil(this.stat.size / 512);
304 this.blockRemain = this.blockLen;
305 const bufLen = Math.min(this.blockLen, this.maxReadSize);
306 this.buf = Buffer.allocUnsafe(bufLen);
307 this.offset = 0;
308 this.pos = 0;
309 this.remain = this.stat.size;
310 this.length = this.buf.length;
311 this[READ]();
312 }
313 [READ]() {
314 const { fd, buf, offset, length, pos } = this;
315 if (fd === undefined || buf === undefined) {
316 throw new Error('cannot read file without first opening');
317 }
318 fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
319 if (er) {
320 // ignoring the error from close(2) is a bad practice, but at
321 // this point we already have an error, don't need another one
322 return this[CLOSE](() => this.emit('error', er));
323 }
324 this[ONREAD](bytesRead);
325 });
326 }
327 /* c8 ignore start */
328 [CLOSE](cb = () => { }) {
329 /* c8 ignore stop */
330 if (this.fd !== undefined)
331 fs.close(this.fd, cb);
332 }
333 [ONREAD](bytesRead) {
334 if (bytesRead <= 0 && this.remain > 0) {
335 const er = Object.assign(new Error('encountered unexpected EOF'), {
336 path: this.absolute,
337 syscall: 'read',
338 code: 'EOF',
339 });
340 return this[CLOSE](() => this.emit('error', er));
341 }
342 if (bytesRead > this.remain) {
343 const er = Object.assign(new Error('did not encounter expected EOF'), {
344 path: this.absolute,
345 syscall: 'read',
346 code: 'EOF',
347 });
348 return this[CLOSE](() => this.emit('error', er));
349 }
350 /* c8 ignore start */
351 if (!this.buf) {
352 throw new Error('should have created buffer prior to reading');
353 }
354 /* c8 ignore stop */
355 // null out the rest of the buffer, if we could fit the block padding
356 // at the end of this loop, we've incremented bytesRead and this.remain
357 // to be incremented up to the blockRemain level, as if we had expected
358 // to get a null-padded file, and read it until the end. then we will
359 // decrement both remain and blockRemain by bytesRead, and know that we
360 // reached the expected EOF, without any null buffer to append.
361 if (bytesRead === this.remain) {
362 for (let i = bytesRead; i < this.length && bytesRead < this.blockRemain; i++) {
363 this.buf[i + this.offset] = 0;
364 bytesRead++;
365 this.remain++;
366 }
367 }
368 const chunk = this.offset === 0 && bytesRead === this.buf.length ?
369 this.buf
370 : this.buf.subarray(this.offset, this.offset + bytesRead);
371 const flushed = this.write(chunk);
372 if (!flushed) {
373 this[AWAITDRAIN](() => this[ONDRAIN]());
374 }
375 else {
376 this[ONDRAIN]();
377 }
378 }
379 [AWAITDRAIN](cb) {
380 this.once('drain', cb);
381 }
382 write(chunk, encoding, cb) {
383 /* c8 ignore start - just junk to comply with NodeJS.WritableStream */
384 if (typeof encoding === 'function') {
385 cb = encoding;
386 encoding = undefined;
387 }
388 if (typeof chunk === 'string') {
389 chunk = Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8');
390 }
391 /* c8 ignore stop */
392 if (this.blockRemain < chunk.length) {
393 const er = Object.assign(new Error('writing more data than expected'), {
394 path: this.absolute,
395 });
396 return this.emit('error', er);
397 }
398 this.remain -= chunk.length;
399 this.blockRemain -= chunk.length;
400 this.pos += chunk.length;
401 this.offset += chunk.length;
402 return super.write(chunk, null, cb);
403 }
404 [ONDRAIN]() {
405 if (!this.remain) {
406 if (this.blockRemain) {
407 super.write(Buffer.alloc(this.blockRemain));
408 }
409 return this[CLOSE](er => (er ? this.emit('error', er) : this.end()));
410 }
411 /* c8 ignore start */
412 if (!this.buf) {
413 throw new Error('buffer lost somehow in ONDRAIN');
414 }
415 /* c8 ignore stop */
416 if (this.offset >= this.length) {
417 // if we only have a smaller bit left to read, alloc a smaller buffer
418 // otherwise, keep it the same length it was before.
419 this.buf = Buffer.allocUnsafe(Math.min(this.blockRemain, this.buf.length));
420 this.offset = 0;
421 }
422 this.length = this.buf.length - this.offset;
423 this[READ]();
424 }
425}
426export class WriteEntrySync extends WriteEntry {
427 sync = true;
428 [LSTAT]() {
429 this[ONLSTAT](fs.lstatSync(this.absolute));
430 }
431 [SYMLINK]() {
432 this[ONREADLINK](fs.readlinkSync(this.absolute));
433 }
434 [OPENFILE]() {
435 this[ONOPENFILE](fs.openSync(this.absolute, 'r'));
436 }
437 [READ]() {
438 let threw = true;
439 try {
440 const { fd, buf, offset, length, pos } = this;
441 /* c8 ignore start */
442 if (fd === undefined || buf === undefined) {
443 throw new Error('fd and buf must be set in READ method');
444 }
445 /* c8 ignore stop */
446 const bytesRead = fs.readSync(fd, buf, offset, length, pos);
447 this[ONREAD](bytesRead);
448 threw = false;
449 }
450 finally {
451 // ignoring the error from close(2) is a bad practice, but at
452 // this point we already have an error, don't need another one
453 if (threw) {
454 try {
455 this[CLOSE](() => { });
456 }
457 catch { }
458 }
459 }
460 }
461 [AWAITDRAIN](cb) {
462 cb();
463 }
464 /* c8 ignore start */
465 [CLOSE](cb = () => { }) {
466 /* c8 ignore stop */
467 if (this.fd !== undefined)
468 fs.closeSync(this.fd);
469 cb();
470 }
471}
472export class WriteEntryTar extends Minipass {
473 blockLen = 0;
474 blockRemain = 0;
475 buf = 0;
476 pos = 0;
477 remain = 0;
478 length = 0;
479 preservePaths;
480 portable;
481 strict;
482 noPax;
483 noMtime;
484 readEntry;
485 type;
486 prefix;
487 path;
488 mode;
489 uid;
490 gid;
491 uname;
492 gname;
493 header;
494 mtime;
495 atime;
496 ctime;
497 linkpath;
498 size;
499 onWriteEntry;
500 warn(code, message, data = {}) {
501 return warnMethod(this, code, message, data);
502 }
503 constructor(readEntry, opt_ = {}) {
504 const opt = dealias(opt_);
505 super();
506 this.preservePaths = !!opt.preservePaths;
507 this.portable = !!opt.portable;
508 this.strict = !!opt.strict;
509 this.noPax = !!opt.noPax;
510 this.noMtime = !!opt.noMtime;
511 this.onWriteEntry = opt.onWriteEntry;
512 this.readEntry = readEntry;
513 const { type } = readEntry;
514 /* c8 ignore start */
515 if (type === 'Unsupported') {
516 throw new Error('writing entry that should be ignored');
517 }
518 /* c8 ignore stop */
519 this.type = type;
520 if (this.type === 'Directory' && this.portable) {
521 this.noMtime = true;
522 }
523 this.prefix = opt.prefix;
524 this.path = normalizeWindowsPath(readEntry.path);
525 this.mode =
526 readEntry.mode !== undefined ? this[MODE](readEntry.mode) : undefined;
527 this.uid = this.portable ? undefined : readEntry.uid;
528 this.gid = this.portable ? undefined : readEntry.gid;
529 this.uname = this.portable ? undefined : readEntry.uname;
530 this.gname = this.portable ? undefined : readEntry.gname;
531 this.size = readEntry.size;
532 this.mtime = this.noMtime ? undefined : opt.mtime || readEntry.mtime;
533 this.atime = this.portable ? undefined : readEntry.atime;
534 this.ctime = this.portable ? undefined : readEntry.ctime;
535 this.linkpath =
536 readEntry.linkpath !== undefined ?
537 normalizeWindowsPath(readEntry.linkpath)
538 : undefined;
539 if (typeof opt.onwarn === 'function') {
540 this.on('warn', opt.onwarn);
541 }
542 let pathWarn = false;
543 if (!this.preservePaths) {
544 const [root, stripped] = stripAbsolutePath(this.path);
545 if (root && typeof stripped === 'string') {
546 this.path = stripped;
547 pathWarn = root;
548 }
549 }
550 this.remain = readEntry.size;
551 this.blockRemain = readEntry.startBlockSize;
552 this.onWriteEntry?.(this);
553 this.header = new Header({
554 path: this[PREFIX](this.path),
555 linkpath: this.type === 'Link' && this.linkpath !== undefined ?
556 this[PREFIX](this.linkpath)
557 : this.linkpath,
558 // only the permissions and setuid/setgid/sticky bitflags
559 // not the higher-order bits that specify file type
560 mode: this.mode,
561 uid: this.portable ? undefined : this.uid,
562 gid: this.portable ? undefined : this.gid,
563 size: this.size,
564 mtime: this.noMtime ? undefined : this.mtime,
565 type: this.type,
566 uname: this.portable ? undefined : this.uname,
567 atime: this.portable ? undefined : this.atime,
568 ctime: this.portable ? undefined : this.ctime,
569 });
570 if (pathWarn) {
571 this.warn('TAR_ENTRY_INFO', `stripping ${pathWarn} from absolute path`, {
572 entry: this,
573 path: pathWarn + this.path,
574 });
575 }
576 if (this.header.encode() && !this.noPax) {
577 super.write(new Pax({
578 atime: this.portable ? undefined : this.atime,
579 ctime: this.portable ? undefined : this.ctime,
580 gid: this.portable ? undefined : this.gid,
581 mtime: this.noMtime ? undefined : this.mtime,
582 path: this[PREFIX](this.path),
583 linkpath: this.type === 'Link' && this.linkpath !== undefined ?
584 this[PREFIX](this.linkpath)
585 : this.linkpath,
586 size: this.size,
587 uid: this.portable ? undefined : this.uid,
588 uname: this.portable ? undefined : this.uname,
589 dev: this.portable ? undefined : this.readEntry.dev,
590 ino: this.portable ? undefined : this.readEntry.ino,
591 nlink: this.portable ? undefined : this.readEntry.nlink,
592 }).encode());
593 }
594 const b = this.header?.block;
595 /* c8 ignore start */
596 if (!b)
597 throw new Error('failed to encode header');
598 /* c8 ignore stop */
599 super.write(b);
600 readEntry.pipe(this);
601 }
602 [PREFIX](path) {
603 return prefixPath(path, this.prefix);
604 }
605 [MODE](mode) {
606 return modeFix(mode, this.type === 'Directory', this.portable);
607 }
608 write(chunk, encoding, cb) {
609 /* c8 ignore start - just junk to comply with NodeJS.WritableStream */
610 if (typeof encoding === 'function') {
611 cb = encoding;
612 encoding = undefined;
613 }
614 if (typeof chunk === 'string') {
615 chunk = Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8');
616 }
617 /* c8 ignore stop */
618 const writeLen = chunk.length;
619 if (writeLen > this.blockRemain) {
620 throw new Error('writing more to entry than is appropriate');
621 }
622 this.blockRemain -= writeLen;
623 return super.write(chunk, cb);
624 }
625 end(chunk, encoding, cb) {
626 if (this.blockRemain) {
627 super.write(Buffer.alloc(this.blockRemain));
628 }
629 /* c8 ignore start - just junk to comply with NodeJS.WritableStream */
630 if (typeof chunk === 'function') {
631 cb = chunk;
632 encoding = undefined;
633 chunk = undefined;
634 }
635 if (typeof encoding === 'function') {
636 cb = encoding;
637 encoding = undefined;
638 }
639 if (typeof chunk === 'string') {
640 chunk = Buffer.from(chunk, encoding ?? 'utf8');
641 }
642 if (cb)
643 this.once('finish', cb);
644 if (chunk)
645 super.end(chunk, cb);
646 else
647 super.end(cb);
648 /* c8 ignore stop */
649 return this;
650 }
651}
652const getType = (stat) => stat.isFile() ? 'File'
653 : stat.isDirectory() ? 'Directory'
654 : stat.isSymbolicLink() ? 'SymbolicLink'
655 : 'Unsupported';
656//# sourceMappingURL=write-entry.js.map