diff --git a/packages/compiler/shims/fs-extra/core/api.ts b/packages/compiler/shims/fs-extra/core/api.ts index 4d34a112f..a17a5f9a7 100644 --- a/packages/compiler/shims/fs-extra/core/api.ts +++ b/packages/compiler/shims/fs-extra/core/api.ts @@ -925,6 +925,64 @@ async function included(state: CopyState, src: string, dest: string): Promise { + if (!destExists) return fsp.symlink(target, dest); + let destLinked: string; + try { + destLinked = await fsp.readlink(dest); + } catch (err) { + const code = codeOf(err); + if (code === "EINVAL" || code === "UNKNOWN") return fsp.symlink(target, dest); + throw err; + } + const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked; + checkLinkTargets(target, destTarget, (await fsp.stat(dest)).isDirectory()); + await fsp.unlink(dest); + await fsp.symlink(target, dest); +} + +function copyLinkSync( + target: string, + dest: string, + destExists: boolean, + dereference: boolean, +): void { + if (!destExists) { + fs.symlinkSync(target, dest); + return; + } + let destLinked: string; + try { + destLinked = fs.readlinkSync(dest); + } catch (err) { + const code = codeOf(err); + if (code !== "EINVAL" && code !== "UNKNOWN") throw err; + fs.symlinkSync(target, dest); + return; + } + const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked; + checkLinkTargets(target, destTarget, fs.statSync(dest).isDirectory()); + fs.unlinkSync(dest); + fs.symlinkSync(target, dest); +} + +/** Replacing a destination symlink never points it into its own target, + * nor removes a directory link the source lives under. */ +function checkLinkTargets(target: string, destTarget: string, destIsDirectory: boolean): void { + if (isSrcSubdir(target, destTarget)) + throw new Error(`Cannot copy '${target}' to a subdirectory of itself, '${destTarget}'.`); + if (destIsDirectory && isSrcSubdir(destTarget, target)) + throw new Error(`Cannot overwrite '${destTarget}' with '${target}'.`); +} + async function copyEntry( state: CopyState, src: string, @@ -954,9 +1012,12 @@ async function copyEntry( } if (srcStat.isSymbolicLink()) { const linked = await fsp.readlink(src); - const target = state.dereference ? path.resolve(process.cwd(), linked) : linked; - if (destExists) await fsp.unlink(dest); - await fsp.symlink(target, dest); + await copyLink( + state.dereference ? path.resolve(process.cwd(), linked) : linked, + dest, + destExists, + state.dereference, + ); return; } if (destExists) { @@ -1027,10 +1088,14 @@ function copyEntrySync( return; } if (srcStat.isSymbolicLink()) { + const dereference = state.dereference === true; const linked = fs.readlinkSync(src); - const target = state.dereference === true ? path.resolve(process.cwd(), linked) : linked; - if (destExists) fs.unlinkSync(dest); - fs.symlinkSync(target, dest); + copyLinkSync( + dereference ? path.resolve(process.cwd(), linked) : linked, + dest, + destExists, + dereference, + ); return; } if (destExists) { diff --git a/tests/corpus/fs-extra-package/main.ts b/tests/corpus/fs-extra-package/main.ts index 8066c1657..e152a287c 100644 --- a/tests/corpus/fs-extra-package/main.ts +++ b/tests/corpus/fs-extra-package/main.ts @@ -115,6 +115,30 @@ async function main(): Promise { } console.log(fs.existsSync(file), fs.existsSync(path.join(root, "deep", "inner"))); + // A symlink source over an existing destination entry: a regular file is + // kept and the copy fails with EEXIST; a destination symlink is replaced. + const linkSrc = path.join(root, "link-src"); + fs.mkdirSync(linkSrc); + fs.symlinkSync(file, path.join(linkSrc, "entry")); + for (const sync of [true, false]) { + const linkDest = path.join(root, sync ? "link-dest-sync" : "link-dest-async"); + fs.mkdirSync(linkDest); + fs.writeFileSync(path.join(linkDest, "entry"), "kept"); + try { + if (sync) fs.copySync(linkSrc, linkDest, { overwrite: false }); + else await fs.copy(linkSrc, linkDest, { overwrite: false }); + } catch (err) { + console.log((err as NodeJS.ErrnoException).code, shown(root, (err as Error).message)); + } + const entry = path.join(linkDest, "entry"); + console.log(fs.readFileSync(entry, "utf8"), fs.lstatSync(entry).isSymbolicLink()); + } + const relinkDest = path.join(root, "relink-dest"); + fs.mkdirSync(relinkDest); + fs.symlinkSync(data, path.join(relinkDest, "entry")); + fs.copySync(linkSrc, relinkDest); + console.log(shown(root, fs.readlinkSync(path.join(relinkDest, "entry")))); + console.log(await readStream(file)); console.log((await fs.promises.readFile(file, "utf8")).length); console.log(fs.readFileSync(file, "utf8").length);