Skip to content

Commit 3d768ca

Browse files
committed
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.
1 parent b157c62 commit 3d768ca

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,9 @@ private struct LocalFileSystem: FileSystem {
735735
}
736736

737737
func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
738-
guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) }
738+
// Don't follow symlinks when checking if source exists - we want to check if the symlink itself exists,
739+
// not whether its target exists. This allows moving symlinks even if their targets have been moved.
740+
guard exists(sourcePath, followSymlink: false) else { throw FileSystemError(.noEntry, sourcePath) }
739741
guard !exists(destinationPath)
740742
else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
741743
do {

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,49 @@ class FileSystemTests: XCTestCase {
511511
}
512512
}
513513

514+
func testMoveSymlinkWithMovedTarget() throws {
515+
let fs = TSCBasic.localFileSystem
516+
517+
try testWithTemporaryDirectory { tmpdir in
518+
let sourceDir = tmpdir.appending(component: "source")
519+
let destDir = tmpdir.appending(component: "dest")
520+
521+
try fs.createDirectory(sourceDir)
522+
try fs.createDirectory(destDir)
523+
524+
// Create a regular file that will be the symlink target
525+
let targetFile = sourceDir.appending(component: "AFile.swift")
526+
try fs.writeFileContents(targetFile, bytes: "// Target file content\n")
527+
528+
// Create a relative symlink pointing to the target file
529+
let symlink = sourceDir.appending(component: "ZLinkToFile.swift")
530+
try fs.createSymbolicLink(symlink, pointingAt: targetFile, relative: true)
531+
532+
XCTAssertTrue(fs.isSymlink(symlink))
533+
XCTAssertTrue(fs.exists(symlink))
534+
535+
// Move the target file first (alphabetically it comes before the symlink)
536+
let movedTarget = destDir.appending(component: "AFile.swift")
537+
try fs.move(from: targetFile, to: movedTarget)
538+
539+
// Now try to move the symlink - this should succeed even though its target has moved
540+
// The symlink's target will be broken after the move, but the symlink itself should be moveable
541+
let movedSymlink = destDir.appending(component: "ZLinkToFile.swift")
542+
XCTAssertNoThrow(try fs.move(from: symlink, to: movedSymlink))
543+
544+
// Verify the symlink was moved
545+
XCTAssertFalse(fs.exists(symlink, followSymlink: false))
546+
XCTAssertTrue(fs.exists(movedSymlink, followSymlink: false))
547+
XCTAssertTrue(fs.isSymlink(movedSymlink))
548+
549+
// Verify the symlink now points correctly (both files are in the same directory again)
550+
XCTAssertTrue(fs.exists(movedSymlink, followSymlink: true))
551+
let symlinkContent = try fs.readFileContents(movedSymlink)
552+
let targetContent = try fs.readFileContents(movedTarget)
553+
XCTAssertEqual(symlinkContent, targetContent)
554+
}
555+
}
556+
514557
// MARK: InMemoryFileSystem Tests
515558

516559
func testInMemoryBasics() throws {

0 commit comments

Comments
 (0)