Skip to content

Commit b6162c7

Browse files
committedAug 12, 2021
fix: reserve paths properly for unicode, windows
This updates the path reservation system such that it will properly await any paths that match based on unicode normalization. On windows, because 8.3 shortnames can collide in ways that are undetectable by any reasonable means, all unpack parallelization is simply disabled.
1 parent 3aaf19b commit b6162c7

File tree

2 files changed

+118
-5
lines changed

2 files changed

+118
-5
lines changed
 

‎lib/path-reservations.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88

99
const assert = require('assert')
1010
const normPath = require('./normalize-windows-path.js')
11+
const stripSlashes = require('./strip-trailing-slashes.js')
1112
const { join } = require('path')
1213

14+
const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
15+
const isWindows = platform === 'win32'
16+
1317
module.exports = () => {
1418
// path => [function or Set]
1519
// A Set object means a directory reservation
@@ -20,10 +24,16 @@ module.exports = () => {
2024
const reservations = new Map()
2125

2226
// return a set of parent dirs for a given path
23-
const getDirs = path =>
24-
path.split('/').slice(0, -1).reduce((set, path) =>
25-
set.length ? set.concat(normPath(join(set[set.length - 1], path)))
26-
: [path], [])
27+
// '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d']
28+
const getDirs = path => {
29+
const dirs = path.split('/').slice(0, -1).reduce((set, path) => {
30+
if (set.length)
31+
path = normPath(join(set[set.length - 1], path))
32+
set.push(path || '/')
33+
return set
34+
}, [])
35+
return dirs
36+
}
2737

2838
// functions currently running
2939
const running = new Set()
@@ -99,7 +109,18 @@ module.exports = () => {
99109
}
100110

101111
const reserve = (paths, fn) => {
102-
paths = paths.map(p => normPath(join(p)).toLowerCase())
112+
// collide on matches across case and unicode normalization
113+
// On windows, thanks to the magic of 8.3 shortnames, it is fundamentally
114+
// impossible to determine whether two paths refer to the same thing on
115+
// disk, without asking the kernel for a shortname.
116+
// So, we just pretend that every path matches every other path here,
117+
// effectively removing all parallelization on windows.
118+
paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
119+
return stripSlashes(normPath(join(p)))
120+
.normalize('NFKD')
121+
.toLowerCase()
122+
})
123+
103124
const dirs = new Set(
104125
paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b))
105126
)

‎test/path-reservations.js

+92
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
const t = require('tap')
2+
3+
// load up the posix and windows versions of the reserver
4+
if (process.platform === 'win32')
5+
process.env.TESTING_TAR_FAKE_PLATFORM = 'posix'
26
const { reserve } = require('../lib/path-reservations.js')()
7+
delete process.env.TESTING_TAR_FAKE_PLATFORM
8+
if (process.platform !== 'win32')
9+
process.env.TESTING_TAR_FAKE_PLATFORM = 'win32'
10+
const { reserve: winReserve } = t.mock('../lib/path-reservations.js')()
311

412
t.test('basic race', t => {
513
// simulate the race conditions we care about
@@ -54,3 +62,87 @@ t.test('basic race', t => {
5462
t.notOk(reserve(['a/b'], dir2), 'dir2 waits')
5563
t.notOk(reserve(['a/b/x'], dir3), 'dir3 waits')
5664
})
65+
66+
t.test('unicode shenanigans', t => {
67+
const e1 = Buffer.from([0xc3, 0xa9])
68+
const e2 = Buffer.from([0x65, 0xcc, 0x81])
69+
let didCafe1 = false
70+
const cafe1 = done => {
71+
t.equal(didCafe1, false, 'did cafe1 only once')
72+
t.equal(didCafe2, false, 'did cafe1 before cafe2')
73+
didCafe1 = true
74+
setTimeout(done)
75+
}
76+
let didCafe2 = false
77+
const cafe2 = done => {
78+
t.equal(didCafe1, true, 'did cafe1 before cafe2')
79+
t.equal(didCafe2, false, 'did cafe2 only once')
80+
didCafe2 = true
81+
done()
82+
t.end()
83+
}
84+
const cafePath1 = `c/a/f/${e1}`
85+
const cafePath2 = `c/a/f/${e2}`
86+
t.ok(reserve([cafePath1], cafe1))
87+
t.notOk(reserve([cafePath2], cafe2))
88+
})
89+
90+
t.test('absolute paths and trailing slash', t => {
91+
let calledA1 = false
92+
let calledA2 = false
93+
const a1 = done => {
94+
t.equal(calledA1, false, 'called a1 only once')
95+
t.equal(calledA2, false, 'called a1 before 2')
96+
calledA1 = true
97+
setTimeout(done)
98+
}
99+
const a2 = done => {
100+
t.equal(calledA1, true, 'called a1 before 2')
101+
t.equal(calledA2, false, 'called a2 only once')
102+
calledA2 = true
103+
done()
104+
if (calledR2)
105+
t.end()
106+
}
107+
let calledR1 = false
108+
let calledR2 = false
109+
const r1 = done => {
110+
t.equal(calledR1, false, 'called r1 only once')
111+
t.equal(calledR2, false, 'called r1 before 2')
112+
calledR1 = true
113+
setTimeout(done)
114+
}
115+
const r2 = done => {
116+
t.equal(calledR1, true, 'called r1 before 2')
117+
t.equal(calledR2, false, 'called r1 only once')
118+
calledR2 = true
119+
done()
120+
if (calledA2)
121+
t.end()
122+
}
123+
t.ok(reserve(['/p/a/t/h'], a1))
124+
t.notOk(reserve(['/p/a/t/h/'], a2))
125+
t.ok(reserve(['p/a/t/h'], r1))
126+
t.notOk(reserve(['p/a/t/h/'], r2))
127+
})
128+
129+
t.test('on windows, everything collides with everything', t => {
130+
const reserve = winReserve
131+
let called1 = false
132+
let called2 = false
133+
const f1 = done => {
134+
t.equal(called1, false, 'only call 1 once')
135+
t.equal(called2, false, 'call 1 before 2')
136+
called1 = true
137+
setTimeout(done)
138+
}
139+
const f2 = done => {
140+
t.equal(called1, true, 'call 1 before 2')
141+
t.equal(called2, false, 'only call 2 once')
142+
called2 = true
143+
done()
144+
t.end()
145+
}
146+
t.equal(reserve(['some/path'], f1), true)
147+
t.equal(reserve(['other/path'], f2), false)
148+
})

0 commit comments

Comments
 (0)