Skip to content

Commit

Permalink
fix: properly prefix hard links
Browse files Browse the repository at this point in the history
This moves all the prefix-handling logic into the WriteEntry classes, where it belongs.

Fix: #284
  • Loading branch information
isaacs committed Aug 4, 2021
1 parent 94b2a74 commit bdf4f51
Show file tree
Hide file tree
Showing 4 changed files with 645 additions and 22 deletions.
17 changes: 3 additions & 14 deletions lib/pack.js
Expand Up @@ -134,9 +134,6 @@ const Pack = warner(class Pack extends MiniPass {

[ADDTARENTRY] (p) {
const absolute = path.resolve(this.cwd, p.path)
if (this.prefix)
p.path = this.prefix + '/' + p.path.replace(/^\.(\/+|$)/, '')

// in this case, we don't have to wait for the stat
if (!this.filter(p.path, p))
p.resume()
Expand All @@ -153,9 +150,6 @@ const Pack = warner(class Pack extends MiniPass {

[ADDFSENTRY] (p) {
const absolute = path.resolve(this.cwd, p)
if (this.prefix)
p = this.prefix + '/' + p.replace(/^\.(\/+|$)/, '')

this[QUEUE].push(new PackJob(p, absolute))
this[PROCESS]()
}
Expand Down Expand Up @@ -298,6 +292,7 @@ const Pack = warner(class Pack extends MiniPass {
statCache: this.statCache,
noMtime: this.noMtime,
mtime: this.mtime,
prefix: this.prefix,
}
}

Expand All @@ -323,10 +318,7 @@ const Pack = warner(class Pack extends MiniPass {

if (job.readdir) {
job.readdir.forEach(entry => {
const p = this.prefix ?
job.path.slice(this.prefix.length + 1) || './'
: job.path

const p = job.path
const base = p === './' ? '' : p.replace(/\/*$/, '/')
this[ADDFSENTRY](base + entry)
})
Expand Down Expand Up @@ -381,10 +373,7 @@ class PackSync extends Pack {

if (job.readdir) {
job.readdir.forEach(entry => {
const p = this.prefix ?
job.path.slice(this.prefix.length + 1) || './'
: job.path

const p = job.path
const base = p === './' ? '' : p.replace(/\/*$/, '/')
this[ADDFSENTRY](base + entry)
})
Expand Down
40 changes: 32 additions & 8 deletions lib/write-entry.js
Expand Up @@ -5,6 +5,13 @@ const Header = require('./header.js')
const fs = require('fs')
const path = require('path')

const prefixPath = (path, prefix) => {
if (!prefix)
return path
path = path.replace(/^\.([/\\]|$)/, '')
return prefix + '/' + path
}

const maxReadSize = 16 * 1024 * 1024
const PROCESS = Symbol('process')
const FILE = Symbol('file')
Expand All @@ -23,6 +30,7 @@ const CLOSE = Symbol('close')
const MODE = Symbol('mode')
const AWAITDRAIN = Symbol('awaitDrain')
const ONDRAIN = Symbol('ondrain')
const PREFIX = Symbol('prefix')
const warner = require('./warn-mixin.js')
const winchars = require('./winchars.js')
const stripAbsolutePath = require('./strip-absolute-path.js')
Expand Down Expand Up @@ -50,6 +58,7 @@ const WriteEntry = warner(class WriteEntry extends MiniPass {
this.noPax = !!opt.noPax
this.noMtime = !!opt.noMtime
this.mtime = opt.mtime || null
this.prefix = opt.prefix || null

this.fd = null
this.blockLen = null
Expand Down Expand Up @@ -128,13 +137,19 @@ const WriteEntry = warner(class WriteEntry extends MiniPass {
return modeFix(mode, this.type === 'Directory', this.portable)
}

[PREFIX] (path) {
return prefixPath(path, this.prefix)
}

[HEADER] () {
if (this.type === 'Directory' && this.portable)
this.noMtime = true

this.header = new Header({
path: this.path,
linkpath: this.linkpath,
path: this[PREFIX](this.path),
// only apply the prefix to hard links.
linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
: this.linkpath,
// only the permissions and setuid/setgid/sticky bitflags
// not the higher-order bits that specify file type
mode: this[MODE](this.stat.mode),
Expand All @@ -155,8 +170,9 @@ const WriteEntry = warner(class WriteEntry extends MiniPass {
ctime: this.portable ? null : this.header.ctime,
gid: this.portable ? null : this.header.gid,
mtime: this.noMtime ? null : this.mtime || this.header.mtime,
path: this.path,
linkpath: this.linkpath,
path: this[PREFIX](this.path),
linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
: this.linkpath,
size: this.header.size,
uid: this.portable ? null : this.header.uid,
uname: this.portable ? null : this.header.uname,
Expand Down Expand Up @@ -385,6 +401,8 @@ const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
if (this.type === 'Directory' && this.portable)
this.noMtime = true

this.prefix = opt.prefix || null

this.path = readEntry.path
this.mode = this[MODE](readEntry.mode)
this.uid = this.portable ? null : readEntry.uid
Expand Down Expand Up @@ -413,8 +431,9 @@ const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
this.blockRemain = readEntry.startBlockSize

this.header = new Header({
path: this.path,
linkpath: this.linkpath,
path: this[PREFIX](this.path),
linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
: this.linkpath,
// only the permissions and setuid/setgid/sticky bitflags
// not the higher-order bits that specify file type
mode: this.mode,
Expand All @@ -441,8 +460,9 @@ const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
ctime: this.portable ? null : this.ctime,
gid: this.portable ? null : this.gid,
mtime: this.noMtime ? null : this.mtime,
path: this.path,
linkpath: this.linkpath,
path: this[PREFIX](this.path),
linkpath: this.type === 'Link' ? this[PREFIX](this.linkpath)
: this.linkpath,
size: this.size,
uid: this.portable ? null : this.uid,
uname: this.portable ? null : this.uname,
Expand All @@ -456,6 +476,10 @@ const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
readEntry.pipe(this)
}

[PREFIX] (path) {
return prefixPath(path, this.prefix)
}

[MODE] (mode) {
return modeFix(mode, this.type === 'Directory', this.portable)
}
Expand Down
94 changes: 94 additions & 0 deletions test/pack.js
Expand Up @@ -1058,3 +1058,97 @@ t.test('prefix and subdirs', t => {
return t.test('./', t => runTest(t, './', Pack.Sync))
})
})

// https://github.com/npm/node-tar/issues/284
t.test('prefix and hard links', t => {
const dir = path.resolve(fixtures, 'pack-prefix-hardlinks')
t.teardown(_ => rimraf.sync(dir))
mkdirp.sync(dir + '/in/z/b/c')
fs.writeFileSync(dir + '/in/target', 'ddd')
fs.linkSync(dir + '/in/target', dir + '/in/z/b/c/d')
fs.linkSync(dir + '/in/target', dir + '/in/z/b/d')
fs.linkSync(dir + '/in/target', dir + '/in/z/d')
fs.linkSync(dir + '/in/target', dir + '/in/y')

const expect = [
'out/x/\0',
{
type: 'File',
size: 3,
path: 'out/x/target',
linkpath: '',
},
'ddd\0\0\0\0\0\0\0\0\0\0\0',
{
path: 'out/x/y',
type: 'Link',
linkpath: 'out/x/target',
},
'out/x/z/\0',
'out/x/z/b/\0',
{
path: 'out/x/z/d',
type: 'Link',
linkpath: 'out/x/target',
},
'out/x/z/b/c/\0',
{
path: 'out/x/z/b/d',
type: 'Link',
linkpath: 'out/x/target',
},
{
path: 'out/x/z/b/c/d',
type: 'Link',
linkpath: 'out/x/target',
},
'\0',
'\0',
]

const check = (out, t) => {
const data = Buffer.concat(out)
expect.forEach((e, i) => {
if (typeof e === 'string')
t.equal(data.slice(i * 512, i * 512 + e.length).toString(), e)
else
t.match(new Header(data.slice(i * 512, (i + 1) * 512)), e)
})
t.end()
}

const runTest = (t, path, Class) => {
const p = new Class({
cwd: dir + '/in',
prefix: 'out/x',
noDirRecurse: true,
})
const out = []
p.on('data', d => out.push(d))
p.on('end', () => check(out, t))
p.write(path)
if (path === '.')
path = './'
p.write(`${path}target`)
p.write(`${path}y`)
p.write(`${path}z`)
p.write(`${path}z/b`)
p.write(`${path}z/d`)
p.write(`${path}z/b/c`)
p.write(`${path}z/b/d`)
p.write(`${path}z/b/c/d`)
p.end()
}

t.test('async', t => {
t.test('.', t => runTest(t, '.', Pack))
return t.test('./', t => runTest(t, './', Pack))
})

t.test('sync', t => {
t.test('.', t => runTest(t, '.', Pack.Sync))
return t.test('./', t => runTest(t, './', Pack.Sync))
})

t.end()
})

0 comments on commit bdf4f51

Please sign in to comment.