Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
write-entry.js657 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 =
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
codekingpro/portable-devtools · Team Ai