From 5642eec803ae2dca60d3af272c703cd0da945b6b Mon Sep 17 00:00:00 2001 From: Jamie Currier Date: Thu, 23 Jul 2026 00:17:15 +0000 Subject: [PATCH] fs: support Buffer paths in cp methods Use byte-preserving path helpers for recursive copies. Read directory entries as buffers and retain Buffer paths where string conversion would lose data. Keep the existing string behavior when paths can be represented without losing information. Fixes: https://github.com/nodejs/node/issues/58634 Signed-off-by: Jamie Currier --- doc/api/fs.md | 24 +- lib/fs.js | 9 +- lib/internal/fs/cp/cp-sync.js | 63 +-- lib/internal/fs/cp/cp.js | 101 ++-- lib/internal/fs/cp/path.js | 135 +++++ lib/internal/fs/promises.js | 5 +- lib/internal/fs/utils.js | 15 +- test/known_issues/test-fs-cp-async-buffer.js | 23 - test/known_issues/test-fs-cp-filter.js | 34 -- test/known_issues/test-fs-cp-non-utf8.js | 54 -- test/parallel/test-fs-cp-buffer-paths.js | 497 +++++++++++++++++++ test/parallel/test-fs-cp-path.js | 131 +++++ 12 files changed, 879 insertions(+), 212 deletions(-) create mode 100644 lib/internal/fs/cp/path.js delete mode 100644 test/known_issues/test-fs-cp-async-buffer.js delete mode 100644 test/known_issues/test-fs-cp-filter.js delete mode 100644 test/known_issues/test-fs-cp-non-utf8.js create mode 100644 test/parallel/test-fs-cp-buffer-paths.js create mode 100644 test/parallel/test-fs-cp-path.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 9cb84007b3b815..de03d99f927e05 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1367,8 +1367,8 @@ changes: whether to perform path resolution for symlinks. --> -* `src` {string|URL} source path to copy. -* `dest` {string|URL} destination path to copy to. +* `src` {string|Buffer|URL} source path to copy. +* `dest` {string|Buffer|URL} destination path to copy to. * `options` {Object} * `dereference` {boolean} dereference symlinks. **Default:** `false`. * `errorOnExist` {boolean} when `force` is `false`, and the destination @@ -1377,8 +1377,8 @@ changes: `true` to copy the item, `false` to ignore it. When ignoring a directory, all of its contents will be skipped as well. Can also return a `Promise` that resolves to `true` or `false` **Default:** `undefined`. - * `src` {string} source path to copy. - * `dest` {string} destination path to copy to. + * `src` {string|Buffer} source path to copy. + * `dest` {string|Buffer} destination path to copy to. * Returns: {boolean|Promise} A value that is coercible to `boolean` or a `Promise` that fulfils with such value. * `force` {boolean} overwrite existing file or directory. The copy @@ -2892,8 +2892,8 @@ changes: whether to perform path resolution for symlinks. --> -* `src` {string|URL} source path to copy. -* `dest` {string|URL} destination path to copy to. +* `src` {string|Buffer|URL} source path to copy. +* `dest` {string|Buffer|URL} destination path to copy to. * `options` {Object} * `dereference` {boolean} dereference symlinks. **Default:** `false`. * `errorOnExist` {boolean} when `force` is `false`, and the destination @@ -2902,8 +2902,8 @@ changes: `true` to copy the item, `false` to ignore it. When ignoring a directory, all of its contents will be skipped as well. Can also return a `Promise` that fulfills with `true` or `false`. **Default:** `undefined`. - * `src` {string} source path to copy. - * `dest` {string} destination path to copy to. + * `src` {string|Buffer} source path to copy. + * `dest` {string|Buffer} destination path to copy to. * Returns: {boolean|Promise} A value that is coercible to `boolean` or a `Promise` that fulfils with such value. * `force` {boolean} overwrite existing file or directory. The copy @@ -6023,8 +6023,8 @@ changes: whether to perform path resolution for symlinks. --> -* `src` {string|URL} source path to copy. -* `dest` {string|URL} destination path to copy to. +* `src` {string|Buffer|URL} source path to copy. +* `dest` {string|Buffer|URL} destination path to copy to. * `options` {Object} * `dereference` {boolean} dereference symlinks. **Default:** `false`. * `errorOnExist` {boolean} when `force` is `false`, and the destination @@ -6032,8 +6032,8 @@ changes: * `filter` {Function} Function to filter copied files/directories. Return `true` to copy the item, `false` to ignore it. When ignoring a directory, all of its contents will be skipped as well. **Default:** `undefined` - * `src` {string} source path to copy. - * `dest` {string} destination path to copy to. + * `src` {string|Buffer} source path to copy. + * `dest` {string|Buffer} destination path to copy to. * Returns: {boolean} Any non-`Promise` value that is coercible to `boolean`. * `force` {boolean} overwrite existing file or directory. The copy diff --git a/lib/fs.js b/lib/fs.js index 4fbdaf8130180a..947c5bf23c523d 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -107,6 +107,7 @@ const { getDirents, getOptions, getValidatedFd, + getValidatedCpPath, getValidatedPath, handleErrorFromBinding, preprocessSymlinkDestination, @@ -3694,8 +3695,8 @@ function cp(src, dest, options, callback) { } callback = makeCallback(callback); options = validateCpOptions(options); - src = getValidatedPath(src, 'src'); - dest = getValidatedPath(dest, 'dest'); + src = getValidatedCpPath(src, 'src'); + dest = getValidatedCpPath(dest, 'dest'); lazyLoadCp(); cpFn(src, dest, options, callback); } @@ -3710,8 +3711,8 @@ function cp(src, dest, options, callback) { */ function cpSync(src, dest, options) { options = validateCpOptions(options); - src = getValidatedPath(src, 'src'); - dest = getValidatedPath(dest, 'dest'); + src = getValidatedCpPath(src, 'src'); + dest = getValidatedCpPath(dest, 'dest'); lazyLoadCp(); cpSyncFn(src, dest, options); } diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index 03fcae9b7cdbda..ad493cfa2ca4dc 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -3,7 +3,13 @@ // This file is a modified version of the fs-extra's copySync method. const fsBinding = internalBinding('fs'); -const { isSrcSubdir } = require('internal/fs/cp/cp'); +const { + dirnamePath, + isSrcSubdir, + joinPath, + maybeDecodePath, + resolveLinkTarget, +} = require('internal/fs/cp/path'); const { codes: { ERR_FS_CP_EEXIST, ERR_FS_CP_EINVAL, @@ -30,12 +36,6 @@ const { unlinkSync, utimesSync, } = require('fs'); -const { - dirname, - isAbsolute, - join, - resolve, -} = require('path'); const { isPromise } = require('util/types'); function cpSyncFn(src, dest, opts) { @@ -138,9 +138,10 @@ function onDir(srcStat, destStat, src, dest, opts) { } function copyDir(src, dest, opts, mkDir, srcMode) { - if (!opts.filter) { - // The caller didn't provide a js filter function, in this case - // we can run the whole function faster in C++ + if (!opts.filter && typeof src === 'string' && typeof dest === 'string') { + // The caller didn't provide a js filter function and both paths are strings, + // in this case we can run the whole function faster in C++. + // Buffer paths require the byte-preserving JavaScript traversal. // TODO(dario-piotrowicz): look into making cpSyncCopyDir also accept the potential filter function return fsBinding.cpSyncCopyDir(src, dest, opts.force, @@ -154,15 +155,15 @@ function copyDir(src, dest, opts, mkDir, srcMode) { mkdirSync(dest); } - const dir = opendirSync(src); + const dir = opendirSync(src, { encoding: 'buffer' }); try { let dirent; while ((dirent = dir.readSync()) !== null) { const { name } = dirent; - const srcItem = join(src, name); - const destItem = join(dest, name); + const srcItem = joinPath(src, name); + const destItem = joinPath(dest, name); let shouldCopy = true; if (opts.filter) { @@ -187,18 +188,23 @@ function copyDir(src, dest, opts, mkDir, srcMode) { // TODO(@anonrig): Move this function to C++. function onLink(destStat, src, dest, verbatimSymlinks) { - let resolvedSrc = readlinkSync(src); - if (!verbatimSymlinks && !isAbsolute(resolvedSrc)) { - resolvedSrc = resolve(dirname(src), resolvedSrc); - } - if (!destStat) { - return symlinkSync(resolvedSrc, dest); + let resolvedSrc = maybeDecodePath( + readlinkSync(src, { encoding: 'buffer' }), + typeof src === 'string', + ); + if (!verbatimSymlinks) { + resolvedSrc = resolveLinkTarget(dirnamePath(src), resolvedSrc); } + if (!destStat) return symlinkSync(resolvedSrc, dest); + let resolvedDest; try { - resolvedDest = readlinkSync(dest); + resolvedDest = maybeDecodePath( + readlinkSync(dest, { encoding: 'buffer' }), + typeof dest === 'string', + ); } catch (err) { - // Dest exists and is a regular file or directory, + // Dest exists and is a regular file or directory. // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { @@ -206,24 +212,25 @@ function onLink(destStat, src, dest, verbatimSymlinks) { } throw err; } - if (!isAbsolute(resolvedDest)) { - resolvedDest = resolve(dirname(dest), resolvedDest); + if (!verbatimSymlinks) { + resolvedDest = resolveLinkTarget(dirnamePath(dest), resolvedDest); } - const srcIsDir = fsBinding.internalModuleStat(src) === 1; + const srcIsDir = typeof src === 'string' ? + fsBinding.internalModuleStat(src) === 1 : + statSync(src).isDirectory(); if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { throw new ERR_FS_CP_EINVAL({ message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + - `${resolvedDest}`, + `${resolvedDest}`, path: dest, syscall: 'cp', errno: EINVAL, code: 'EINVAL', }); } - // Prevent copy if src is a subdir of dest since unlinking - // dest in this case would result in removing src contents - // and therefore a broken symlink would be created. + // Prevent copy if src is a subdir of dest since unlinking dest in this case + // would result in removing src contents and therefore a broken symlink. if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 10c52b11463445..e8f96c01ec0c68 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -3,13 +3,9 @@ // This file is a modified version of the fs-extra's copy method. const { - ArrayPrototypeEvery, - ArrayPrototypeFilter, - Boolean, PromisePrototypeThen, PromiseReject, SafePromiseAll, - StringPrototypeSplit, } = primordials; const { codes: { @@ -46,16 +42,20 @@ const { unlink, utimes, } = require('fs/promises'); -const { - dirname, - isAbsolute, - join, - parse, - resolve, - sep, -} = require('path'); const fsBinding = internalBinding('fs'); + +const { + dirnamePath, + isPathRoot, + isSrcSubdir, + joinPath, + maybeDecodePath, + pathEquals, + resolveLinkTarget, + resolvePath, +} = require('internal/fs/cp/path'); + async function cpFn(src, dest, opts) { // Warn about using preserveTimestamps on 32-bit node if (opts.preserveTimestamps && process.arch === 'ia32') { @@ -138,7 +138,7 @@ function getStats(src, dest, opts) { } async function checkParentDir(destStat, src, dest, opts) { - const destParent = dirname(dest); + const destParent = dirnamePath(dest); const dirExists = await pathExists(destParent); if (dirExists) return getStatsForCopy(destStat, src, dest, opts); await mkdir(destParent, { recursive: true }); @@ -153,15 +153,14 @@ function pathExists(dest) { } // Recursively check if dest parent is a subdirectory of src. -// It works for all file types including symlinks since it -// checks the src and dest inodes. It starts from the deepest -// parent and stops once it reaches the src parent or the root path. +// It works for all file types including symlinks since it checks the src and +// dest inodes. It starts from the deepest parent and stops once it reaches the +// src parent or the root path. async function checkParentPaths(src, srcStat, dest) { - const srcParent = resolve(dirname(src)); - const destParent = resolve(dirname(dest)); - if (destParent === srcParent || destParent === parse(destParent).root) { - return; - } + const srcParent = resolvePath(dirnamePath(src)); + const destParent = resolvePath(dirnamePath(dest)); + if (pathEquals(destParent, srcParent) || isPathRoot(destParent)) return; + let destStat; try { destStat = await stat(destParent, { bigint: true }); @@ -181,17 +180,6 @@ async function checkParentPaths(src, srcStat, dest) { return checkParentPaths(src, srcStat, destParent); } -const normalizePathToArray = (path) => - ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean); - -// Return true if dest is a subdir of src, otherwise false. -// It only checks the path strings. -function isSrcSubdir(src, dest) { - const srcArr = normalizePathToArray(src); - const destArr = normalizePathToArray(dest); - return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur); -} - async function getStatsForCopy(destStat, src, dest, opts) { const statFn = opts.dereference ? stat : lstat; const srcStat = await statFn(src); @@ -321,29 +309,34 @@ async function mkDirAndCopy(srcMode, src, dest, opts) { } async function copyDir(src, dest, opts) { - const dir = await opendir(src); + const dir = await opendir(src, { encoding: 'buffer' }); for await (const { name } of dir) { - const srcItem = join(src, name); - const destItem = join(dest, name); + const srcItem = joinPath(src, name); + const destItem = joinPath(dest, name); const { destStat, skipped } = await checkPaths(srcItem, destItem, opts); if (!skipped) await getStatsForCopy(destStat, srcItem, destItem, opts); } } async function onLink(destStat, src, dest, opts) { - let resolvedSrc = await readlink(src); - if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) { - resolvedSrc = resolve(dirname(src), resolvedSrc); - } - if (!destStat) { - return symlink(resolvedSrc, dest); + let resolvedSrc = maybeDecodePath( + await readlink(src, { encoding: 'buffer' }), + typeof src === 'string', + ); + if (!opts.verbatimSymlinks) { + resolvedSrc = resolveLinkTarget(dirnamePath(src), resolvedSrc); } + if (!destStat) return symlink(resolvedSrc, dest); + let resolvedDest; try { - resolvedDest = await readlink(dest); + resolvedDest = maybeDecodePath( + await readlink(dest, { encoding: 'buffer' }), + typeof dest === 'string', + ); } catch (err) { - // Dest exists and is a regular file or directory, + // Dest exists and is a regular file or directory. // Windows may throw UNKNOWN error. If dest already exists, // fs throws error anyway, so no need to guard against it here. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { @@ -351,27 +344,28 @@ async function onLink(destStat, src, dest, opts) { } throw err; } - if (!isAbsolute(resolvedDest)) { - resolvedDest = resolve(dirname(dest), resolvedDest); + if (!opts.verbatimSymlinks) { + resolvedDest = resolveLinkTarget(dirnamePath(dest), resolvedDest); } - const srcIsDir = fsBinding.internalModuleStat(src) === 1; - + const srcStat = typeof src === 'string' ? null : await stat(src); + const srcIsDir = srcStat ? + srcStat.isDirectory() : + fsBinding.internalModuleStat(src) === 1; if (srcIsDir && isSrcSubdir(resolvedSrc, resolvedDest)) { throw new ERR_FS_CP_EINVAL({ message: `cannot copy ${resolvedSrc} to a subdirectory of self ` + - `${resolvedDest}`, + `${resolvedDest}`, path: dest, syscall: 'cp', errno: EINVAL, code: 'EINVAL', }); } - // Do not copy if src is a subdir of dest since unlinking - // dest in this case would result in removing src contents - // and therefore a broken symlink would be created. - const srcStat = await stat(src); - if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + // Do not copy if src is a subdir of dest since unlinking dest in this case + // would result in removing src contents and therefore a broken symlink. + const followedSrcStat = srcStat || await stat(src); + if (followedSrcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`, path: dest, @@ -391,5 +385,4 @@ async function copyLink(resolvedSrc, dest) { module.exports = { areIdentical, cpFn, - isSrcSubdir, }; diff --git a/lib/internal/fs/cp/path.js b/lib/internal/fs/cp/path.js new file mode 100644 index 00000000000000..1fbefeff947be2 --- /dev/null +++ b/lib/internal/fs/cp/path.js @@ -0,0 +1,135 @@ +'use strict'; + +const { + ArrayPrototypeEvery, + ArrayPrototypeFilter, + ArrayPrototypeMap, + Boolean, + StringPrototypeSplit, +} = primordials; + +const { Buffer, isUtf8 } = require('buffer'); +const { isWindows } = require('internal/util'); +const { + dirname, + isAbsolute, + join, + resolve, + sep, +} = require('path'); + +// POSIX paths are opaque byte sequences. Latin-1 provides a reversible mapping +// between each byte and one JavaScript code unit while the path module handles +// separators and dot segments. Windows paths use their native UTF-8 encoding. +const pathEncoding = isWindows ? 'utf8' : 'latin1'; + +function isUtf8Path(path) { + return isUtf8(path); +} + +function toPathBuffer(path) { + if (typeof path === 'string') return Buffer.from(path); + return Buffer.isBuffer(path) ? path : Buffer.from(path); +} + +function toPathString(path) { + return typeof path === 'string' ? + path : toPathBuffer(path).toString(pathEncoding); +} + +function toBytePathString(path) { + return toPathBuffer(path).toString(pathEncoding); +} + +function fromPathString(path) { + return Buffer.from(path, pathEncoding); +} + +function pathEquals(left, right) { + if (typeof left === 'string' && typeof right === 'string') { + return left === right; + } + return toPathBuffer(left).equals(toPathBuffer(right)); +} + +function joinPath(parent, name) { + if (typeof parent === 'string' && typeof name === 'string') { + return join(parent, name); + } + + if (typeof parent === 'string' && isUtf8Path(name)) { + return join(parent, name.toString()); + } + + return fromPathString(join(toBytePathString(parent), + toBytePathString(name))); +} + +function dirnamePath(path) { + if (typeof path === 'string') return dirname(path); + return fromPathString(dirname(toPathString(path))); +} + +function isAbsolutePath(path) { + return isAbsolute(toPathString(path)); +} + +function resolvePath(path) { + if (typeof path === 'string') return resolve(path); + const pathBuffer = toPathBuffer(path); + if (!isWindows && !isAbsolutePath(pathBuffer)) { + return fromPathString(resolve(toBytePathString(Buffer.concat([ + Buffer.from(process.cwd()), + Buffer.from(sep), + pathBuffer, + ])))); + } + return fromPathString(resolve(toBytePathString(pathBuffer))); +} + +function isPathRoot(path) { + const resolved = resolvePath(path); + return pathEquals(resolved, dirnamePath(resolved)); +} + +function resolvedPathParts(path) { + const resolved = toPathBuffer(resolvePath(path)).toString('latin1'); + const parts = ArrayPrototypeFilter( + StringPrototypeSplit(resolved, sep), + Boolean, + ); + return ArrayPrototypeMap(parts, (part) => Buffer.from(part, 'latin1')); +} + +function isSrcSubdir(src, dest) { + const srcParts = resolvedPathParts(src); + const destParts = resolvedPathParts(dest); + if (srcParts.length > destParts.length) return false; + return ArrayPrototypeEvery( + srcParts, + (part, index) => part.equals(destParts[index]), + ); +} + +function maybeDecodePath(path, preferString) { + return preferString && isUtf8Path(path) ? path.toString() : path; +} + +function resolveLinkTarget(parent, target) { + if (isAbsolutePath(target)) return target; + if (typeof parent === 'string' && typeof target === 'string') { + return resolve(parent, target); + } + return resolvePath(joinPath(parent, target)); +} + +module.exports = { + dirnamePath, + isPathRoot, + isSrcSubdir, + joinPath, + maybeDecodePath, + pathEquals, + resolveLinkTarget, + resolvePath, +}; diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 3e336024a15a3c..894f754daa2b2f 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -65,6 +65,7 @@ const { getOptions, getStatFsFromBinding, getStatsFromBinding, + getValidatedCpPath, getValidatedPath, getReadFileBuffer, getReadFileBufferByteLengthName, @@ -1323,8 +1324,8 @@ async function access(path, mode = F_OK) { async function cp(src, dest, options) { options = validateCpOptions(options); - src = getValidatedPath(src, 'src'); - dest = getValidatedPath(dest, 'dest'); + src = getValidatedCpPath(src, 'src'); + dest = getValidatedCpPath(dest, 'dest'); return lazyLoadCpPromises()(src, dest, options); } diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index 70aaa7e7c58e36..d04c331ad50b34 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -53,7 +53,7 @@ const { once, setOwnProperty, } = require('internal/util'); -const { toPathIfFileURL } = require('internal/url'); +const { fileURLToPathBuffer, isURL, toPathIfFileURL } = require('internal/url'); const { validateAbortSignal, validateBoolean, @@ -859,6 +859,18 @@ const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => { return path; }); +const getValidatedCpPath = hideStackFrames((fileURLOrPath, propName = 'path') => { + if (!isURL(fileURLOrPath)) { + validatePath(fileURLOrPath, propName); + return typeof fileURLOrPath === 'string' || Buffer.isBuffer(fileURLOrPath) ? + fileURLOrPath : Buffer.from(fileURLOrPath); + } + + const pathBuffer = fileURLToPathBuffer(fileURLOrPath); + const path = pathBuffer.toString(); + return Buffer.from(path).equals(pathBuffer) ? path : pathBuffer; +}); + const getValidatedFd = hideStackFrames((fd, propName = 'fd') => { if (ObjectIs(fd, -0)) { return 0; @@ -1129,6 +1141,7 @@ module.exports = { getDirents, getOptions, getValidatedFd, + getValidatedCpPath, getValidatedPath, handleErrorFromBinding, preprocessSymlinkDestination, diff --git a/test/known_issues/test-fs-cp-async-buffer.js b/test/known_issues/test-fs-cp-async-buffer.js deleted file mode 100644 index 4cbf02414749fe..00000000000000 --- a/test/known_issues/test-fs-cp-async-buffer.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -// We expect this test to fail because the implementation of fsPromise.cp -// does not properly support the use of Buffer as the source or destination -// argument like fs.cpSync does. -// Refs: https://github.com/nodejs/node/issues/58634 -// Refs: https://github.com/nodejs/node/issues/58869 - -const common = require('../common'); -const { mkdirSync, promises } = require('fs'); -const { join } = require('path'); -const tmpdir = require('../common/tmpdir'); - -tmpdir.refresh(); - -const tmpA = join(tmpdir.path, 'a'); -const tmpB = join(tmpdir.path, 'b'); - -mkdirSync(tmpA, { recursive: true }); - -promises.cp(Buffer.from(tmpA), Buffer.from(tmpB), { - recursive: true, -}).then(common.mustCall()); diff --git a/test/known_issues/test-fs-cp-filter.js b/test/known_issues/test-fs-cp-filter.js deleted file mode 100644 index 334ade532b33ea..00000000000000 --- a/test/known_issues/test-fs-cp-filter.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -// This test will fail because the implementation does not properly -// handle the case when the `src` or `dest` is a Buffer and the `filter` -// function is utilized when recursively copying directories. -// Refs: https://github.com/nodejs/node/issues/58634 -// Refs: https://github.com/nodejs/node/issues/58869 - -const common = require('../common'); - -const { - cpSync, - mkdirSync, -} = require('fs'); - -const { - join, -} = require('path'); - -const tmpdir = require('../common/tmpdir'); -tmpdir.refresh(); - -const pathA = join(tmpdir.path, 'a'); -const pathAC = join(pathA, 'c'); -const pathB = join(tmpdir.path, 'b'); -mkdirSync(pathAC, { recursive: true }); - -cpSync(Buffer.from(pathA), Buffer.from(pathB), { - recursive: true, - // This should be called multiple times, once for each file/directory, - // but it's only called once in this test because we're expecting this - // to fail. - filter: common.mustCall(() => true, 1), -}); diff --git a/test/known_issues/test-fs-cp-non-utf8.js b/test/known_issues/test-fs-cp-non-utf8.js deleted file mode 100644 index 814859be8ea63e..00000000000000 --- a/test/known_issues/test-fs-cp-non-utf8.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -// Refs: https://github.com/nodejs/node/issues/58634 -// Refs: https://github.com/nodejs/node/issues/58869 - -const common = require('../common'); - -if (!common.isLinux) { - common.skip('This test is only applicable to Linux'); -} - -const assert = require('assert'); -const { join } = require('path'); -const path = require('path'); -const tmpdir = require('../common/tmpdir'); -const { - cpSync, - existsSync, - mkdirSync, - writeFileSync, - readFileSync, -} = require('fs'); -tmpdir.refresh(); - -const tmpdirPath = Buffer.from(join(tmpdir.path, 'a', 'c')); -const sepBuf = Buffer.from(path.sep); - -mkdirSync(tmpdirPath, { recursive: true }); - -// The name is the Shift-JIS encoded version of こんにちは世界, -// or "Hello, World" in Japanese. On Linux systems, this name is -// a valid path name and should be handled correctly by the copy -// operation. However, the implementation of cp makes the assumption -// that the path names are UTF-8 encoded, so while we can create the -// file and check its existence using a Buffer, the recursive cp -// operation will fail because it tries to interpret every file name -// as UTF-8. -const name = Buffer.from([ - 0x82, 0xB1, 0x82, 0xF1, 0x82, 0xC9, 0x82, - 0xBF, 0x82, 0xCD, 0x90, 0x6C, 0x8C, 0x8E, -]); -const testPath = Buffer.concat([tmpdirPath, sepBuf, name]); - -writeFileSync(testPath, 'test content'); -assert.ok(existsSync(testPath)); -assert.strictEqual(readFileSync(testPath, 'utf8'), 'test content'); - -// The cpSync is expected to fail because the implementation does not -// properly handle non-UTF8 names in the path. - -cpSync(join(tmpdir.path, 'a'), join(tmpdir.path, 'b'), { - recursive: true, - filter: common.mustCall(() => true, 1), -}); diff --git a/test/parallel/test-fs-cp-buffer-paths.js b/test/parallel/test-fs-cp-buffer-paths.js new file mode 100644 index 00000000000000..0299f14b4b29c5 --- /dev/null +++ b/test/parallel/test-fs-cp-buffer-paths.js @@ -0,0 +1,497 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + cp, + cpSync, + existsSync, + mkdirSync, + promises, + readFileSync, + readlinkSync, + symlinkSync, + writeFileSync, +} = require('fs'); +const { Buffer } = require('buffer'); +const { join, sep } = require('path'); +const { pathToFileURL } = require('url'); +const tmpdir = require('../common/tmpdir'); + +const sepBuffer = Buffer.from(sep); + +function bufferPath(...parts) { + const result = []; + for (let i = 0; i < parts.length; i++) { + if (i !== 0) result.push(sepBuffer); + result.push(Buffer.isBuffer(parts[i]) ? parts[i] : Buffer.from(parts[i])); + } + return Buffer.concat(result); +} + +function prepare(name) { + const root = join(tmpdir.path, name); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'file.txt'), 'content'); + return { src, dest }; +} + +tmpdir.refresh(); + +{ + const root = join(tmpdir.path, 'buffer-files'); + const src = join(root, 'source.txt'); + const syncDest = join(root, 'sync.txt'); + const callbackDest = join(root, 'callback.txt'); + const promiseDest = join(root, 'promise.txt'); + mkdirSync(root, { recursive: true }); + writeFileSync(src, 'file-content'); + + cpSync(Buffer.from(src), Buffer.from(syncDest)); + assert.strictEqual(readFileSync(syncDest, 'utf8'), 'file-content'); + + cp(Buffer.from(src), Buffer.from(callbackDest), common.mustSucceed(() => { + assert.strictEqual(readFileSync(callbackDest, 'utf8'), 'file-content'); + })); + + promises.cp(Buffer.from(src), Buffer.from(promiseDest)).then(common.mustCall(() => { + assert.strictEqual(readFileSync(promiseDest, 'utf8'), 'file-content'); + })); +} + +{ + const { src, dest } = prepare('sync-buffer'); + const seen = []; + cpSync(Buffer.from(src), Buffer.from(dest), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + seen.push([srcPath, destPath]); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); + assert.strictEqual(seen.length, 2); +} + +{ + const { src, dest } = prepare('sync-string'); + cpSync(src, dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert.strictEqual(typeof destPath, 'string'); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); +} + +{ + const { src, dest } = prepare('sync-url'); + cpSync(pathToFileURL(src), pathToFileURL(dest), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert.strictEqual(typeof destPath, 'string'); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); +} + +{ + const root = join(tmpdir.path, 'sync-buffer-normalized'); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'file.txt'), 'normalized'); + const seen = []; + cpSync(Buffer.from(`${src}${sep}.${sep}`), Buffer.from(`${dest}${sep}.${sep}`), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + seen.push([srcPath, destPath]); + return true; + }, 2), + }); + assert(seen[1][0].equals(Buffer.from(join(src, 'file.txt')))); + assert(seen[1][1].equals(Buffer.from(join(dest, 'file.txt')))); +} + +{ + const { src, dest } = prepare('sync-uint8array'); + cpSync(new Uint8Array(Buffer.from(src)), new Uint8Array(Buffer.from(dest)), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); +} + +{ + const { src, dest } = prepare('sync-mixed-buffer-string'); + cpSync(Buffer.from(src), dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert.strictEqual(typeof destPath, 'string'); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); +} + +{ + const { src, dest } = prepare('sync-mixed-string-buffer'); + cpSync(src, Buffer.from(dest), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert(Buffer.isBuffer(destPath)); + return true; + }, 2), + }); + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); +} + +{ + const { src, dest } = prepare('callback-buffer'); + cp(Buffer.from(src), Buffer.from(dest), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + return true; + }, 2), + }, common.mustSucceed(() => { + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); + })); +} + +{ + const { src, dest } = prepare('promise-buffer'); + promises.cp(Buffer.from(src), Buffer.from(dest), { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + return true; + }, 2), + }).then(common.mustCall(() => { + assert.strictEqual(readFileSync(join(dest, 'file.txt'), 'utf8'), 'content'); + })); +} + +if (common.isLinux) { + const root = join(tmpdir.path, 'non-utf8'); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + const name = Buffer.from([ + 0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, + 0xbf, 0x82, 0xcd, 0x90, 0x6c, 0x8a, 0x45, + ]); + mkdirSync(src, { recursive: true }); + const srcFile = bufferPath(src, name); + const destFile = bufferPath(dest, name); + writeFileSync(srcFile, 'byte-exact'); + + let sawBytePath = false; + cpSync(src, dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + if (Buffer.isBuffer(srcPath)) { + assert(Buffer.isBuffer(destPath)); + assert(srcPath.equals(srcFile)); + assert(destPath.equals(destFile)); + sawBytePath = true; + } + return true; + }, 2), + }); + + assert(sawBytePath); + assert(existsSync(destFile)); + assert.strictEqual(readFileSync(destFile, 'utf8'), 'byte-exact'); + + const urlSource = new URL(`${pathToFileURL(src).href}/${ + Array.from(name, (byte) => `%${byte.toString(16).padStart(2, '0')}`).join('')}`); + const urlDest = new URL(`${pathToFileURL(dest).href}/url-copy`); + cpSync(urlSource, urlDest); + assert.strictEqual(readFileSync(join(dest, 'url-copy'), 'utf8'), 'byte-exact'); +} + + +if (common.isLinux) { + const invalidParent = Buffer.from([0x70, 0x61, 0xff, 0x72, 0x65, 0x6e, 0x74]); + const invalidChild = Buffer.from([0x63, 0x68, 0xfe, 0x69, 0x6c, 0x64]); + + { + const root = join(tmpdir.path, 'nested-non-utf8'); + const src = bufferPath(root, 'src'); + const dest = bufferPath(root, 'dest'); + const srcFile = bufferPath(src, invalidParent, invalidChild, 'file.txt'); + const destFile = bufferPath(dest, invalidParent, invalidChild, 'file.txt'); + mkdirSync(bufferPath(src, invalidParent, invalidChild), { recursive: true }); + writeFileSync(srcFile, 'nested-byte-exact'); + + cpSync(src, dest, { recursive: true }); + assert.strictEqual(readFileSync(destFile, 'utf8'), 'nested-byte-exact'); + } + + { + const root = join(tmpdir.path, 'mixed-path-types'); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + const srcFile = bufferPath(src, invalidParent, 'file.txt'); + const destFile = bufferPath(dest, invalidParent, 'file.txt'); + mkdirSync(bufferPath(src, invalidParent), { recursive: true }); + writeFileSync(srcFile, 'mixed-types'); + + const seen = []; + cpSync(Buffer.from(src), dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + seen.push([srcPath, destPath]); + return true; + }, 3), + }); + + assert.strictEqual(typeof seen[0][0], 'object'); + assert.strictEqual(typeof seen[0][1], 'string'); + assert(Buffer.isBuffer(seen[1][0])); + assert(Buffer.isBuffer(seen[1][1])); + assert.strictEqual(readFileSync(destFile, 'utf8'), 'mixed-types'); + } + + { + const root = join(tmpdir.path, 'self-copy-non-utf8'); + const src = bufferPath(root, invalidParent); + const dest = bufferPath(src, invalidChild); + mkdirSync(src, { recursive: true }); + writeFileSync(bufferPath(src, 'file.txt'), 'self-copy'); + + assert.throws( + () => cpSync(src, dest, { recursive: true }), + { code: 'ERR_FS_CP_EINVAL' }, + ); + } + + { + const root = join(tmpdir.path, 'callback-nested-non-utf8'); + const src = bufferPath(root, 'src'); + const dest = bufferPath(root, 'dest'); + const destFile = bufferPath(dest, invalidParent, invalidChild, 'file.txt'); + mkdirSync(bufferPath(src, invalidParent, invalidChild), { recursive: true }); + writeFileSync(bufferPath(src, invalidParent, invalidChild, 'file.txt'), 'callback-bytes'); + + cp(src, dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + return true; + }, 4), + }, common.mustSucceed(() => { + assert.strictEqual(readFileSync(destFile, 'utf8'), 'callback-bytes'); + })); + } + + { + const root = join(tmpdir.path, 'promise-nested-non-utf8'); + const src = bufferPath(root, 'src'); + const dest = bufferPath(root, 'dest'); + const destFile = bufferPath(dest, invalidParent, invalidChild, 'file.txt'); + mkdirSync(bufferPath(src, invalidParent, invalidChild), { recursive: true }); + writeFileSync(bufferPath(src, invalidParent, invalidChild, 'file.txt'), 'promise-bytes'); + + promises.cp(src, dest, { + recursive: true, + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert(Buffer.isBuffer(destPath)); + return true; + }, 4), + }).then(common.mustCall(() => { + assert.strictEqual(readFileSync(destFile, 'utf8'), 'promise-bytes'); + })); + } + + { + const root = join(tmpdir.path, 'url-non-utf8-all-apis'); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + mkdirSync(src, { recursive: true }); + const name = Buffer.from([0x66, 0x80, 0x6f]); + writeFileSync(bufferPath(src, name), 'url-bytes'); + const encodedName = Array.from( + name, + (byte) => `%${byte.toString(16).padStart(2, '0')}`, + ).join(''); + const source = new URL(`${pathToFileURL(src).href}/${encodedName}`); + const callbackDest = new URL(`${pathToFileURL(dest).href}/callback`); + const promiseDest = new URL(`${pathToFileURL(dest).href}/promise`); + + cp(source, callbackDest, { + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert.strictEqual(typeof destPath, 'string'); + return true; + }), + }, common.mustSucceed(() => { + assert.strictEqual(readFileSync(join(dest, 'callback'), 'utf8'), 'url-bytes'); + })); + promises.cp(source, promiseDest, { + filter: common.mustCall((srcPath, destPath) => { + assert(Buffer.isBuffer(srcPath)); + assert.strictEqual(typeof destPath, 'string'); + return true; + }), + }).then(common.mustCall(() => { + assert.strictEqual(readFileSync(join(dest, 'promise'), 'utf8'), 'url-bytes'); + })); + } + + { + const root = join(tmpdir.path, 'url-non-utf8-destinations'); + const src = join(root, 'source.txt'); + mkdirSync(root, { recursive: true }); + writeFileSync(src, 'destination-url-bytes'); + const names = [ + Buffer.from([0x73, 0x79, 0x6e, 0x63, 0x80]), + Buffer.from([0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x81]), + Buffer.from([0x70, 0x72, 0x6f, 0x6d, 0x69, 0x73, 0x65, 0x82]), + ]; + const urls = names.map((name) => new URL( + Array.from( + name, + (byte) => `%${byte.toString(16).padStart(2, '0')}`, + ).join(''), + pathToFileURL(`${root}${sep}`), + )); + + cpSync(src, urls[0], { + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert(Buffer.isBuffer(destPath)); + return true; + }), + }); + assert.strictEqual(readFileSync(bufferPath(root, names[0]), 'utf8'), 'destination-url-bytes'); + + cp(src, urls[1], { + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert(Buffer.isBuffer(destPath)); + return true; + }), + }, common.mustSucceed(() => { + assert.strictEqual(readFileSync(bufferPath(root, names[1]), 'utf8'), 'destination-url-bytes'); + })); + + promises.cp(src, urls[2], { + filter: common.mustCall((srcPath, destPath) => { + assert.strictEqual(typeof srcPath, 'string'); + assert(Buffer.isBuffer(destPath)); + return true; + }), + }).then(common.mustCall(() => { + assert.strictEqual(readFileSync(bufferPath(root, names[2]), 'utf8'), 'destination-url-bytes'); + })); + } + + { + const root = join(tmpdir.path, 'async-self-copy-non-utf8'); + const callbackSrc = bufferPath(root, 'callback', invalidParent); + const callbackDest = bufferPath(callbackSrc, invalidChild); + mkdirSync(callbackSrc, { recursive: true }); + writeFileSync(bufferPath(callbackSrc, 'file.txt'), 'callback-self-copy'); + cp(callbackSrc, callbackDest, { recursive: true }, common.mustCall((err) => { + assert.strictEqual(err?.code, 'ERR_FS_CP_EINVAL'); + })); + + const promiseSrc = bufferPath(root, 'promise', invalidParent); + const promiseDest = bufferPath(promiseSrc, invalidChild); + mkdirSync(promiseSrc, { recursive: true }); + writeFileSync(bufferPath(promiseSrc, 'file.txt'), 'promise-self-copy'); + assert.rejects( + promises.cp(promiseSrc, promiseDest, { recursive: true }), + { code: 'ERR_FS_CP_EINVAL' }, + ).then(common.mustCall()); + } + + { + const root = join(tmpdir.path, 'symlink-non-utf8'); + const srcDir = bufferPath(root, 'src'); + const destDir = bufferPath(root, 'dest'); + mkdirSync(srcDir, { recursive: true }); + const target = invalidChild; + writeFileSync(bufferPath(srcDir, target), 'symlink-target'); + symlinkSync(target, bufferPath(srcDir, 'link')); + + cpSync(srcDir, destDir, { recursive: true, verbatimSymlinks: true }); + assert(readlinkSync(bufferPath(destDir, 'link'), { encoding: 'buffer' }).equals(target)); + + const resolvedDestDir = bufferPath(root, 'resolved-dest'); + cpSync(srcDir, resolvedDestDir, { recursive: true }); + assert( + readlinkSync(bufferPath(resolvedDestDir, 'link'), { encoding: 'buffer' }) + .equals(bufferPath(srcDir, target)), + ); + } +} + + +if (!common.isWindows) { + const root = join(tmpdir.path, 'verbatim-existing-relative-symlink'); + const target = join(root, 'target'); + mkdirSync(join(target, 'child'), { recursive: true }); + + const src = join(root, 'src'); + const dest = join(root, 'dest'); + symlinkSync('target/child', src); + symlinkSync('target', dest); + assert.rejects( + promises.cp(Buffer.from(src), Buffer.from(dest), { + verbatimSymlinks: true, + }), + { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' }, + ).then(common.mustCall()); +} + +if (common.isLinux) { + const root = join(tmpdir.path, 'absolute-symlink-non-utf8'); + const srcDir = bufferPath(root, 'src'); + const destDir = bufferPath(root, 'dest'); + const invalidName = Buffer.from([0x74, 0x61, 0x72, 0xff, 0x67, 0x65, 0x74]); + const absoluteTarget = bufferPath(root, 'unused', '..', invalidName); + + mkdirSync(srcDir, { recursive: true }); + symlinkSync(absoluteTarget, bufferPath(srcDir, 'link')); + cpSync(srcDir, destDir, { recursive: true }); + assert( + readlinkSync(bufferPath(destDir, 'link'), { encoding: 'buffer' }) + .equals(absoluteTarget), + ); +} + + +if (common.isLinux) { + const root = join(tmpdir.path, 'unicode-\u03c0-parent'); + const src = join(root, 'src'); + const dest = join(root, 'dest'); + const invalidName = Buffer.from([0x66, 0x69, 0xff, 0x6c, 0x65]); + mkdirSync(src, { recursive: true }); + writeFileSync(bufferPath(src, invalidName), 'unicode-parent'); + + cpSync(src, dest, { recursive: true }); + assert.strictEqual( + readFileSync(bufferPath(dest, invalidName), 'utf8'), + 'unicode-parent', + ); +} diff --git a/test/parallel/test-fs-cp-path.js b/test/parallel/test-fs-cp-path.js new file mode 100644 index 00000000000000..c8114013cb204c --- /dev/null +++ b/test/parallel/test-fs-cp-path.js @@ -0,0 +1,131 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const assert = require('assert'); +const { Buffer } = require('buffer'); +const { mkdirSync } = require('fs'); +const path = require('path'); +const { join, sep } = path; +const tmpdir = require('../common/tmpdir'); +const { + dirnamePath, + isPathRoot, + isSrcSubdir, + joinPath, + maybeDecodePath, + pathEquals, + resolveLinkTarget, + resolvePath, +} = require('internal/fs/cp/path'); + +tmpdir.refresh(); + +const paths = [ + '', + '.', + '..', + '/', + '//', + '///', + 'a', + 'a/', + '/a', + '//a', + 'a/b', + '/a/b', + 'a//b', + 'a/./b', + 'a/../b', + '../a', + '../../a', + '/a/../b', + '/..', + '/../../a', +]; + +for (const value of paths) { + const buffer = Buffer.from(value); + assert.strictEqual(dirnamePath(buffer).toString(), path.dirname(value)); + assert.strictEqual(resolvePath(buffer).toString(), path.resolve(value)); + assert.strictEqual(isPathRoot(buffer), path.parse(path.resolve(value)).root === path.resolve(value)); +} + +for (const parent of paths) { + for (const name of ['x', 'xy', '.x', '..x']) { + assert.strictEqual( + joinPath(Buffer.from(parent), Buffer.from(name)).toString(), + path.join(parent, name), + ); + } +} + +for (const src of paths) { + for (const dest of paths) { + const srcParts = path.resolve(src).split(path.sep).filter(Boolean); + const destParts = path.resolve(dest).split(path.sep).filter(Boolean); + const expected = srcParts.every((part, index) => destParts[index] === part); + assert.strictEqual( + isSrcSubdir(Buffer.from(src), Buffer.from(dest)), + expected, + ); + } +} + +assert(pathEquals('abc', Buffer.from('abc'))); +assert.strictEqual(maybeDecodePath(Buffer.from('abc'), true), 'abc'); + +if (!common.isWindows) { + const parent = Buffer.from('/tmp/parent'); + const name = Buffer.from([0x66, 0x80, 0x6f]); + const child = Buffer.concat([parent, Buffer.from('/'), name]); + const grandchild = Buffer.concat([child, Buffer.from('/child')]); + + assert(joinPath(parent, name).equals(child)); + assert(dirnamePath(child).equals(parent)); + assert(isSrcSubdir(child, grandchild)); + assert(!isSrcSubdir(grandchild, child)); + assert.strictEqual(maybeDecodePath(name, true), name); + assert(resolveLinkTarget(parent, name).equals(child)); +} + +if (!common.isWindows) { + const absoluteStringTarget = '/tmp//target/../file'; + const absoluteBufferTarget = Buffer.from('/tmp//opaque/../target'); + const invalidParent = Buffer.from([0x2f, 0x74, 0x6d, 0x70, 0x2f, 0xff]); + + assert.strictEqual( + resolveLinkTarget('/source', absoluteStringTarget), + absoluteStringTarget, + ); + assert( + resolveLinkTarget(invalidParent, absoluteBufferTarget) + .equals(absoluteBufferTarget), + ); +} + + +if (!common.isWindows) { + const originalCwd = process.cwd(); + const unicodeCwd = join(tmpdir.path, 'unicode-\u03c0'); + mkdirSync(unicodeCwd, { recursive: true }); + process.chdir(unicodeCwd); + try { + assert( + resolvePath(Buffer.from('child')) + .equals(Buffer.from(join(unicodeCwd, 'child'))), + ); + const invalidName = Buffer.from([0x63, 0x68, 0xff, 0x69, 0x6c, 0x64]); + assert( + joinPath(unicodeCwd, invalidName) + .equals(Buffer.concat([ + Buffer.from(unicodeCwd), + Buffer.from(sep), + invalidName, + ])), + ); + } finally { + process.chdir(originalCwd); + } +}