Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
write-entry.js656 linesDownload Raw Back to esm
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
codekingpro/portable-devtools · Team Ai