Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The readdirSync and readdir functions both have a bug when one filesystem has an empty directory #742

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/__tests__/union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,87 @@ describe('union', () => {
expect(ufs.readdirSync('/bar')).toEqual(['baz', 'qux']);
});

it('readdirSync reads other fss when one fails -- in both orders, and when directory is empty', () => {
const vol = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
vol2.mkdirSync('/b')

const ufs = new Union();
ufs.use(vol as any);
ufs.use(vol2 as any);
expect(ufs.readdirSync('/b')).toEqual([]);

const ufs2 = new Union();
ufs2.use(vol2 as any);
ufs2.use(vol as any);
expect(ufs2.readdirSync('/b')).toEqual([]);
});

it('readdir reads other fss when one fails, and correctly handles case when either directory is empty', () => {
const vol = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
vol2.mkdirSync('/b')

const ufs = new Union();
ufs.use(vol as any);
ufs.use(vol2 as any);
expect(ufs.readdir('/b', (err, files) => {
expect(files).toEqual([])
}));

const ufs2 = new Union();
ufs2.use(vol2 as any);
ufs2.use(vol as any);
expect(ufs.readdir('/b', (err, files) => {
expect(files).toEqual([])
}));

});

it('readdirSync on a file should raise ENOTDIR instead of ENOENT', () => {
const vol = Volume.fromJSON({
'/foo/bar': 'bar'
});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol as any);
ufs.use(vol2 as any);
expect(()=>ufs.readdirSync("/foo/bar")).toThrow('ENOTDIR');

const ufs2 = new Union();
ufs2.use(vol2 as any);
ufs2.use(vol as any);
expect(()=>ufs2.readdirSync("/foo/bar")).toThrow('ENOTDIR');
// really not there:
expect(()=>ufs2.readdirSync("/foo/bar2")).toThrow('ENOENT');
});

describe('readdir on a file should raise ENOTDIR instead of ENOENT', () => {
const vol = Volume.fromJSON({
'/foo/bar': 'bar'
});
const vol2 = Volume.fromJSON({});
it("throws correct error", done => {
const ufs = new Union();
ufs.use(vol as any);
ufs.use(vol2 as any);
ufs.readdir("/foo/bar", (err) => {
expect(err?.code).toBe('ENOTDIR');
done();
});
});

it("throw correct error in other order", done => {
const ufs2 = new Union();
ufs2.use(vol2 as any);
ufs2.use(vol as any);
ufs2.readdir("/foo/bar", (err) => {
expect(err?.code).toBe('ENOTDIR');
done();
});
});
});

it('honors the withFileTypes: true option', () => {
const vol = Volume.fromJSON({
'/foo/bar': 'bar',
Expand Down
49 changes: 44 additions & 5 deletions src/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const SPECIAL_METHODS = new Set([
'unwatchFile',
]);

const SPECIAL_ERRORS = new Set(["ENOTDIR", "EEXIST"]);

const createFSProxy = (watchers: FSWatcher[]) =>
new Proxy(
{},
Expand Down Expand Up @@ -177,23 +179,39 @@ export class Union {

let lastError: IUnionFsError | null = null;
let result = new Map<string, readdirEntry>();
let pathExists = false;
const iterate = (i = 0, error?: IUnionFsError | null) => {
if (error) {
if (SPECIAL_ERRORS.has(error["code"])) {
// Immediately fail with this error.
// see comment in readdirSync
if (cb) {
cb(error);
}
return;
}
error.prev = lastError;
lastError = error;
}

// Already tried all file systems, return the last error.
// Already tried all file systems; return the last error if every attempt failed.
if (i >= this.fss.length) {
// last one
if (cb) {
cb(error || Error('No file systems attached.'));
if(pathExists) {
cb(null, this.sortedArrayFromReaddirResult(result));
} else {
cb(error || Error('No file systems attached.'));
}
}
return;
}

// Replace `callback` with our intermediate function.
args[lastarg] = (err, resArg: readdirEntry[]) => {
if(!err) {
pathExists = true;
}
if (result.size === 0 && err) {
return iterate(i + 1, err);
}
Expand Down Expand Up @@ -223,18 +241,30 @@ export class Union {
public readdirSync = (...args): Array<readdirEntry> => {
let lastError: IUnionFsError | null = null;
let result = new Map<string, readdirEntry>();
let pathExists = false;
for (let i = this.fss.length - 1; i >= 0; i--) {
const fs = this.fss[i];
try {
if (!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`);
if (!fs.readdirSync)
throw Error(
`Method not supported: "readdirSync" with args "${args}"`
);
for (const res of fs.readdirSync.apply(fs, args)) {
result.set(this.pathFromReaddirEntry(res), res);
}
pathExists = true;
} catch (err) {
if (SPECIAL_ERRORS.has(err.code)) {
// The file *does* exist in this filesystem in the union, but ENOTDIR happened.
// E.g., if you try to get a directory listing on a file one fs doesn't have the file and the
// the other fs has the file, then the one that has it throws ENOTDIR, which is what this
// function should throw.
throw err;
}
err.prev = lastError;
lastError = err;
if (result.size === 0 && !i) {
// last one
if (!i && !pathExists) {
// Last one and the path didn't exist in any case above.
throw err;
} else {
// Ignore error...
Expand Down Expand Up @@ -356,6 +386,9 @@ export class Union {
if (!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`);
return fs[method].apply(fs, args);
} catch (err) {
if (SPECIAL_ERRORS.has(err["code"])) { // see comment in readdirSync
throw err;
}
err.prev = lastError;
lastError = err;
if (!i) {
Expand All @@ -379,6 +412,12 @@ export class Union {

let lastError: IUnionFsError | null = null;
const iterate = (i = 0, err?: IUnionFsError) => {
if (err != null && SPECIAL_ERRORS.has(err?.["code"])) { // see comment in readdirSync
if(cb) {
cb(err);
}
return;
}
if (err) {
err.prev = lastError;
lastError = err;
Expand Down
Loading