Skip to content

Conversation

@PazerOP
Copy link
Contributor

@PazerOP PazerOP commented Dec 13, 2025

I couldn't see anything immediately obvious banning AI PRs, hope this isn't a bother. I had Claude create this fix for part of the build process of https://neutralino.js.org/.

When copying a directory containing symlinks, if the destination already contains symlinks pointing to the same target as the source symlinks, the operation would incorrectly fail with "Cannot copy X to a subdirectory of itself" error.

This happened because the isSrcSubdir() check returns true when two paths are identical. However, two different symlinks that happen to point to the same target are distinct filesystem entries that should be copyable.

The fix adds a check to ensure the resolved paths are actually different before applying the subdirectory constraints. When both symlinks resolve to the same target (resolvedSrc === resolvedDest), we skip the subdirectory checks and allow the copy to proceed, which correctly replaces the destination symlink with the source symlink.

Fixes issue where:

  • Source: ./src/link → symlink to /tmp/target
  • Dest: ./dest/link → symlink to /tmp/target
  • copySync('./src', './dest') would fail

Added comprehensive test cases for:

  • Copy symlink when dest has no existing symlink
  • Overwrite symlink when dest has symlink pointing to SAME target
  • Overwrite symlink when dest has symlink pointing to DIFFERENT target
  • File symlinks with same target
  • Edge case: copying symlink to itself still fails
  • Edge case: copying symlink into subdirectory of its target works

@RyanZim
Copy link
Collaborator

RyanZim commented Dec 15, 2025

Nevermind the macOS CI failures; unrelated to this PR.

@RyanZim
Copy link
Collaborator

RyanZim commented Dec 15, 2025

macOS CI fixed in #1061; feel free to rebase.

Copy link
Collaborator

@RyanZim RyanZim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; just might be good to have a little more test completeness. This is a fix for #1019, right?

@RyanZim RyanZim requested review from JPeer264 and manidlou December 15, 2025 19:47
@RyanZim
Copy link
Collaborator

RyanZim commented Dec 15, 2025

I couldn't see anything immediately obvious banning AI PRs, hope this isn't a bother. I had Claude create this fix

@PazerOP Not a bother at all, this PR is in better shape than a lot of human-created PRs I see. OOI, what was your prompt?

@PazerOP
Copy link
Contributor Author

PazerOP commented Dec 16, 2025

I couldn't see anything immediately obvious banning AI PRs, hope this isn't a bother. I had Claude create this fix

@PazerOP Not a bother at all, this PR is in better shape than a lot of human-created PRs I see. OOI, what was your prompt?

It's a bit of a stretch to call this a prompt IMO, basically handed it the answer. This is the result of debugging the problem in Claude Code CLI, then saying "just describe in detail the issue and how to fix it, i'll delegate it to another coding agent". Then I stuck this in the web edition of Claude Code Opus 4.5. It took 8.5 minutes.

Issue: fs-extra incorrectly fails when copying symlinks that point to the same target

Problem Description

When using fs-extra's copy() or copySync() functions to copy a directory containing symlinks, if the destination already contains symlinks pointing to the same target as the source symlinks, the operation fails with:

Error: Source and destination must not be the same.

This is incorrect behavior. Two different symlinks that happen to point to the same target are not the same file - they are distinct filesystem entries that should be copyable/overwritable.

Root Cause

In /node_modules/fs-extra/lib/util/stat.js, lines 9-10:

const stat = (file) => nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file)
const statSync = (file) => nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file)

The fs.stat() function follows symlinks and returns the stat of the target file. When checkPaths() (line 34-46) compares source and destination using areIdentical(), it compares the resolved target's inode, not the symlink's inode.

So when you have:

Source: ./extensions/bin → symlink to /tmp/build/bin
Dest: ./dist/extensions/bin → symlink to /tmp/build/bin

Both fs.stat() calls return the stat of /tmp/build/bin, making areIdentical() return true (same inode), triggering the error.

Proposed Fix

Change fs.stat to fs.lstat so it compares the symlinks themselves, not their targets:

// Use lstat to not follow symlinks - two different symlinks pointing to the same target are NOT the same file
const stat = (file) => nodeSupportsBigInt ? fs.lstat(file, { bigint: true }) : fs.lstat(file)
const statSync = (file) => nodeSupportsBigInt ? fs.lstatSync(file, { bigint: true }) : fs.lstatSync(file)

Affected Versions

fs-extra: At least version 9.x-11.x (current in neutralino-cli)
This affects any OS with symlink support: macOS, Linux, Windows (with developer mode)

