Skip to content

Commit c2a0948

Browse files
committed
fix: refactoring to pass tests on Windows
This is a larger refactoring than I tend to prefer to do in a single commit, but here goes. - The path normalization of \ to / is made more comprehensive. - Checking to ensure we aren't overwriting the cwd is done earlier in the unpack process, and more thoroughly, so there is less need for repetitive checks later. - The cwd is checked at the start in our recursive mkdir, saving an extra fs.mkdir call which would almost always result in an EEXIST. - Many edge cases resulting in dangling file descriptors were found and addressed. (Much as I complain about Windows stubbornly refusing to delete files currently open, it did come in handy here.) - The Unpack[MAKEFS] methods are refactored for readability, and no longer rely on fall-through behavior which made the sync and async versions slightly different in some edge cases. - Many of the tests were refactored to use async rimraf (the better to avoid Windows problems) and more modern tap affordances. Note: coverage on Windows is not 100%, due to skipping many tests that use symbolic links. Given the value of having those code paths covered, I believe that adding istanbul hints to skip coverage of those portions of the code would be a bad idea. And given the complexity and hazards involved in mocking that much of the filesystem implementation, it's probably best to just let Windows not have 100% coverage.
1 parent d0ce670 commit c2a0948

14 files changed

+950
-599
lines changed

lib/mkdir.js

