From a9f8fdf353754174b65ca7b00620ba85d90c47f6 Mon Sep 17 00:00:00 2001 From: fortmarek Date: Sat, 22 Nov 2025 18:02:38 +0100 Subject: [PATCH] Fix LocalFileSystem.move() to not follow symlinks when checking source existence The move() method was using exists(sourcePath) which follows symlinks by default. This caused failures when moving a symlink whose target had already been moved, because it would try to follow the symlink to validate the target exists. Now uses exists(sourcePath, followSymlink: false) to check if the symlink itself exists, allowing symlinks to be moved even if their targets have been relocated. This fixes an issue where extracting package archives with symlinks could fail if files were moved in alphabetical order and a target file was moved before its symlink. Added test testMoveSymlinkWithMovedTarget() to verify this behavior. --- Sources/TSCBasic/FileSystem.swift | 2 +- Tests/TSCBasicTests/FileSystemTests.swift | 41 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/TSCBasic/FileSystem.swift b/Sources/TSCBasic/FileSystem.swift index 1ee457bc..69e39e89 100644 --- a/Sources/TSCBasic/FileSystem.swift +++ b/Sources/TSCBasic/FileSystem.swift @@ -735,7 +735,7 @@ private struct LocalFileSystem: FileSystem { } func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } + guard exists(sourcePath, followSymlink: false) else { throw FileSystemError(.noEntry, sourcePath) } guard !exists(destinationPath) else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } do { diff --git a/Tests/TSCBasicTests/FileSystemTests.swift b/Tests/TSCBasicTests/FileSystemTests.swift index e31b27cc..27e5bdc4 100644 --- a/Tests/TSCBasicTests/FileSystemTests.swift +++ b/Tests/TSCBasicTests/FileSystemTests.swift @@ -511,6 +511,47 @@ class FileSystemTests: XCTestCase { } } + func testMoveSymlinkWithMovedTarget() throws { + let fs = TSCBasic.localFileSystem + + try testWithTemporaryDirectory { tmpdir in + let sourceDir = tmpdir.appending(component: "source") + let destDir = tmpdir.appending(component: "dest") + + try fs.createDirectory(sourceDir) + try fs.createDirectory(destDir) + + // Create a regular file that will be the symlink target + let targetFile = sourceDir.appending(component: "AFile.swift") + try fs.writeFileContents(targetFile, bytes: "// Target file content\n") + + // Create a relative symlink pointing to the target file + let symlink = sourceDir.appending(component: "ZLinkToFile.swift") + try fs.createSymbolicLink(symlink, pointingAt: targetFile, relative: true) + + XCTAssertTrue(fs.isSymlink(symlink)) + XCTAssertTrue(fs.exists(symlink)) + + // Move the target file first + let movedTarget = destDir.appending(component: "AFile.swift") + try fs.move(from: targetFile, to: movedTarget) + + // Now try to move the symlink - this should succeed even though its target has moved + // The symlink's target will be broken after the move, but the symlink itself should be moveable + let movedSymlink = destDir.appending(component: "ZLinkToFile.swift") + XCTAssertNoThrow(try fs.move(from: symlink, to: movedSymlink)) + + XCTAssertFalse(fs.exists(symlink, followSymlink: false)) + XCTAssertTrue(fs.exists(movedSymlink, followSymlink: false)) + XCTAssertTrue(fs.isSymlink(movedSymlink)) + + XCTAssertTrue(fs.exists(movedSymlink, followSymlink: true)) + let symlinkContent = try fs.readFileContents(movedSymlink) + let targetContent = try fs.readFileContents(movedTarget) + XCTAssertEqual(symlinkContent, targetContent) + } + } + // MARK: InMemoryFileSystem Tests func testInMemoryBasics() throws {