Test Cases Needed

Basic symlink copy: Copy directory with symlink, no existing dest → should work
Overwrite symlink with same target: Copy symlink over existing symlink pointing to same target → should work (currently fails)
Overwrite symlink with different target: Copy symlink over existing symlink pointing to different target → should work
Copy regular files: Ensure the change doesn't break regular file identity checks
Prevent actual same-file copy: cp file file should still fail (same actual path)
Cross-device symlink copy: Symlinks on different volumes pointing to same target

Files to Modify

fs-extra (lib/util/stat.js):

Line 9: fs.stat → fs.lstat
Line 10: fs.statSync → fs.lstatSync

Reproduction Steps
Setup

mkdir -p /tmp/target /tmp/src /tmp/dest
echo "content" > /tmp/target/file.txt
ln -s /tmp/target /tmp/src/link
ln -s /tmp/target /tmp/dest/link
This fails with current fs-extra

node -e "require('fs-extra').copySync('/tmp/src', '/tmp/dest')"
Error: Source and destination must not be the same.

Related Issues

https://github.com/jprichardson/node-fs-extra/issues/657
https://github.com/jprichardson/node-fs-extra/issues/792

claude and others added 2 commits December 15, 2025 22:01
…target

When copying a directory containing symlinks, if the destination already
contains symlinks pointing to the same target as the source symlinks,
the operation would incorrectly fail with "Cannot copy X to a subdirectory
of itself" error.

This happened because the isSrcSubdir() check returns true when two paths
are identical. However, two different symlinks that happen to point to
the same target are distinct filesystem entries that should be copyable.

The fix adds a check to ensure the resolved paths are actually different
before applying the subdirectory constraints. When both symlinks resolve
to the same target (resolvedSrc === resolvedDest), we skip the subdirectory
checks and allow the copy to proceed, which correctly replaces the
destination symlink with the source symlink.

Fixes issue where:
- Source: ./src/link → symlink to /tmp/target
- Dest: ./dest/link → symlink to /tmp/target
- copySync('./src', './dest') would fail

Added comprehensive test cases for:
- Copy symlink when dest has no existing symlink
- Overwrite symlink when dest has symlink pointing to SAME target
- Overwrite symlink when dest has symlink pointing to DIFFERENT target
- File symlinks with same target
- Edge case: copying symlink to itself still fails
- Edge case: copying symlink into subdirectory of its target works
- Add DIFFERENT target tests for file symlinks (async + sync)
- Add direct symlink-to-symlink copy test for issue jprichardson#1019
- Add issue jprichardson#1027 reference to existing tests
@PazerOP PazerOP force-pushed the claude/fix-symlink-copy-issue-01YKukoUh1UW9oynPRTtqhjM branch from 8614b40 to fba3725 Compare December 16, 2025 06:26
@PazerOP
Copy link
Contributor Author

PazerOP commented Dec 16, 2025

I believe this fixes #639, #1019, #1027, and #1043.

Copy link
Collaborator

@JPeer264 JPeer264 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Amazing that you added so many tests for it

@PazerOP
Copy link
Contributor Author

PazerOP commented Dec 17, 2025

Thanks. I think someone needs to approve the Github Actions workflow to run before this can be merged.

Copy link
Collaborator

@manidlou manidlou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@RyanZim RyanZim merged commit ddc46f7 into jprichardson:master Dec 18, 2025
18 checks passed
@RyanZim
Copy link
Collaborator

RyanZim commented Dec 18, 2025

Will release tomorrow.

BTW, unless things have changed since then, as per #1019 (comment), this is also a bug in Node core fs.cp. We should report this and explore if backporting this fix is feasible.

@RyanZim
Copy link
Collaborator

RyanZim commented Dec 18, 2025

Published as v11.3.3 🎉