+33-32
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@ class CwdError extends Error {
3737
const cGet = (cache, key) => cache.get(normPath(key))
3838
const cSet = (cache, key, val) => cache.set(normPath(key), val)
3939

40+
const checkCwd = (dir, cb) => {
41+
fs.stat(dir, (er, st) => {
42+
if (er || !st.isDirectory())
43+
er = new CwdError(dir, er && er.code || 'ENOTDIR')
44+
cb(er)
45+
})
46+
}
47+
4048
module.exports = (dir, opt, cb) => {
4149
dir = normPath(dir)
50+
4251
// if there's any overlap between mask and mode,
4352
// then we'll need an explicit chmod
4453
const umask = opt.umask
@@ -73,18 +82,13 @@ module.exports = (dir, opt, cb) => {
7382
if (cache && cGet(cache, dir) === true)
7483
return done()
7584

76-
if (dir === cwd) {
77-
return fs.stat(dir, (er, st) => {
78-
if (er || !st.isDirectory())
79-
er = new CwdError(dir, er && er.code || 'ENOTDIR')
80-
done(er)
81-
})
82-
}
85+
if (dir === cwd)
86+
return checkCwd(dir, done)
8387

8488
if (preserve)
8589
return mkdirp(dir, {mode}).then(made => done(null, made), done)
8690

87-
const sub = path.relative(cwd, dir)
91+
const sub = normPath(path.relative(cwd, dir))
8892
const parts = sub.split('/')
8993
mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done)
9094
}
@@ -93,22 +97,19 @@ const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => {
9397
if (!parts.length)
9498
return cb(null, created)
9599
const p = parts.shift()
96-
const part = base + '/' + p
100+
const part = normPath(path.resolve(base + '/' + p))
97101
if (cGet(cache, part))
98102
return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
99103
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
100104
}
101105

102106
const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
103107
if (er) {
104-
if (er.path && path.dirname(er.path) === cwd &&
105-
(er.code === 'ENOTDIR' || er.code === 'ENOENT'))
106-
return cb(new CwdError(cwd, er.code))
107-
108108
fs.lstat(part, (statEr, st) => {
109-
if (statEr)
109+
if (statEr) {
110+
statEr.path = statEr.path && normPath(statEr.path)
110111
cb(statEr)
111-
else if (st.isDirectory())
112+
} else if (st.isDirectory())
112113
mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
113114
else if (unlink) {
114115
fs.unlink(part, er => {
@@ -127,6 +128,19 @@ const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
127128
}
128129
}
129130

131+
const checkCwdSync = dir => {
132+
let ok = false
133+
let code = 'ENOTDIR'
134+
try {
135+
ok = fs.statSync(dir).isDirectory()
136+
} catch (er) {
137+
code = er.code
138+
} finally {
139+
if (!ok)
140+
throw new CwdError(dir, code)
141+
}
142+
}
143+
130144
module.exports.sync = (dir, opt) => {
131145
dir = normPath(dir)
132146
// if there's any overlap between mask and mode,
@@ -158,29 +172,20 @@ module.exports.sync = (dir, opt) => {
158172
return done()
159173

160174
if (dir === cwd) {
161-
let ok = false
162-
let code = 'ENOTDIR'
163-
try {
164-
ok = fs.statSync(dir).isDirectory()
165-
} catch (er) {
166-
code = er.code
167-
} finally {
168-
if (!ok)
169-
throw new CwdError(dir, code)
170-
}
171-
done()
172-
return
175+
checkCwdSync(cwd)
176+
return done()
173177
}
174178

175179
if (preserve)
176180
return done(mkdirp.sync(dir, mode))
177181

178-
const sub = path.relative(cwd, dir)
182+
const sub = normPath(path.relative(cwd, dir))
179183
const parts = sub.split('/')
180184
let created = null
181185
for (let p = parts.shift(), part = cwd;
182186
p && (part += '/' + p);
183187
p = parts.shift()) {
188+
part = normPath(path.resolve(part))
184189
if (cGet(cache, part))
185190
continue
186191

@@ -189,10 +194,6 @@ module.exports.sync = (dir, opt) => {
189194
created = created || part
190195
cSet(cache, part, true)
191196
} catch (er) {
192-
if (er.path && path.dirname(er.path) === cwd &&
193-
(er.code === 'ENOTDIR' || er.code === 'ENOENT'))
194-
return new CwdError(cwd, er.code)
195-
196197
const st = fs.lstatSync(part)
197198
if (st.isDirectory()) {
198199
cSet(cache, part, true)

lib/normalize-windows-path.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
77
module.exports = platform !== 'win32' ? p => p
8-
: p => p.replace(/\\/g, '/')
8+
: p => p && p.replace(/\\/g, '/')

lib/read-entry.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22
const MiniPass = require('minipass')
3+
const normPath = require('./normalize-windows-path.js')
34

45
const SLURP = Symbol('slurp')
56
module.exports = class ReadEntry extends MiniPass {
@@ -46,7 +47,7 @@ module.exports = class ReadEntry extends MiniPass {
4647
this.ignore = true
4748
}
4849

49-
this.path = header.path
50+
this.path = normPath(header.path)
5051
this.mode = header.mode
5152
if (this.mode)
5253
this.mode = this.mode & 0o7777
@@ -58,7 +59,7 @@ module.exports = class ReadEntry extends MiniPass {
5859
this.mtime = header.mtime
5960
this.atime = header.atime
6061
this.ctime = header.ctime
61-
this.linkpath = header.linkpath
62+
this.linkpath = normPath(header.linkpath)
6263
this.uname = header.uname
6364
this.gname = header.gname
6465

@@ -93,7 +94,7 @@ module.exports = class ReadEntry extends MiniPass {
9394
// a global extended header, because that's weird.
9495
if (ex[k] !== null && ex[k] !== undefined &&
9596
!(global && k === 'path'))
96-
this[k] = ex[k]
97+
this[k] = k === 'path' || k === 'linkpath' ? normPath(ex[k]) : ex[k]
9798
}
9899
}
99100
}

lib/replace.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ const replace = (opt, files, cb) => {
170170

171171
fs.fstat(fd, (er, st) => {
172172
if (er)
173-
return reject(er)
173+
return fs.close(fd, () => reject(er))
174+
174175
getPos(fd, st.size, (er, position) => {
175176
if (er)
176177
return reject(er)

0 commit comments

Comments
 (0)