Skip to content

Commit bb93ba2

Browse files
committed
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 2f1bca0 commit bb93ba2

File tree

2 files changed

+119
-5
lines changed

2 files changed

+119
-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

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

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

0 commit comments

Comments
 (0)