Skip to content

Commit 202605f

Browse files
committed
fs: support encoding option in glob
Add an `encoding` option to `fs.glob()`, `fs.globSync()`, and `fs.promises.glob()`. The glob walker already operates on raw path bytes; encode results at the native-to-JavaScript boundary instead so that filenames which are not valid UTF-8 can be returned without loss when `encoding` is `'buffer'`. With `withFileTypes`, the `name` and `parentPath` properties of each Dirent, as well as the values passed to `exclude` callbacks, use the requested encoding. Filesystem operations, including the lstat used for path callbacks, keep using the original paths. The default encoding remains `'utf8'`. Refs: #59202 Signed-off-by: XadillaX <i@2333.moe> Assisted-by: Trae
1 parent 1d5f8e6 commit 202605f

7 files changed

Lines changed: 437 additions & 56 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,9 @@ changes:
14611461
* `pattern` {string|string\[]}
14621462
* `options` {Object}
14631463
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
1464+
* `encoding` {string} Encoding for returned paths and paths passed to an
1465+
`exclude` function. Use `'buffer'` to preserve filename bytes.
1466+
**Default:** `'utf8'`.
14641467
* `exclude` {Function|string\[]} Function to filter out files/directories or a
14651468
list of [glob patterns][] to be excluded. If a function is provided, return
14661469
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
@@ -1478,6 +1481,11 @@ changes:
14781481
14791482
See [Glob patterns][] for the syntax `pattern` accepts.
14801483
1484+
When `encoding` is `'buffer'`, paths are returned as {Buffer} objects.
1485+
With `withFileTypes: true`, the `name` and `parentPath` properties of each
1486+
{fs.Dirent} use the specified encoding, including those passed to `exclude`.
1487+
The encoding does not change how patterns are matched.
1488+
14811489
When `followSymlinks` is enabled, detected symbolic link cycles are not
14821490
traversed recursively.
14831491
@@ -3744,6 +3752,9 @@ changes:
37443752
37453753
* `options` {Object}
37463754
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
3755+
* `encoding` {string} Encoding for returned paths and paths passed to an
3756+
`exclude` function. Use `'buffer'` to preserve filename bytes.
3757+
**Default:** `'utf8'`.
37473758
* `exclude` {Function|string\[]} Function to filter out files/directories or a
37483759
list of [glob patterns][] to be excluded. If a function is provided, return
37493760
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
@@ -3761,6 +3772,11 @@ changes:
37613772
37623773
See [Glob patterns][] for the syntax `pattern` accepts.
37633774
3775+
When `encoding` is `'buffer'`, paths are returned as {Buffer} objects.
3776+
With `withFileTypes: true`, the `name` and `parentPath` properties of each
3777+
{fs.Dirent} use the specified encoding, including those passed to `exclude`.
3778+
The encoding does not change how patterns are matched.
3779+
37643780
When `followSymlinks` is enabled, detected symbolic link cycles are not
37653781
traversed recursively.
37663782
@@ -6460,6 +6476,9 @@ changes:
64606476
* `pattern` {string|string\[]}
64616477
* `options` {Object}
64626478
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
6479+
* `encoding` {string} Encoding for returned paths and paths passed to an
6480+
`exclude` function. Use `'buffer'` to preserve filename bytes.
6481+
**Default:** `'utf8'`.
64636482
* `exclude` {Function|string\[]} Function to filter out files/directories or a
64646483
list of [glob patterns][] to be excluded. If a function is provided, return
64656484
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
@@ -6469,10 +6488,16 @@ changes:
64696488
The `cwd` directory has a depth of `0`. **Default:** `Infinity`.
64706489
* `withFileTypes` {boolean} `true` if the glob should return paths as Dirents,
64716490
`false` otherwise. **Default:** `false`.
6472-
* Returns: {string\[]} paths of files that match the pattern.
6491+
* Returns: {string\[]|Buffer\[]|fs.Dirent\[]} paths or directory entries that
6492+
match the pattern.
64736493
64746494
See [Glob patterns][] for the syntax `pattern` accepts.
64756495
6496+
When `encoding` is `'buffer'`, paths are returned as {Buffer} objects.
6497+
With `withFileTypes: true`, the `name` and `parentPath` properties of each
6498+
{fs.Dirent} use the specified encoding, including those passed to `exclude`.
6499+
The encoding does not change how patterns are matched.
6500+
64766501
When `followSymlinks` is enabled, detected symbolic link cycles are not
64776502
traversed recursively.
64786503
@@ -7731,8 +7756,8 @@ added: v10.10.0
77317756
* Type: {string|Buffer}
77327757
77337758
The file name that this {fs.Dirent} object refers to. The type of this
7734-
value is determined by the `options.encoding` passed to [`fs.readdir()`][] or
7735-
[`fs.readdirSync()`][].
7759+
value is determined by the `options.encoding` passed to the directory-reading
7760+
or glob function that created it.
77367761
77377762
#### `dirent.parentPath`
77387763
@@ -7749,9 +7774,10 @@ changes:
77497774
description: Marking the API stable.
77507775
-->
77517776
7752-
* Type: {string}
7777+
* Type: {string|Buffer}
77537778
77547779
The path to the parent directory of the file this {fs.Dirent} object refers to.
7780+
For glob results, this value uses the specified `options.encoding`.
77557781
77567782
### Class: `fs.FSWatcher`
77577783

