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 9, 2021
1 parent fd2a38d commit e29a665
Show file tree
Hide file tree
Showing 4 changed files with 659 additions and 25 deletions.
19 changes: 4 additions & 15 deletions lib/pack.js
Expand Up @@ -133,9 +133,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 @@ -152,9 +149,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,7 +292,8 @@ const Pack = warner(class Pack extends MiniPass {
linkCache: this.linkCache,
statCache: this.statCache,
noMtime: this.noMtime,
mtime: this.mtime
mtime: this.mtime,
prefix: this.prefix,
}
}

Expand All @@ -324,10 +319,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 @@ -380,10 +372,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
41 changes: 32 additions & 9 deletions lib/write-entry.js
Expand Up @@ -7,7 +7,13 @@ const ReadEntry = require('./read-entry.js')
const fs = require('fs')
const path = require('path')

const types = require('./types.js')
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 @@ -26,6 +32,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 @@ -53,6 +60,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 @@ -123,13 +131,19 @@ const WriteEntry = warner(class WriteEntry extends MiniPass {
return modeFix(mode, this.type === 'Directory')
}

[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 @@ -150,8 +164,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 +400,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 @@ -415,8 +432,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 @@ -436,8 +454,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 @@ -450,6 +469,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')
}
Expand Down
94 changes: 94 additions & 0 deletions test/pack.js
Expand Up @@ -1055,3 +1055,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 e29a665

Please sign in to comment.