@PazerOP PazerOP deleted the claude/fix-symlink-copy-issue-01YKukoUh1UW9oynPRTtqhjM branch December 20, 2025 06:03
valentineus pushed a commit to valentineus/strapi-plugin-checkbox-list that referenced this pull request Feb 5, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fs-extra](https://github.com/jprichardson/node-fs-extra) | [`^10.0.0` → `^11.0.0`](https://renovatebot.com/diffs/npm/fs-extra/10.1.0/11.3.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fs-extra/11.3.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fs-extra/10.1.0/11.3.3?slim=true) |

---

### Release Notes

<details>
<summary>jprichardson/node-fs-extra (fs-extra)</summary>

### [`v11.3.3`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1133--2025-12-18)

[Compare Source](jprichardson/node-fs-extra@11.3.2...11.3.3)

- Fix copying symlink when destination is a symlink to the same target ([#&#8203;1019](jprichardson/node-fs-extra#1019), [#&#8203;1060](jprichardson/node-fs-extra#1060))

### [`v11.3.2`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1132--2025-09-15)

[Compare Source](jprichardson/node-fs-extra@11.3.1...11.3.2)

- Fix spurrious `UnhandledPromiseRejectionWarning` that could occur when calling `.copy()` in some cases ([#&#8203;1056](jprichardson/node-fs-extra#1056), [#&#8203;1058](jprichardson/node-fs-extra#1058))

### [`v11.3.1`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1131--2025-08-05)

[Compare Source](jprichardson/node-fs-extra@11.3.0...11.3.1)

- Fix case where `move`/`moveSync` could incorrectly think files are identical on Windows ([#&#8203;1050](jprichardson/node-fs-extra#1050))

### [`v11.3.0`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1130--2025-01-15)

[Compare Source](jprichardson/node-fs-extra@11.2.0...11.3.0)

- Add promise support for newer `fs` methods ([#&#8203;1044](jprichardson/node-fs-extra#1044), [#&#8203;1045](jprichardson/node-fs-extra#1045))
- Use `fs.opendir` in `copy()`/`copySync()` for better perf/scalability ([#&#8203;972](jprichardson/node-fs-extra#972), [#&#8203;1028](jprichardson/node-fs-extra#1028))

### [`v11.2.0`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1120--2023-11-27)

[Compare Source](jprichardson/node-fs-extra@11.1.1...11.2.0)

- Copy directory contents in parallel for better performance ([#&#8203;1026](jprichardson/node-fs-extra#1026))
- Refactor internal code to use `async`/`await` ([#&#8203;1020](jprichardson/node-fs-extra#1020))

### [`v11.1.1`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1111--2023-03-20)

[Compare Source](jprichardson/node-fs-extra@11.1.0...11.1.1)

- Preserve timestamps when moving files across devices ([#&#8203;992](jprichardson/node-fs-extra#992), [#&#8203;994](jprichardson/node-fs-extra#994))

### [`v11.1.0`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1110--2022-11-29)

[Compare Source](jprichardson/node-fs-extra@11.0.0...11.1.0)

- Re-add `main` field to `package.json` for better TypeScript compatibility ([#&#8203;979](jprichardson/node-fs-extra#979), [#&#8203;981](jprichardson/node-fs-extra#981))

### [`v11.0.0`](https://github.com/jprichardson/node-fs-extra/blob/HEAD/CHANGELOG.md#1100--2022-11-28)

[Compare Source](jprichardson/node-fs-extra@10.1.0...11.0.0)

##### Breaking Changes

- Don't allow requiring `fs-extra/lib/SOMETHING` (switched to `exports`) ([#&#8203;974](jprichardson/node-fs-extra#974))
- Require Node v14.14+ ([#&#8203;968](jprichardson/node-fs-extra#968), [#&#8203;969](jprichardson/node-fs-extra#969))

##### New Features

- Add `fs-extra/esm` for ESM named export support; see [docs](https://github.com/jprichardson/node-fs-extra#esm) for details ([#&#8203;746](jprichardson/node-fs-extra#746), [#&#8203;974](jprichardson/node-fs-extra#974))
- Add promise support for `fs.readv()` ([#&#8203;970](jprichardson/node-fs-extra#970))

##### Bugfixes

- Don't `stat` filtered items in `copy*` ([#&#8203;965](jprichardson/node-fs-extra#965), [#&#8203;971](jprichardson/node-fs-extra#971))
- Remove buggy stats check in `copy` ([#&#8203;918](jprichardson/node-fs-extra#918), [#&#8203;976](jprichardson/node-fs-extra#976))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4wLjUiLCJ1cGRhdGVkSW5WZXIiOiI0My4wLjUiLCJ0YXJnZXRCcmFuY2giOiJtYXN0ZXIiLCJsYWJlbHMiOlsiYXV0b21hdGVkIiwiZGVwZW5kZW5jaWVzIl19-->

Reviewed-on: https://code.popov.link/valentineus/strapi-plugin-checkbox-list/pulls/6
Co-authored-by: renovate[bot] <renovatebot@noreply.localhost>
Co-committed-by: renovate[bot] <renovatebot@noreply.localhost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants