Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/write-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,13 @@ export class WriteEntry
throw new Error('cannot create file entry without stat')
}
/* c8 ignore stop */
if (this.stat.nlink > 1) {
// Windows file indexes are 64 bits wide, but fs.Stats reports ino as a
// double, so any value above Number.MAX_SAFE_INTEGER may be shared by
// several distinct files. Such an ino cannot identify a file, and using
// it as a linkCache key archives unrelated files as hardlinks to one
// another, silently dropping their contents. When we cannot tell files
// apart, treat the file as unlinked rather than guess.
if (this.stat.nlink > 1 && Number.isSafeInteger(this.stat.ino)) {
const linkKey = `${this.stat.dev}:${this.stat.ino}` as LinkCacheKey
const linkpath = this.linkCache.get(linkKey)
if (linkpath?.indexOf(this.cwd) === 0) {
Expand Down
30 changes: 30 additions & 0 deletions test/write-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,36 @@ t.test('hardlinks far away', t => {
})
})

// fs.Stats reports a 64-bit Windows file index as a double, so distinct
// files can share this ino.
const unsafeIno = 9570149211882252

t.test('unsafe ino is not used to identify hardlinks', t => {
t.teardown(
mutateFS.statMutate((_er, st) => {
if (st) {
st.dev = 204880295
st.ino = unsafeIno
st.nlink = 2
}
}),
)

const linkCache = new Map()
new WriteEntrySync('one-byte.txt', { cwd: files, linkCache })
const ws = new WriteEntrySync('512-bytes.txt', { cwd: files, linkCache })

t.equal(ws.type, 'File', 'unrelated file is not archived as a hardlink')
t.equal(ws.linkpath, undefined)
t.equal(ws.stat.size, 512, 'contents are still packed')
t.equal(
linkCache.size,
0,
'an ino that cannot identify a file is not cached',
)
t.end()
})

t.test('really deep path', t => {
const f =
'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
Expand Down