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