Skip to content

Commit ce5148e

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 3f2e2da commit ce5148e

14 files changed

+1050
-671
lines changed

lib/mkdir.js

+32-30
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
@@ -74,16 +83,12 @@ module.exports = (dir, opt, cb) => {
7483
return done()
7584

7685
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-
})
86+
return checkCwd(dir, done)
8287

8388
if (preserve)
8489
return mkdirp(dir, mode, done)
8590

86-
const sub = path.relative(cwd, dir)
91+
const sub = normPath(path.relative(cwd, dir))
8792
const parts = sub.split('/')
8893
mkdir_(cwd, parts, mode, cache, unlink, cwd, null, done)
8994
}
@@ -92,22 +97,19 @@ const mkdir_ = (base, parts, mode, cache, unlink, cwd, created, cb) => {
9297
if (!parts.length)
9398
return cb(null, created)
9499
const p = parts.shift()
95-
const part = base + '/' + p
100+
const part = normPath(path.resolve(base + '/' + p))
96101
if (cGet(cache, part))
97102
return mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
98103
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cwd, created, cb))
99104
}
100105

101106
const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
102107
if (er) {
103-
if (er.path && path.dirname(er.path) === cwd &&
104-
(er.code === 'ENOTDIR' || er.code === 'ENOENT'))
105-
return cb(new CwdError(cwd, er.code))
106-
107108
fs.lstat(part, (statEr, st) => {
108-
if (statEr)
109+
if (statEr) {
110+
statEr.path = statEr.path && normPath(statEr.path)
109111
cb(statEr)
110-
else if (st.isDirectory())
112+
} else if (st.isDirectory())
111113
mkdir_(part, parts, mode, cache, unlink, cwd, created, cb)
112114
else if (unlink)
113115
fs.unlink(part, er => {
@@ -126,6 +128,19 @@ const onmkdir = (part, parts, mode, cache, unlink, cwd, created, cb) => er => {
126128
}
127129
}
128130

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+
129144
module.exports.sync = (dir, opt) => {
130145
dir = normPath(dir)
131146
// if there's any overlap between mask and mode,
@@ -157,29 +172,20 @@ module.exports.sync = (dir, opt) => {
157172
return done()
158173

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

174179
if (preserve)
175180
return done(mkdirp.sync(dir, mode))
176181

177-
const sub = path.relative(cwd, dir)
182+
const sub = normPath(path.relative(cwd, dir))
178183
const parts = sub.split('/')
179184
let created = null
180185
for (let p = parts.shift(), part = cwd;
181186
p && (part += '/' + p);
182187
p = parts.shift()) {
188+
part = normPath(path.resolve(part))
183189
if (cGet(cache, part))
184190
continue
185191

@@ -188,10 +194,6 @@ module.exports.sync = (dir, opt) => {
188194
created = created || part
189195
cSet(cache, part, true)
190196
} catch (er) {
191-
if (er.path && path.dirname(er.path) === cwd &&
192-
(er.code === 'ENOTDIR' || er.code === 'ENOENT'))
193-
return new CwdError(cwd, er.code)
194-
195197
const st = fs.lstatSync(part)
196198
if (st.isDirectory()) {
197199
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,6 +1,7 @@
11
'use strict'
22
const types = require('./types.js')
33
const MiniPass = require('minipass')
4+
const normPath = require('./normalize-windows-path.js')
45

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

50-
this.path = header.path
51+
this.path = normPath(header.path)
5152
this.mode = header.mode
5253
if (this.mode)
5354
this.mode = this.mode & 0o7777
@@ -59,7 +60,7 @@ module.exports = class ReadEntry extends MiniPass {
5960
this.mtime = header.mtime
6061
this.atime = header.atime
6162
this.ctime = header.ctime
62-
this.linkpath = header.linkpath
63+
this.linkpath = normPath(header.linkpath)
6364
this.uname = header.uname
6465
this.gname = header.gname
6566

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

lib/replace.js

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

169169
fs.fstat(fd, (er, st) => {
170170
if (er)
171-
return reject(er)
171+
return fs.close(fd, () => reject(er))
172+
172173
getPos(fd, st.size, (er, position) => {
173174
if (er)
174175
return reject(er)

0 commit comments

Comments
 (0)