diff --git a/lib/unpack.js b/lib/unpack.js index 3e1c527b..1c16d5d0 100644 --- a/lib/unpack.js +++ b/lib/unpack.js @@ -12,6 +12,7 @@ const wc = require('./winchars.js') const ONENTRY = Symbol('onEntry') const CHECKFS = Symbol('checkFs') +const ISREUSABLE = Symbol('isReusable') const MAKEFS = Symbol('makeFs') const FILE = Symbol('file') const DIRECTORY = Symbol('directory') @@ -351,6 +352,17 @@ class Unpack extends Parser { entry.resume() } + // Check if we can reuse an existing filesystem entry safely and + // overwrite it, rather than unlinking and recreating + // Windows doesn't report a useful nlink, so we just never reuse entries + [ISREUSABLE] (entry, st) { + return entry.type === 'File' && + !this.unlink && + st.isFile() && + st.nlink <= 1 && + process.platform !== 'win32' + } + // check if a thing is there, and if so, try to clobber it [CHECKFS] (entry) { this[PEND]() @@ -360,7 +372,7 @@ class Unpack extends Parser { fs.lstat(entry.absolute, (er, st) => { if (st && (this.keep || this.newer && st.mtime > entry.mtime)) this[SKIP](entry) - else if (er || (entry.type === 'File' && !this.unlink && st.isFile())) + else if (er || this[ISREUSABLE](entry, st)) this[MAKEFS](null, entry) else if (st.isDirectory()) { if (entry.type === 'Directory') { @@ -422,7 +434,7 @@ class UnpackSync extends Unpack { const st = fs.lstatSync(entry.absolute) if (this.keep || this.newer && st.mtime > entry.mtime) return this[SKIP](entry) - else if (entry.type === 'File' && !this.unlink && st.isFile()) + else if (this[ISREUSABLE](entry, st)) return this[MAKEFS](null, entry) else { try { diff --git a/test/unpack.js b/test/unpack.js index c7bf7911..20ca79ac 100644 --- a/test/unpack.js +++ b/test/unpack.js @@ -2286,3 +2286,66 @@ t.test('onentry option is preserved', t => { t.end() }) + +t.test('do not reuse hardlinks, only nlink=1 files', t => { + const basedir = path.resolve(unpackdir, 'hardlink-reuse') + mkdirp.sync(basedir) + t.teardown(() => rimraf.sync(basedir)) + + const now = new Date('2018-04-30T18:30:39.025Z') + + const data = makeTar([ + { + path: 'overwriteme', + type: 'File', + size: 4, + mode: 0o644, + mtime: now + }, + 'foo\n', + { + path: 'link', + linkpath: 'overwriteme', + type: 'Link', + mode: 0o644, + mtime: now + }, + { + path: 'link', + type: 'File', + size: 4, + mode: 0o644, + mtime: now + }, + 'bar\n', + '', + '' + ]) + + const checks = { + 'link': 'bar\n', + 'overwriteme': 'foo\n' + } + + const check = t => { + for (let f in checks) { + t.equal(fs.readFileSync(basedir + '/' + f, 'utf8'), checks[f], f) + t.equal(fs.statSync(basedir + '/' + f).nlink, 1, f) + } + t.end() + } + + t.test('async', t => { + const u = new Unpack({ cwd: basedir }) + u.on('close', () => check(t)) + u.end(data) + }) + + t.test('sync', t => { + const u = new UnpackSync({ cwd: basedir }) + u.end(data) + check(t) + }) + + t.end() +})