diff --git a/doc/api/fs.md b/doc/api/fs.md index 2c5690b5f345..11c2bb37b65c 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1461,6 +1461,9 @@ changes: * `pattern` {string|string\[]} * `options` {Object} * `cwd` {string|URL} current working directory. **Default:** `process.cwd()` + * `encoding` {string} Encoding for returned paths and paths passed to an + `exclude` function. Use `'buffer'` to preserve filename bytes. + **Default:** `'utf8'`. * `exclude` {Function|string\[]} Function to filter out files/directories or a list of [glob patterns][] to be excluded. If a function is provided, return `true` to exclude the item, `false` to include it. **Default:** `undefined`. @@ -1478,6 +1481,11 @@ changes: See [Glob patterns][] for the syntax `pattern` accepts. +When `encoding` is `'buffer'`, paths are returned as {Buffer} objects. +With `withFileTypes: true`, the `name` and `parentPath` properties of each +{fs.Dirent} use the specified encoding, including those passed to `exclude`. +The encoding does not change how patterns are matched. + When `followSymlinks` is enabled, detected symbolic link cycles are not traversed recursively. @@ -3744,6 +3752,9 @@ changes: * `options` {Object} * `cwd` {string|URL} current working directory. **Default:** `process.cwd()` + * `encoding` {string} Encoding for returned paths and paths passed to an + `exclude` function. Use `'buffer'` to preserve filename bytes. + **Default:** `'utf8'`. * `exclude` {Function|string\[]} Function to filter out files/directories or a list of [glob patterns][] to be excluded. If a function is provided, return `true` to exclude the item, `false` to include it. **Default:** `undefined`. @@ -3761,6 +3772,11 @@ changes: See [Glob patterns][] for the syntax `pattern` accepts. +When `encoding` is `'buffer'`, paths are returned as {Buffer} objects. +With `withFileTypes: true`, the `name` and `parentPath` properties of each +{fs.Dirent} use the specified encoding, including those passed to `exclude`. +The encoding does not change how patterns are matched. + When `followSymlinks` is enabled, detected symbolic link cycles are not traversed recursively. @@ -6460,6 +6476,9 @@ changes: * `pattern` {string|string\[]} * `options` {Object} * `cwd` {string|URL} current working directory. **Default:** `process.cwd()` + * `encoding` {string} Encoding for returned paths and paths passed to an + `exclude` function. Use `'buffer'` to preserve filename bytes. + **Default:** `'utf8'`. * `exclude` {Function|string\[]} Function to filter out files/directories or a list of [glob patterns][] to be excluded. If a function is provided, return `true` to exclude the item, `false` to include it. **Default:** `undefined`. @@ -6469,10 +6488,16 @@ changes: The `cwd` directory has a depth of `0`. **Default:** `Infinity`. * `withFileTypes` {boolean} `true` if the glob should return paths as Dirents, `false` otherwise. **Default:** `false`. -* Returns: {string\[]} paths of files that match the pattern. +* Returns: {string\[]|Buffer\[]|fs.Dirent\[]} paths or directory entries that + match the pattern. See [Glob patterns][] for the syntax `pattern` accepts. +When `encoding` is `'buffer'`, paths are returned as {Buffer} objects. +With `withFileTypes: true`, the `name` and `parentPath` properties of each +{fs.Dirent} use the specified encoding, including those passed to `exclude`. +The encoding does not change how patterns are matched. + When `followSymlinks` is enabled, detected symbolic link cycles are not traversed recursively. @@ -7731,8 +7756,8 @@ added: v10.10.0 * Type: {string|Buffer} The file name that this {fs.Dirent} object refers to. The type of this -value is determined by the `options.encoding` passed to [`fs.readdir()`][] or -[`fs.readdirSync()`][]. +value is determined by the `options.encoding` passed to the directory-reading +or glob function that created it. #### `dirent.parentPath` @@ -7749,9 +7774,10 @@ changes: description: Marking the API stable. --> -* Type: {string} +* Type: {string|Buffer} The path to the parent directory of the file this {fs.Dirent} object refers to. +For glob results, this value uses the specified `options.encoding`. ### Class: `fs.FSWatcher` diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 12165cf42194..9ff24886d78c 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -7,7 +7,7 @@ const { } = primordials; const { lstatSync } = require('fs'); -const { basename, dirname, isAbsolute, join, resolve } = require('path'); +const { resolve } = require('path'); const { kEmptyObject, @@ -20,7 +20,7 @@ const { validateString, validateStringArray, } = require('internal/validators'); -const { Dirent, DirentFromStats } = require('internal/fs/utils'); +const { assertEncoding, Dirent, DirentFromStats } = require('internal/fs/utils'); const { codes: { ERR_INVALID_ARG_TYPE, @@ -41,17 +41,19 @@ const { } = globBinding; /** - * @param {string} path + * @param {Buffer} path + * @param {string|Buffer} name + * @param {string|Buffer} parentPath * @returns {DirentFromStats|null} */ -function getDirentSync(path) { +function getDirentSync(path, name, parentPath) { let stat; try { stat = lstatSync(path); } catch { return null; } - return new DirentFromStats(basename(path), stat, dirname(path)); + return new DirentFromStats(name, stat, parentPath); } /** @@ -75,12 +77,17 @@ class Glob { #excludePatterns; #excludeFn; #withFileTypes; + #encoding; #followSymlinks = false; #maxDepth = Infinity; constructor(pattern, options = kEmptyObject) { validateObject(options, 'options'); - const { exclude, cwd, followSymlinks, maxDepth, withFileTypes } = options; + const { exclude, cwd, encoding, followSymlinks, maxDepth, withFileTypes } = options; + if (encoding !== 'buffer') { + assertEncoding(encoding); + } + this.#encoding = encoding || 'utf8'; this.#root = toPathIfFileURL(cwd) ?? '.'; validateString(this.#root, 'options.cwd'); if (followSymlinks != null) { @@ -126,14 +133,14 @@ class Glob { globSync() { const result = globBinding.globSync( this.#root, this.#patterns, this.#excludePatterns, this.#flags(), - this.#excludeAdapter(), this.#maxDepth); + this.#excludeAdapter(), this.#maxDepth, this.#encoding); if (!this.#withFileTypes) { return result; } - const { 0: paths, 1: types } = result; + const { 0: names, 1: types, 2: parents } = result; const dirents = []; - for (let i = 0; i < paths.length; i++) { - ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i])); + for (let i = 0; i < names.length; i++) { + ArrayPrototypePush(dirents, new Dirent(names[i], types[i], parents[i])); } return dirents; } @@ -141,15 +148,15 @@ class Glob { async globAll() { const handle = globBinding.globStart( this.#root, this.#patterns, this.#excludePatterns, this.#flags(), - this.#excludeAdapter(), this.#maxDepth); + this.#excludeAdapter(), this.#maxDepth, this.#encoding); try { - const { 0: paths, 1: types } = await handle.all(); + const { 0: paths, 1: types, 3: parents } = await handle.all(); if (!this.#withFileTypes) { return paths; } const dirents = []; for (let i = 0; i < paths.length; i++) { - ArrayPrototypePush(dirents, this.#toDirent(paths[i], types[i])); + ArrayPrototypePush(dirents, new Dirent(paths[i], types[i], parents[i])); } return dirents; } finally { @@ -161,14 +168,14 @@ class Glob { async * glob() { const handle = globBinding.globStart( this.#root, this.#patterns, this.#excludePatterns, this.#flags(), - this.#excludeAdapter(), this.#maxDepth); + this.#excludeAdapter(), this.#maxDepth, this.#encoding); try { let done = false; while (!done) { - const { 0: paths, 1: types, 2: batchDone } = await handle.next(); + const { 0: paths, 1: types, 2: batchDone, 3: parents } = await handle.next(); done = batchDone; for (let i = 0; i < paths.length; i++) { - yield this.#withFileTypes ? this.#toDirent(paths[i], types[i]) : paths[i]; + yield this.#withFileTypes ? new Dirent(paths[i], types[i], parents[i]) : paths[i]; } } } finally { @@ -191,19 +198,14 @@ class Glob { if (!this.#withFileTypes) { return (entry, value) => exclude(value); } - return (entry, value, parentPath, type) => { + return (entry, value, parentPath, type, rawPath) => { if (entry) { return exclude(new Dirent(value, type, parentPath)); } - const dirent = getDirentSync(value); + const dirent = getDirentSync(rawPath, value, parentPath); return dirent === null ? false : exclude(dirent); }; } - - #toDirent(path, type) { - const full = isAbsolute(path) ? path : join(this.#root, path); - return new Dirent(basename(full), type, dirname(full)); - } } /** diff --git a/src/glob/glob_walker.cc b/src/glob/glob_walker.cc index dda6e7c1c1f2..7f0ccc998a8f 100644 --- a/src/glob/glob_walker.cc +++ b/src/glob/glob_walker.cc @@ -1782,6 +1782,73 @@ void ScanPool::RunOne(Mutex::ScopedLock& lock) { } // namespace +DirentPath GetDirentPath(const std::string& cwd, const std::string& path) { + std::string full = path; + const bool joined = !cwd.empty() && !IsAbsolute(path); + if (joined) { + full = cwd + kSep + path; + if constexpr (kWindows) { + // Like path.join(), only retain a UNC prefix supplied by cwd itself. + const bool unc = cwd.size() > 2 && IsPathSeparator(cwd[0]) && + IsPathSeparator(cwd[1]) && !IsPathSeparator(cwd[2]); + if (!unc) { + size_t leading = 0; + while (leading < full.size() && IsPathSeparator(full[leading])) { + leading++; + } + if (leading > 1) full.erase(0, leading - 1); + } + } + full = Normalize(full); + } + // basename() ignores a drive prefix, but not the server/share in a UNC root. + const bool has_drive = kWindows && full.size() >= 2 && + IsWindowsDeviceRoot(full[0]) && full[1] == ':'; + const size_t drive = has_drive ? 2 : 0; + size_t end = full.size(); + while (end > drive && IsPathSeparator(full[end - 1])) end--; + size_t start = end; + while (start > drive && !IsPathSeparator(full[start - 1])) start--; + + size_t root = RootLength(full); + if (kWindows && full.size() >= 2 && IsPathSeparator(full[0]) && + IsPathSeparator(full[1])) { + // Unlike resolved walker paths, callback paths can have an incomplete UNC + // prefix. dirname() recognizes a UNC root only with both server and share. + size_t server_end = 2; + while (server_end < full.size() && !IsPathSeparator(full[server_end])) { + server_end++; + } + size_t share_start = server_end; + while (share_start < full.size() && IsPathSeparator(full[share_start])) { + share_start++; + } + root = 1; + if (server_end > 2 && share_start < full.size()) { + root = share_start; + while (root < full.size() && !IsPathSeparator(full[root])) root++; + if (root < full.size()) { + root++; + } else if (joined) { + // path.join() terminates a bare UNC share root with a separator. + full += kSep; + root++; + } + } + } + size_t parent_end = end; + while (parent_end > root && !IsPathSeparator(full[parent_end - 1])) { + parent_end--; + } + parent_end = parent_end > root ? parent_end - 1 : root; + // POSIX dirname('//name') keeps both leading separators. + if (!kWindows && root != 0 && parent_end == 1 && start == 2) { + parent_end = 2; + } + return {full.substr(start, end - start), + parent_end == 0 ? "." : full.substr(0, parent_end)}; +} + // The pimpl the public Walk hands its calls to. class WalkerImpl : public WalkerState { public: diff --git a/src/glob/glob_walker.h b/src/glob/glob_walker.h index 9a8ec0647a09..c52bca372987 100644 --- a/src/glob/glob_walker.h +++ b/src/glob/glob_walker.h @@ -52,6 +52,15 @@ struct WalkEntry { int type = UV_DIRENT_UNKNOWN; }; +struct DirentPath { + std::string name; + std::string parent; +}; + +// Split before encoding: path operations must preserve filesystem bytes. +// An empty cwd leaves path as given, for exclude callbacks' lstat inputs. +DirentPath GetDirentPath(const std::string& cwd, const std::string& path); + // One call a walk off the main thread needs made to the exclude callback, // which only that thread can do. A directory's questions are recorded in // the order the callback would see them, together with what decides diff --git a/src/glob/node_glob.cc b/src/glob/node_glob.cc index 9337775c79da..12f0828241c6 100644 --- a/src/glob/node_glob.cc +++ b/src/glob/node_glob.cc @@ -8,8 +8,10 @@ #include "glob/glob_parser.h" #include "glob/glob_walker.h" #include "memory_tracker-inl.h" +#include "node_buffer.h" #include "node_errors.h" #include "node_external_reference.h" +#include "string_bytes.h" #include "util-inl.h" #include "v8.h" @@ -108,22 +110,39 @@ void ThrowCompileError(Isolate* isolate, CompileError error) { constexpr size_t kMarshalStackEntries = GlobRequest::kBatchSize; -// Builds [paths array, types array|undefined] for a batch of results. +// Builds paths, or separately encoded names, types and parents for Dirents. bool MarshalEntries(Environment* env, const std::vector& entries, bool with_types, + const std::string& cwd, + enum encoding encoding, Local* paths_out, - Local* types_out) { + Local* types_out, + Local* parents_out) { Isolate* isolate = env->isolate(); - Local context = env->context(); MaybeStackBuffer paths(isolate, entries.size()); + MaybeStackBuffer parents( + isolate, with_types ? entries.size() : 0); for (size_t i = 0; i < entries.size(); i++) { - if (!ToV8Value(context, entries[i].path, isolate).ToLocal(&paths[i])) { + DirentPath dirent; + std::string_view path = entries[i].path; + if (with_types) { + dirent = GetDirentPath(cwd, entries[i].path); + path = dirent.name; + if (!StringBytes::Encode( + isolate, dirent.parent.data(), dirent.parent.size(), encoding) + .ToLocal(&parents[i])) { + return false; + } + } + if (!StringBytes::Encode(isolate, path.data(), path.size(), encoding) + .ToLocal(&paths[i])) { return false; } } *paths_out = paths.ToArray(); *types_out = Undefined(isolate); + *parents_out = Undefined(isolate); if (with_types) { MaybeStackBuffer types(isolate, entries.size()); @@ -131,6 +150,7 @@ bool MarshalEntries(Environment* env, types[i] = Integer::New(isolate, entries[i].type); } *types_out = types.ToArray(); + *parents_out = parents.ToArray(); } return true; } @@ -277,23 +297,26 @@ void BindingData::HasMagic(const FunctionCallbackInfo& args) { // Both strings are one-byte and the pattern is normally // already compiled, so the whole call is a hash lookup plus the match. // Reads (cwd, includePatterns[], excludePatterns[], flags, excludeFn, -// maxDepth) and compiles the patterns. +// maxDepth, encoding) and compiles the patterns. bool BindingData::ReadWalkArguments(const FunctionCallbackInfo& args, WalkOptions* options, std::vector* includes, std::vector* excludes, - std::unique_ptr* filter) { + std::unique_ptr* filter, + enum encoding* encoding) { Realm* realm = Realm::GetCurrent(args); Isolate* isolate = realm->isolate(); Local context = realm->context(); - CHECK_EQ(args.Length(), 6); + CHECK_EQ(args.Length(), 7); CHECK(args[0]->IsString()); // cwd CHECK(args[1]->IsArray()); // include patterns CHECK(args[2]->IsArray()); // exclude patterns CHECK(args[3]->IsUint32()); // packed flags CHECK(args[4]->IsFunction() || args[4]->IsUndefined()); // exclude callback CHECK(args[5]->IsNumber()); // maxDepth + CHECK(args[6]->IsString()); // encoding + *encoding = ParseEncoding(isolate, args[6], UTF8); const uint32_t packed = args[3].As()->Value() | kHostWalkFlags; const CompileFlags flags = UnpackFlags(packed); @@ -306,8 +329,10 @@ bool BindingData::ReadWalkArguments(const FunctionCallbackInfo& args, options->follow_symlinks = (packed & kFlagFollowSymlinks) != 0; options->with_file_types = (packed & kFlagWithFileTypes) != 0; if (args[4]->IsFunction()) { - *filter = - std::make_unique(realm->env(), args[4].As()); + *filter = std::make_unique(realm->env(), + args[4].As(), + options->with_file_types, + *encoding); options->exclude_filter = filter->get(); } { @@ -346,7 +371,7 @@ bool BindingData::ReadWalkArguments(const FunctionCallbackInfo& args, } // globSync(cwd, includePatterns[], excludePatterns[], flags, excludeFn, -// maxDepth) +// maxDepth, encoding) void BindingData::GlobSync(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); Environment* env = realm->env(); @@ -357,8 +382,9 @@ void BindingData::GlobSync(const FunctionCallbackInfo& args) { std::vector includes; std::vector excludes; std::unique_ptr filter; + enum encoding encoding; if (!binding->ReadWalkArguments( - args, &options, &includes, &excludes, &filter)) { + args, &options, &includes, &excludes, &filter, &encoding)) { return; } @@ -369,20 +395,34 @@ void BindingData::GlobSync(const FunctionCallbackInfo& args) { Local paths; Local types; - if (!MarshalEntries(env, entries, options.with_file_types, &paths, &types)) { + Local parents; + if (!MarshalEntries(env, + entries, + options.with_file_types, + options.cwd, + encoding, + &paths, + &types, + &parents)) { return; } if (!options.with_file_types) { args.GetReturnValue().Set(paths); return; } - Local pair[] = {paths, types}; + Local pair[] = {paths, types, parents}; args.GetReturnValue().Set(Array::New(isolate, pair, arraysize(pair))); } // Calls the user's `exclude` -JsExcludeFilter::JsExcludeFilter(Environment* env, Local callback) - : env_(env), callback_(env->isolate(), callback) {} +JsExcludeFilter::JsExcludeFilter(Environment* env, + Local callback, + bool with_file_types, + enum encoding encoding) + : env_(env), + callback_(env->isolate(), callback), + with_file_types_(with_file_types), + encoding_(encoding) {} Maybe JsExcludeFilter::Call(bool entry, std::string_view first, @@ -391,10 +431,25 @@ Maybe JsExcludeFilter::Call(bool entry, Isolate* isolate = env_->isolate(); HandleScope scope(isolate); Local context = env_->context(); - // (isEntry, name-or-path, parentPath, type), see #excludeAdapter(). - Local argv[4]; - if (!ToV8Value(context, first, isolate).ToLocal(&argv[1]) || - !ToV8Value(context, second, isolate).ToLocal(&argv[2])) { + // (isEntry, name-or-path, parentPath, type, rawPath), see #excludeAdapter(). + Local argv[5]; + argv[4] = Undefined(isolate); + DirentPath dirent; + if (with_file_types_ && !entry) { + // lstat must use the original bytes, regardless of the output encoding. + Local raw; + if (!Buffer::Copy(isolate, first.data(), first.size()).ToLocal(&raw)) { + return Nothing(); + } + argv[4] = raw; + dirent = GetDirentPath("", std::string(first)); + first = dirent.name; + second = dirent.parent; + } + if (!StringBytes::Encode(isolate, first.data(), first.size(), encoding_) + .ToLocal(&argv[1]) || + !StringBytes::Encode(isolate, second.data(), second.size(), encoding_) + .ToLocal(&argv[2])) { return Nothing(); } argv[0] = Boolean::New(isolate, entry); @@ -429,16 +484,20 @@ GlobRequest::GlobRequest(Environment* env, const WalkOptions& options, const std::vector& includes, const std::vector& excludes, - std::unique_ptr filter) + std::unique_ptr filter, + enum encoding encoding) : AsyncWrap(env, object, AsyncWrap::PROVIDER_GLOBREQUEST), ThreadPoolWork(env, "glob"), filter_(std::move(filter)), walk_(env, options, includes, excludes), - with_file_types_(options.with_file_types) { + with_file_types_(options.with_file_types), + encoding_(encoding), + cwd_(options.cwd) { MakeWeak(); } void GlobRequest::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("cwd", cwd_); // A slice in flight owns batch_ on another thread. if (!in_flight_) { tracker->TrackFieldWithSize("batch", batch_.size() * sizeof(WalkEntry)); @@ -494,18 +553,26 @@ void GlobRequest::AnswerQuestions() { walk_.SetAnswers(std::move(answers)); } -// Builds [paths, types|undefined, done] +// Builds [paths, types|undefined, done, parents|undefined]. MaybeLocal GlobRequest::Settle() { Environment* env = AsyncWrap::env(); Isolate* isolate = env->isolate(); Local paths; Local types; - if (!MarshalEntries(env, batch_, with_file_types_, &paths, &types)) { + Local parents; + if (!MarshalEntries(env, + batch_, + with_file_types_, + cwd_, + encoding_, + &paths, + &types, + &parents)) { return MaybeLocal(); } batch_.clear(); - Local result[] = {paths, types, Boolean::New(isolate, done_)}; + Local result[] = {paths, types, Boolean::New(isolate, done_), parents}; return Array::New(isolate, result, arraysize(result)); } @@ -627,8 +694,9 @@ void BindingData::GlobStart(const FunctionCallbackInfo& args) { std::vector includes; std::vector excludes; std::unique_ptr filter; + enum encoding encoding; if (!binding->ReadWalkArguments( - args, &options, &includes, &excludes, &filter)) { + args, &options, &includes, &excludes, &filter, &encoding)) { return; } @@ -639,7 +707,8 @@ void BindingData::GlobStart(const FunctionCallbackInfo& args) { .ToLocal(&object)) { return; } - new GlobRequest(env, object, options, includes, excludes, std::move(filter)); + new GlobRequest( + env, object, options, includes, excludes, std::move(filter), encoding); args.GetReturnValue().Set(object); } diff --git a/src/glob/node_glob.h b/src/glob/node_glob.h index cf106a888eba..d32191d8db03 100644 --- a/src/glob/node_glob.h +++ b/src/glob/node_glob.h @@ -20,7 +20,10 @@ namespace node::glob { // Consults a JavaScript `exclude` callback. class JsExcludeFilter final : public ExcludeFilter { public: - JsExcludeFilter(Environment* env, v8::Local callback); + JsExcludeFilter(Environment* env, + v8::Local callback, + bool with_file_types, + enum encoding encoding); bool ExcludesPath(std::string_view path) override; bool ExcludesEntry(std::string_view name, @@ -37,6 +40,8 @@ class JsExcludeFilter final : public ExcludeFilter { Environment* env_; v8::Global callback_; + const bool with_file_types_; + const enum encoding encoding_; bool failed_ = false; }; @@ -72,7 +77,8 @@ class BindingData : public SnapshotableObject { WalkOptions* options, std::vector* includes, std::vector* excludes, - std::unique_ptr* filter); + std::unique_ptr* filter, + enum encoding* encoding); // Compiles (or fetches) `pattern`; returns nullptr on a compile error, // with the reason written to *error. @@ -103,7 +109,8 @@ class GlobRequest : public AsyncWrap, public ThreadPoolWork { const WalkOptions& options, const std::vector& includes, const std::vector& excludes, - std::unique_ptr filter); + std::unique_ptr filter, + enum encoding encoding); // How many results an iterator pull returns at a time. // See the explanation in this header's .cc file for @@ -134,6 +141,9 @@ class GlobRequest : public AsyncWrap, public ThreadPoolWork { std::unique_ptr filter_; Walk walk_; const bool with_file_types_; + // Output conversion only; the walker always owns raw filesystem paths. + const enum encoding encoding_; + const std::string cwd_; // Set by all(): the pull consumes the rest of the walk in one batch. bool drain_ = false; bool done_ = false; diff --git a/test/parallel/test-fs-glob-encoding.mjs b/test/parallel/test-fs-glob-encoding.mjs new file mode 100644 index 000000000000..8afd7cc0218b --- /dev/null +++ b/test/parallel/test-fs-glob-encoding.mjs @@ -0,0 +1,198 @@ +import * as common from '../common/index.mjs'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { join, sep } from 'node:path'; +import { promisify } from 'node:util'; +import { test } from 'node:test'; +import tmpdir from '../common/tmpdir.js'; + +tmpdir.refresh(); +const cwd = tmpdir.path; +fs.mkdirSync(join(cwd, 'dir')); +fs.writeFileSync(join(cwd, 'dir', '文件.txt'), 'hello'); + +const apis = [ + ['sync', fs.globSync], + ['callback', promisify(fs.glob)], + ['iterator', (pattern, options) => Array.fromAsync(fs.promises.glob(pattern, options))], +]; + +// Encode independently of glob, starting with the bytes used to create files. +function encode(value, encoding) { + const bytes = Buffer.from(value); + return encoding === 'buffer' ? bytes : bytes.toString(encoding || 'utf8'); +} + +for (const [api, glob] of apis) { + for (const encoding of [undefined, null, 'utf8', 'buffer', 'latin1', 'hex', 'base64', 'utf16le']) { + await test(`${api}: ${encoding} paths and Dirents`, async () => { + const options = { cwd, encoding }; + assert.deepStrictEqual(await glob('dir/*', options), [ + encode(join('dir', '文件.txt'), encoding), + ]); + const entries = await glob('dir/*', { ...options, withFileTypes: true }); + assert.strictEqual(entries.length, 1); + assert(entries[0].isFile()); + assert.deepStrictEqual(entries[0].name, encode('文件.txt', encoding)); + assert.deepStrictEqual(entries[0].parentPath, encode(join(cwd, 'dir'), encoding)); + }); + } + + await test(`${api}: validates encoding`, async () => { + for (const encoding of ['invalid', 123, {}]) { + await assert.rejects(async () => glob('*', { cwd, encoding }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + }); + + for (const withFileTypes of [false, true]) { + for (const encoding of ['buffer', 'hex']) { + await test(`${api}: ${encoding} exclude, withFileTypes=${withFileTypes}`, async () => { + let seen = false; + const results = await glob('dir/**', { + cwd, + encoding, + withFileTypes, + exclude: common.mustCallAtLeast((value) => { + const name = withFileTypes ? value.name : value; + assert.strictEqual(Buffer.isBuffer(name), encoding === 'buffer'); + if (withFileTypes) { + assert.strictEqual(Buffer.isBuffer(value.parentPath), encoding === 'buffer'); + } + if (name.toString() === encode('文件.txt', encoding).toString()) { + seen = true; + if (withFileTypes) assert(value.isFile()); + return true; + } + return false; + }), + }); + assert(seen); + assert.deepStrictEqual( + results.map((value) => (withFileTypes ? value.name : value)), + [encode('dir', encoding)], + ); + }); + } + } + + await test(`${api}: relative cwd`, async () => { + const options = { cwd: '.', encoding: 'buffer', withFileTypes: true }; + const [entry] = await glob('test/common', options); + assert(entry.isDirectory()); + assert.deepStrictEqual(entry.name, Buffer.from('common')); + assert.deepStrictEqual(entry.parentPath, Buffer.from('test')); + }); + + await test(`${api}: Windows root cwd`, { skip: !common.isWindows }, async () => { + const name = fs.readdirSync('\\').find((name) => !/[*?[\]{}()!]/.test(name)); + assert(name); + for (const encoding of ['utf8', 'buffer']) { + const [entry] = await glob(name, { cwd: '\\', encoding, withFileTypes: true }); + assert.deepStrictEqual(entry.name, encode(name, encoding)); + assert.deepStrictEqual(entry.parentPath, encode('\\', encoding)); + } + }); +} + +await test('async iterator preserves encoding across batches', async () => { + const dir = join(cwd, 'batch'); + fs.mkdirSync(dir); + const expected = new Map(); + // A native batch can consume a whole directory before it yields. + for (let batch = 0; batch < 3; batch++) { + const parent = join(dir, `${batch}`); + fs.mkdirSync(parent); + for (let i = 0; i < 150; i++) { + const name = `${batch}-${i}.txt`; + fs.writeFileSync(join(parent, name), ''); + expected.set(name, parent); + } + } + const entries = await Array.fromAsync(fs.promises.glob('*/*', { + cwd: dir, encoding: 'buffer', withFileTypes: true, + })); + assert.strictEqual(entries.length, expected.size); + assert.strictEqual(new Set(entries.map((entry) => entry.name.toString())).size, expected.size); + for (const entry of entries) { + assert(Buffer.isBuffer(entry.name)); + assert.deepStrictEqual(entry.parentPath, Buffer.from(expected.get(entry.name.toString()))); + } +}); + +await test('non-UTF8 filenames survive traversal, filtering and reading', { + skip: !common.isLinux && 'Requires a filesystem accepting non-UTF8 names', +}, async () => { + const root = join(cwd, 'raw'); + fs.mkdirSync(root); + const prefix = Buffer.from(`${root}/`); + const names = [Buffer.from([0xe9]), Buffer.from([0xea])]; + for (const name of names) { + const dir = Buffer.concat([prefix, name]); + fs.mkdirSync(dir); + fs.writeFileSync(Buffer.concat([dir, Buffer.from('/'), name]), name); + } + + for (const [api, glob] of apis) { + const expected = names.map((name) => Buffer.concat([name, Buffer.from('/'), name])); + assert.deepStrictEqual( + (await glob('[^a-z]', { cwd: root, encoding: 'buffer' })).sort(Buffer.compare), + names, + api, + ); + assert.deepStrictEqual( + (await glob('*/*', { cwd: root, encoding: 'buffer' })).sort(Buffer.compare), + expected, + api, + ); + const entries = await glob('**/*', { + cwd: root, encoding: 'buffer', withFileTypes: true, + }); + assert.strictEqual(entries.length, 4, api); + for (const entry of entries.filter((entry) => entry.isFile())) { + const path = Buffer.concat([entry.parentPath, Buffer.from(sep), entry.name]); + assert.deepStrictEqual(fs.readFileSync(path), entry.name, api); + } + const paths = await glob('**/*', { + cwd: root, + encoding: 'buffer', + exclude: (name) => name.equals(names[0]), + }); + assert.deepStrictEqual(paths.sort(Buffer.compare), [names[1], expected[1]], api); + const filtered = await glob('**/*', { + cwd: root, + encoding: 'buffer', + withFileTypes: true, + exclude: (entry) => entry.name.equals(names[0]), + }); + assert.strictEqual(filtered.length, 2, api); + assert(filtered.every((entry) => entry.name.equals(names[1])), api); + } + + // Path callbacks use lstat rather than a directory entry's cached type. + // In particular, a hex-encoded display path must never be used for lstat. + const originalCwd = process.cwd(); + process.chdir(root); + try { + for (const [api, glob] of apis) { + let sawPath = false; + const entries = await glob('**/*', { + encoding: 'hex', + withFileTypes: true, + exclude: common.mustCallAtLeast((entry) => { + if (entry.parentPath === '2e') { + sawPath = true; + assert(entry.isDirectory()); + assert(['e9', 'ea'].includes(entry.name)); + } + return false; + }), + }); + assert(sawPath, api); + assert.strictEqual(entries.length, 4, api); + } + } finally { + process.chdir(originalCwd); + } +});