Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fs: fix recursive readdir with buffer encoding by canblmz1 · Pull Request #64954 · nodejs/node · GitHub
Skip to content
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
15 changes: 9 additions & 6 deletions lib/fs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,10 @@ const {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
Stats,
getReadFileBuffer,
getReadFileBufferByteLengthName,
Expand DownExpand Up@@ -1732,24 +1735,24 @@ function handleDirents({ result, currentPath, context }) {
for (let i = 0; i < length; i++) {
// Avoid excluding symlinks, as they are not directories.
// Refs: https://github.com/nodejs/node/issues/52663
const fullPath = pathModule.join(currentPath, names[i]);
const fullPath = joinPath(currentPath, names[i]);
const dirent = getDirent(currentPath, names[i], types[i]);
ArrayPrototypePush(context.readdirResults, dirent);

if (dirent.isDirectory() || binding.internalModuleStat(fullPath) === 1) {
if (dirent.isDirectory() || isDirectoryPath(fullPath)) {
ArrayPrototypePush(context.pathsQueue, fullPath);
}
}
}

function handleFilePaths({ result, currentPath, context }) {
for (let i = 0; i < result.length; i++) {
const resultPath = pathModule.join(currentPath, result[i]);
const relativeResultPath = pathModule.relative(context.basePath, resultPath);
const stat = binding.internalModuleStat(resultPath);
const resultPath = joinPath(currentPath, result[i]);
const relativeResultPath = relativeToBasePath(context.basePath, resultPath);
const stat = isDirectoryPath(resultPath);
ArrayPrototypePush(context.readdirResults, relativeResultPath);

if (stat === 1) {
if (stat) {
ArrayPrototypePush(context.pathsQueue, resultPath);
}
}
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/fs/promises.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,10 @@ const {
getValidatedPath,
getReadFileBuffer,
getReadFileBufferByteLengthName,
isDirectoryPath,
join: joinPath,
preprocessSymlinkDestination,
relativeToBasePath,
stringToFlags,
stringToSymlinkType,
toUnixTimestamp,
Expand DownExpand Up@@ -1640,7 +1643,7 @@ async function readdirRecursive(originalPath, options) {
for (const dirent of getDirents(path, readdir)) {
ArrayPrototypePush(result, dirent);
if (dirent.isDirectory()) {
const direntPath = pathModule.join(path, dirent.name);
const direntPath = joinPath(path, dirent.name);
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand All@@ -1661,13 +1664,13 @@ async function readdirRecursive(originalPath, options) {
while (queue.length > 0) {
const { 0: path, 1: readdir } = ArrayPrototypePop(queue);
for (const ent of readdir) {
const direntPath = pathModule.join(path, ent);
const stat = binding.internalModuleStat(direntPath);
const direntPath = joinPath(path, ent);
const isDir = isDirectoryPath(direntPath);
ArrayPrototypePush(
result,
pathModule.relative(originalPath, direntPath),
relativeToBasePath(originalPath, direntPath),
);
if (stat === 1) {
if (isDir) {
ArrayPrototypePush(queue, [
direntPath,
await PromisePrototypeThen(
Expand Down
35 changes: 35 additions & 0 deletions lib/internal/fs/utils.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ const {
validateUint32,
} = require('internal/validators');
const pathModule = require('path');
const binding = internalBinding('fs');
const kType = Symbol('type');
const kStats = Symbol('stats');
const kPartialAtimeNs = Symbol('partialAtimeNs');
Expand DownExpand Up@@ -249,6 +250,37 @@ function join(path, name) {
'path', ['string', 'Buffer'], path);
}

// Computes the equivalent of `path.relative(basePath, fullPath)` when
// either argument may be a Buffer (as with `readdir(..., { recursive: true,
// encoding: 'buffer' })`). `fullPath` is always built by repeatedly calling
// `join()` (above) starting from `basePath`, so stripping the `basePath`
// prefix - and the separator `join()` would have inserted - gives the same
// result as `path.relative()` without needing its general Buffer support.
function relativeToBasePath(basePath, fullPath) {
if (typeof basePath === 'string' && typeof fullPath === 'string') {
return pathModule.relative(basePath, fullPath);
}
const baseBuffer = isUint8Array(basePath) ? basePath : Buffer.from(basePath);
let offset = baseBuffer.length;
if (offset !== 0 && baseBuffer[offset - 1] !== bufferSep[0]) {
offset += bufferSep.length;
}
return fullPath.subarray(offset);
}

// `internalModuleStat` is a CommonJS-module-resolution-specific binding
// (see lib/internal/modules/cjs/loader.js) that only accepts strings. For
// Buffer paths, fall back to the general-purpose `stat` binding used by
// `fs.statSync()`, which handles Buffers correctly at the native layer
// without a lossy string round-trip.
function isDirectoryPath(path) {
if (typeof path === 'string') {
return binding.internalModuleStat(path) === 1;
}
const stats = binding.stat(path, false, undefined, false);
return stats !== undefined && getStatsFromBinding(stats).isDirectory();
}

function getDirents(path, { 0: names, 1: types }, callback) {
let i;
if (typeof callback === 'function') {
Expand DownExpand Up@@ -1128,6 +1160,9 @@ module.exports = {
getDirent,
getDirents,
getOptions,
isDirectoryPath,
join,
relativeToBasePath,
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-fs-readdir-recursive-buffer.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
'use strict';

// Regression test for https://github.com/nodejs/node/issues/58892
// `readdir`/`readdirSync` with `{ recursive: true }` throw
// ERR_INVALID_ARG_TYPE when `encoding: 'buffer'` is used, because the
// internal recursive walk joins path segments with `path.join()`, which
// does not accept Buffer arguments.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const nested = path.join(tmpdir.path, 'a', 'b');
fs.mkdirSync(nested, { recursive: true });
fs.writeFileSync(path.join(nested, 'file.txt'), 'hello');

// readdirSync
const syncResult = fs.readdirSync(tmpdir.path, { recursive: true, encoding: 'buffer' });
assert.ok(syncResult.every((entry) => Buffer.isBuffer(entry)));
assert.ok(syncResult.some((entry) => entry.toString().includes('file.txt')));

// readdirSync with withFileTypes
const syncDirents = fs.readdirSync(
tmpdir.path,
{ recursive: true, encoding: 'buffer', withFileTypes: true }
);
assert.ok(syncDirents.some((dirent) => dirent.name.toString() === 'file.txt'));

// readdir (callback)
fs.readdir(
tmpdir.path,
{ recursive: true, encoding: 'buffer' },
common.mustSucceed((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
})
);

// fs.promises.readdir
fs.promises
.readdir(tmpdir.path, { recursive: true, encoding: 'buffer' })
.then(common.mustCall((entries) => {
assert.ok(entries.every((entry) => Buffer.isBuffer(entry)));
assert.ok(entries.some((entry) => entry.toString().includes('file.txt')));
}));
Loading