Skip to content

Commit bb21c9d

Browse files
authored
fix: Avoid blowing the call stack when processing many files (#133)
1 parent cf8b197 commit bb21c9d

File tree

4 files changed

+75
-71
lines changed

4 files changed

+75
-71
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ typings/
6565

6666
# Test results
6767
test.xunit
68+
69+
# Massive, generated directory
70+
test/fixtures/too-many/

index.js

Lines changed: 41 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ var globParent = require('glob-parent');
1212
var normalizePath = require('normalize-path');
1313
var isNegatedGlob = require('is-negated-glob');
1414
var toAbsoluteGlob = require('@gulpjs/to-absolute-glob');
15-
var mapSeries = require('now-and-later').mapSeries;
1615

1716
var globErrMessage1 = 'File not found with singular glob: ';
1817
var globErrMessage2 = ' (if this was purposeful, use `allowEmpty` option)';
@@ -24,29 +23,14 @@ function isFound(glob) {
2423
return isGlob(glob);
2524
}
2625

27-
function getSymlinkInfo(filepath, cb) {
28-
fs.realpath(filepath, function (err, realPath) {
29-
if (err) return cb(err);
30-
31-
fs.lstat(realPath, function (err, lstat) {
32-
if (err) return cb(err);
33-
34-
cb(null, {
35-
destinationPath: realPath,
36-
destinationStat: lstat,
37-
});
38-
});
39-
});
40-
}
41-
4226
function walkdir() {
4327
var readdirOpts = {
4428
withFileTypes: true,
4529
};
4630

4731
var ee = new EventEmitter();
4832

49-
var queue = fastq(process, 1);
33+
var queue = fastq(onAction, 1);
5034
queue.drain = function () {
5135
ee.emit('end');
5236
};
@@ -69,10 +53,7 @@ function walkdir() {
6953
};
7054
ee.walk = walk;
7155
ee.exists = exists;
72-
73-
function isDefined(value) {
74-
return typeof value !== 'undefined';
75-
}
56+
ee.resolve = resolve;
7657

7758
function walk(path) {
7859
queue.push({ action: 'walk', path: path });
@@ -82,11 +63,41 @@ function walkdir() {
8263
queue.push({ action: 'exists', path: path });
8364
}
8465

85-
function process(data, cb) {
66+
function resolve(path) {
67+
queue.push({ action: 'resolve', path: path });
68+
}
69+
70+
function resolveSymlink(symlinkPath, cb) {
71+
fs.realpath(symlinkPath, function (err, realpath) {
72+
if (err) {
73+
return cb(err);
74+
}
75+
76+
fs.lstat(realpath, function (err, stat) {
77+
if (err) {
78+
return cb(err);
79+
}
80+
81+
if (stat.isDirectory() && !symlinkPath.startsWith(realpath + path.sep)) {
82+
walk(symlinkPath);
83+
}
84+
85+
cb();
86+
})
87+
});
88+
}
89+
90+
function onAction(data, cb) {
8691
if (data.action === 'walk') {
87-
fs.readdir(data.path, readdirOpts, onReaddir);
88-
} else {
89-
fs.stat(data.path, onStat);
92+
return fs.readdir(data.path, readdirOpts, onReaddir);
93+
}
94+
95+
if (data.action === 'exists') {
96+
return fs.stat(data.path, onStat);
97+
}
98+
99+
if (data.action === 'resolve') {
100+
return resolveSymlink(data.path, cb);
90101
}
91102

92103
function onStat(err, stat) {
@@ -106,48 +117,22 @@ function walkdir() {
106117
return cb(err);
107118
}
108119

109-
mapSeries(dirents, processDirents, function (err, dirs) {
110-
if (err) {
111-
return cb(err);
112-
}
120+
dirents.forEach(processDirent);
113121

114-
dirs.filter(isDefined).forEach(walk);
115-
116-
cb();
117-
});
122+
cb();
118123
}
119124

120-
function processDirents(dirent, key, cb) {
125+
function processDirent(dirent) {
121126
var nextpath = path.join(data.path, dirent.name);
122127
ee.emit('path', nextpath, dirent);
123128

124129
if (dirent.isDirectory()) {
125-
cb(null, nextpath);
126-
127-
return;
130+
return walk(nextpath);
128131
}
129132

130133
if (dirent.isSymbolicLink()) {
131-
// If it's a symlink, check if the symlink points to a directory
132-
getSymlinkInfo(nextpath, function (err, info) {
133-
if (err) {
134-
return cb(err);
135-
}
136-
137-
if (
138-
info.destinationStat.isDirectory() &&
139-
!nextpath.startsWith(info.destinationPath + path.sep) // don't follow circular symlinks
140-
) {
141-
cb(null, nextpath);
142-
} else {
143-
cb();
144-
}
145-
});
146-
147-
return;
134+
return resolve(nextpath);
148135
}
149-
150-
cb();
151136
}
152137
}
153138

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"is-glob": "^4.0.3",
3131
"is-negated-glob": "^1.0.0",
3232
"normalize-path": "^3.0.0",
33-
"now-and-later": "^3.0.0",
3433
"streamx": "^2.12.5"
3534
},
3635
"devDependencies": {

test/index.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,37 @@ function suite(moduleName) {
370370
stream.pipeline([globStream(globs, { cwd: dir }), concat(assert)], done);
371371
});
372372

373+
// By default, we only run this in non-Windows CI since it takes so long
374+
it('does not stack overflow if there are an insane amount of files', function (done) {
375+
if (process.env.CI !== "true" || os.platform() === 'win32') {
376+
this.skip();
377+
}
378+
379+
this.timeout(0);
380+
var largeDir = path.join(dir, 'fixtures/too-many');
381+
fs.mkdirSync(largeDir, { recursive: true });
382+
383+
for (var i = 0; i < 100000; i++) {
384+
fs.writeFileSync(path.join(largeDir, 'file-' + i + '.txt'), "test-" + i)
385+
}
386+
387+
function assert(pathObjs) {
388+
for (var i = 0; i < 100000; i++) {
389+
fs.unlinkSync(path.join(largeDir, 'file-' + i + '.txt'))
390+
}
391+
fs.rmdirSync(largeDir);
392+
393+
expect(pathObjs.length).toEqual(100000);
394+
}
395+
396+
var glob = deWindows(largeDir) + '/*.txt';
397+
398+
stream.pipeline([globStream(glob), concat(assert)], done);
399+
});
400+
373401
it('emits all objects (unordered) when given multiple absolute paths and no cwd', function (done) {
374-
var testFile = path.join(os.tmpdir(), "glob-stream-test.txt");
375-
fs.writeFileSync(testFile, "test");
402+
var testFile = path.join(os.tmpdir(), 'glob-stream-test.txt');
403+
fs.writeFileSync(testFile, 'test');
376404

377405
var tmp = deWindows(os.tmpdir());
378406

@@ -408,7 +436,7 @@ function suite(moduleName) {
408436
];
409437

410438
function assert(pathObjs) {
411-
fs.unlinkSync(testFile, "test");
439+
fs.unlinkSync(testFile, 'test');
412440
expect(pathObjs.length).toEqual(4);
413441
expect(pathObjs).toContainEqual(expected[0]);
414442
expect(pathObjs).toContainEqual(expected[1]);
@@ -818,7 +846,6 @@ function suite(moduleName) {
818846
dir +
819847
'/fixtures/symlinks/symlink-dest/hey/isaidhey/whatsgoingon/test.txt',
820848
},
821-
822849
{
823850
cwd: dir,
824851
base: dir + '/fixtures/symlinks',
@@ -829,7 +856,6 @@ function suite(moduleName) {
829856
base: dir + '/fixtures/symlinks',
830857
path: dir + '/fixtures/symlinks/folder-b/folder-b-file.txt',
831858
},
832-
833859
// It should follow these circular symlinks, but not infinitely
834860
{
835861
cwd: dir,
@@ -841,15 +867,6 @@ function suite(moduleName) {
841867
base: dir + '/fixtures/symlinks',
842868
path: dir + '/fixtures/symlinks/folder-b/link-to-a/folder-a-file.txt',
843869
},
844-
845-
// And it should follow a symlink to a parent directory (circular symlink) without blowing up
846-
{
847-
cwd: dir,
848-
base: dir + '/fixtures/symlinks',
849-
path:
850-
dir +
851-
'/fixtures/symlinks/symlink-dest/hey/isaidhey/whatsgoingon/test.txt',
852-
},
853870
];
854871

855872
function assert(pathObjs) {

0 commit comments

Comments
 (0)