‎lib/internal/fs/glob.js‎

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77
} = primordials;
88

99
const { lstatSync } = require('fs');
10-
const { basename, dirname, isAbsolute, join, resolve } = require('path');
10+
const { resolve } = require('path');
1111

1212
const {
1313
kEmptyObject,
@@ -20,7 +20,7 @@ const {
2020
validateString,
2121
validateStringArray,
2222
} = require('internal/validators');
23-
const { Dirent, DirentFromStats } = require('internal/fs/utils');
23+
const { assertEncoding, Dirent, DirentFromStats } = require('internal/fs/utils');
2424
const {
2525
codes: {
2626
ERR_INVALID_ARG_TYPE,
@@ -41,17 +41,19 @@ const {
4141
} = globBinding;
4242

4343
/**
44-
* @param {string} path
44+
* @param {Buffer} path
45+
* @param {string|Buffer} name
46+
* @param {string|Buffer} parentPath
4547
* @returns {DirentFromStats|null}
4648
*/
47-
function getDirentSync(path) {
49+
function getDirentSync(path, name, parentPath) {
4850
let stat;
4951
try {
5052
stat = lstatSync(path);
5153
} catch {
5254
return null;
5355
}
54-
return new DirentFromStats(basename(path), stat, dirname(path));
56+
return new DirentFromStats(name, stat, parentPath);
5557
}
5658

5759
/**
@@ -75,12 +77,17 @@ class Glob {
7577
#excludePatterns;
7678
#excludeFn;
7779
#withFileTypes;
80+
#encoding;
7881
#followSymlinks = false;
7982
#maxDepth = Infinity;
8083

8184
constructor(pattern, options = kEmptyObject) {
8285
validateObject(options, 'options');
83-
const { exclude, cwd, followSymlinks, maxDepth, withFileTypes } = options;
86+
const { exclude, cwd, encoding, followSymlinks, maxDepth, withFileTypes } = options;
87+
if (encoding !== 'buffer') {
88+
assertEncoding(encoding);
89+
}
90+
this.#encoding = encoding || 'utf8';
8491
this.#root = toPathIfFileURL(cwd) ?? '.';
8592
validateString(this.#root, 'options.cwd');
8693
if (followSymlinks != null) {
@@ -126,30 +133,30 @@ class Glob {
126133
globSync() {
127134
const result = globBinding.globSync(
128135
this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
129-
this.#excludeAdapter(), this.#maxDepth);
136+
this.#excludeAdapter(), this.#maxDepth, this.#encoding);
130137
if (!this.#withFileTypes) {
131138
return result;
132139
}
133-
const { 0: paths, 1: types } = result;
140+
const { 0: names, 1: types, 2: parents } = result;
134141
const dirents = [];
135-
for (let i = 0; i < paths.length; i++) {
136-
ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i]));
142+
for (let i = 0; i < names.length; i++) {
143+
ArrayPrototypePush(dirents, new Dirent(names[i], types[i], parents[i]));
137144
}
138145
return dirents;
139146
}
140147

141148
async globAll() {
142149
const handle = globBinding.globStart(
143150
this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
144-
this.#excludeAdapter(), this.#maxDepth);
151+
this.#excludeAdapter(), this.#maxDepth, this.#encoding);
145152
try {
146-
const { 0: paths, 1: types } = await handle.all();
153+
const { 0: paths, 1: types, 3: parents } = await handle.all();
147154
if (!this.#withFileTypes) {
148155
return paths;
149156
}
150157
const dirents = [];
151158
for (let i = 0; i < paths.length; i++) {
152-
ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i]));
159+
ArrayPrototypePush(dirents, new Dirent(paths[i], types[i], parents[i]));
153160
}
154161
return dirents;
155162
} finally {
@@ -161,14 +168,14 @@ class Glob {
161168
async * glob() {
162169
const handle = globBinding.globStart(
163170
this.#root, this.#patterns, this.#excludePatterns, this.#flags(),
164-
this.#excludeAdapter(), this.#maxDepth);
171+
this.#excludeAdapter(), this.#maxDepth, this.#encoding);
165172
try {
166173
let done = false;
167174
while (!done) {
168-
const { 0: paths, 1: types, 2: batchDone } = await handle.next();
175+
const { 0: paths, 1: types, 2: batchDone, 3: parents } = await handle.next();
169176
done = batchDone;
170177
for (let i = 0; i < paths.length; i++) {
171-
yield this.#withFileTypes ? this.#toDirent(paths[i], types[i]) : paths[i];
178+
yield this.#withFileTypes ? new Dirent(paths[i], types[i], parents[i]) : paths[i];
172179
}
173180
}
174181
} finally {
@@ -191,19 +198,14 @@ class Glob {
191198
if (!this.#withFileTypes) {
192199
return (entry, value) => exclude(value);
193200
}
194-
return (entry, value, parentPath, type) => {
201+
return (entry, value, parentPath, type, rawPath) => {
195202
if (entry) {
196203
return exclude(new Dirent(value, type, parentPath));
197204
}
198-
const dirent = getDirentSync(value);
205+
const dirent = getDirentSync(rawPath, value, parentPath);
199206
return dirent === null ? false : exclude(dirent);
200207
};
201208
}
202-
203-
#toDirent(path, type) {
204-
const full = isAbsolute(path) ? path : join(this.#root, path);
205-
return new Dirent(basename(full), type, dirname(full));
206-
}
207209
}
208210

209211
/**

‎src/glob/glob_walker.cc‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,6 +1782,73 @@ void ScanPool::RunOne(Mutex::ScopedLock& lock) {
17821782

17831783
} // namespace
17841784

1785+
DirentPath GetDirentPath(const std::string& cwd, const std::string& path) {
1786+
std::string full = path;
1787+
const bool joined = !cwd.empty() && !IsAbsolute(path);
1788+
if (joined) {
1789+
full = cwd + kSep + path;
1790+
if constexpr (kWindows) {
1791+
// Like path.join(), only retain a UNC prefix supplied by cwd itself.
1792+
const bool unc = cwd.size() > 2 && IsPathSeparator(cwd[0]) &&
1793+
IsPathSeparator(cwd[1]) && !IsPathSeparator(cwd[2]);
1794+
if (!unc) {
1795+
size_t leading = 0;
1796+
while (leading < full.size() && IsPathSeparator(full[leading])) {
1797+
leading++;
1798+
}
1799+
if (leading > 1) full.erase(0, leading - 1);
1800+
}
1801+
}
1802+
full = Normalize(full);
1803+
}
1804+
// basename() ignores a drive prefix, but not the server/share in a UNC root.
1805+
const bool has_drive = kWindows && full.size() >= 2 &&
1806+
IsWindowsDeviceRoot(full[0]) && full[1] == ':';
1807+
const size_t drive = has_drive ? 2 : 0;
1808+
size_t end = full.size();
1809+
while (end > drive && IsPathSeparator(full[end - 1])) end--;
1810+
size_t start = end;
1811+
while (start > drive && !IsPathSeparator(full[start - 1])) start--;
1812+
1813+
size_t root = RootLength(full);
1814+
if (kWindows && full.size() >= 2 && IsPathSeparator(full[0]) &&
1815+
IsPathSeparator(full[1])) {
1816+
// Unlike resolved walker paths, callback paths can have an incomplete UNC
1817+
// prefix. dirname() recognizes a UNC root only with both server and share.
1818+
size_t server_end = 2;
1819+
while (server_end < full.size() && !IsPathSeparator(full[server_end])) {
1820+
server_end++;
1821+
}
1822+
size_t share_start = server_end;
1823+
while (share_start < full.size() && IsPathSeparator(full[share_start])) {
1824+
share_start++;
1825+
}
1826+
root = 1;
1827+
if (server_end > 2 && share_start < full.size()) {
1828+
root = share_start;
1829+
while (root < full.size() && !IsPathSeparator(full[root])) root++;
1830+
if (root < full.size()) {
1831+
root++;
1832+
} else if (joined) {
1833+
// path.join() terminates a bare UNC share root with a separator.
1834+
full += kSep;
1835+
root++;
1836+
}
1837+
}
1838+
}
1839+
size_t parent_end = end;
1840+
while (parent_end > root && !IsPathSeparator(full[parent_end - 1])) {
1841+
parent_end--;
1842+
}
1843+
parent_end = parent_end > root ? parent_end - 1 : root;
1844+
// POSIX dirname('//name') keeps both leading separators.
1845+
if (!kWindows && root != 0 && parent_end == 1 && start == 2) {
1846+
parent_end = 2;
1847+
}
1848+
return {full.substr(start, end - start),
1849+
parent_end == 0 ? "." : full.substr(0, parent_end)};
1850+
}
1851+
17851852
// The pimpl the public Walk hands its calls to.
17861853
class WalkerImpl : public WalkerState {
17871854
public:

‎src/glob/glob_walker.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ struct WalkEntry {
5252
int type = UV_DIRENT_UNKNOWN;
5353
};
5454

55+
struct DirentPath {
56+
std::string name;
57+
std::string parent;
58+
};
59+
60+
// Split before encoding: path operations must preserve filesystem bytes.
61+
// An empty cwd leaves path as given, for exclude callbacks' lstat inputs.
62+
DirentPath GetDirentPath(const std::string& cwd, const std::string& path);
63+
5564
// One call a walk off the main thread needs made to the exclude callback,
5665
// which only that thread can do. A directory's questions are recorded in
5766
// the order the callback would see them, together with what decides

0 commit comments

Comments
 (0)