Skip to content

Commit a9f8fdf

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 a9f8fdf

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ 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+
guard exists(sourcePath, followSymlink: false) else { throw FileSystemError(.noEntry, sourcePath) }
739739
guard !exists(destinationPath)
740740
else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
741741
do {

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,47 @@ 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
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+
XCTAssertFalse(fs.exists(symlink, followSymlink: false))
545+
XCTAssertTrue(fs.exists(movedSymlink, followSymlink: false))
546+
XCTAssertTrue(fs.isSymlink(movedSymlink))
547+
548+
XCTAssertTrue(fs.exists(movedSymlink, followSymlink: true))
549+
let symlinkContent = try fs.readFileContents(movedSymlink)
550+
let targetContent = try fs.readFileContents(movedTarget)
551+
XCTAssertEqual(symlinkContent, targetContent)
552+
}
553+
}
554+
514555
// MARK: InMemoryFileSystem Tests
515556

516557
func testInMemoryBasics() throws {

0 commit comments

Comments
 (0)