diff --git a/src/write-entry.ts b/src/write-entry.ts index 36fa276e..9d6eef45 100644 --- a/src/write-entry.ts +++ b/src/write-entry.ts @@ -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) { diff --git a/test/write-entry.js b/test/write-entry.js index afd0a063..38864568 100644 --- a/test/write-entry.js +++ b/test/write-entry.js @@ -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'