Skip to content

Commit 43fb628

Browse files
authored
Merge pull request #2717 from pvieito/symlinks-fix
[SR-7857] URL.resolvingSymlinksInPath() now recursively resolves symlinks
2 parents cd58de4 + 0198ebc commit 43fb628

File tree

4 files changed

+117
-7
lines changed

4 files changed

+117
-7
lines changed

Sources/Foundation/FileManager+POSIX.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,18 +459,35 @@ extension FileManager {
459459
This method replaces pathContentOfSymbolicLinkAtPath:
460460
*/
461461
internal func _destinationOfSymbolicLink(atPath path: String) throws -> String {
462+
let bufferSize = Int(PATH_MAX + 1)
463+
let buffer = try [Int8](unsafeUninitializedCapacity: bufferSize) { buffer, initializedCount in
464+
let len = try _fileSystemRepresentation(withPath: path) { (path) -> Int in
465+
return readlink(path, buffer.baseAddress!, bufferSize)
466+
}
467+
guard len >= 0 else {
468+
throw _NSErrorWithErrno(errno, reading: true, path: path)
469+
}
470+
initializedCount = len
471+
}
472+
return self.string(withFileSystemRepresentation: buffer, length: buffer.count)
473+
}
474+
475+
internal func _recursiveDestinationOfSymbolicLink(atPath path: String) throws -> String {
476+
// Throw error if path is not a symbolic link:
477+
let path = try _destinationOfSymbolicLink(atPath: path)
478+
462479
let bufSize = Int(PATH_MAX + 1)
463480
var buf = [Int8](repeating: 0, count: bufSize)
464-
let len = try _fileSystemRepresentation(withPath: path) {
465-
readlink($0, &buf, bufSize)
481+
let _resolvedPath = try _fileSystemRepresentation(withPath: path) {
482+
realpath($0, &buf)
466483
}
467-
if len < 0 {
484+
guard let resolvedPath = _resolvedPath else {
468485
throw _NSErrorWithErrno(errno, reading: true, path: path)
469486
}
470487

471-
return self.string(withFileSystemRepresentation: buf, length: Int(len))
488+
return String(cString: resolvedPath)
472489
}
473-
490+
474491
/* Returns a String with a canonicalized path for the element at the specified path. */
475492
internal func _canonicalizedPath(toFileAtPath path: String) throws -> String {
476493
let bufSize = Int(PATH_MAX + 1)

Sources/Foundation/FileManager+Win32.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,28 @@ extension FileManager {
412412
}
413413
return substitutePath
414414
}
415+
416+
private func _realpath(_ path: String) -> String {
417+
return (try? _destinationOfSymbolicLink(atPath: path)) ?? path
418+
}
419+
420+
internal func _recursiveDestinationOfSymbolicLink(atPath path: String) throws -> String {
421+
// Throw error if path is not a symbolic link:
422+
var previousIterationDestination = try _destinationOfSymbolicLink(atPath: path)
423+
424+
// Same recursion limit as in Darwin:
425+
let symbolicLinkRecursionLimit = 32
426+
for _ in 0..<symbolicLinkRecursionLimit {
427+
let iterationDestination = _realpath(previousIterationDestination)
428+
if previousIterationDestination == iterationDestination {
429+
return iterationDestination
430+
}
431+
previousIterationDestination = iterationDestination
432+
}
433+
434+
// As in Darwin Foundation, after the recursion limit we return the initial path without resolution.
435+
return path
436+
}
415437

416438
internal func _canonicalizedPath(toFileAtPath path: String) throws -> String {
417439
let hFile: HANDLE = try FileManager.default._fileSystemRepresentation(withPath: path) {

Sources/Foundation/FileManager.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,10 @@ open class FileManager : NSObject {
630630
open func destinationOfSymbolicLink(atPath path: String) throws -> String {
631631
return try _destinationOfSymbolicLink(atPath: path)
632632
}
633+
634+
internal func recursiveDestinationOfSymbolicLink(atPath path: String) throws -> String {
635+
return try _recursiveDestinationOfSymbolicLink(atPath: path)
636+
}
633637

634638
internal func extraErrorInfo(srcPath: String?, dstPath: String?, userVariant: String?) -> [String : Any] {
635639
var result = [String : Any]()
@@ -1123,8 +1127,8 @@ open class FileManager : NSObject {
11231127
}
11241128

11251129
internal func _tryToResolveTrailingSymlinkInPath(_ path: String) -> String? {
1126-
// destinationOfSymbolicLink(atPath:) will fail if the path is not a symbolic link
1127-
guard let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else {
1130+
// FileManager.recursiveDestinationOfSymbolicLink(atPath:) will fail if the path is not a symbolic link
1131+
guard let destination = try? self.recursiveDestinationOfSymbolicLink(atPath: path) else {
11281132
return nil
11291133
}
11301134

Tests/Foundation/Tests/TestFileManager.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,72 @@ class TestFileManager : XCTestCase {
934934
}
935935
XCTAssertNil(try? fm.linkItem(atPath: srcLink, toPath: destLink), "Creating link where one already exists")
936936
}
937+
938+
func test_resolvingSymlinksInPath() throws {
939+
try withTemporaryDirectory { (temporaryDirectoryURL, _) in
940+
// Initialization
941+
var baseURL = temporaryDirectoryURL
942+
.appendingPathComponent("test_resolvingSymlinksInPath")
943+
.appendingPathComponent(UUID().uuidString)
944+
945+
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
946+
let testData = Data([0x01])
947+
948+
baseURL.resolveSymlinksInPath()
949+
baseURL.standardize()
950+
951+
let link1URL = baseURL.appendingPathComponent("link1")
952+
let link2URL = baseURL.appendingPathComponent("link2")
953+
let link3URL = baseURL.appendingPathComponent("link3")
954+
let testFileURL = baseURL.appendingPathComponent("test").standardized.absoluteURL
955+
956+
try FileManager.default.removeItem(at: baseURL)
957+
958+
// A) Check non-symbolic linking resolution
959+
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
960+
try testData.write(to: testFileURL)
961+
let resolvedURL_A = testFileURL.resolvingSymlinksInPath().standardized.absoluteURL
962+
XCTAssertEqual(resolvedURL_A.path, testFileURL.path)
963+
try FileManager.default.removeItem(at: baseURL)
964+
965+
// B) Check simple symbolic linking resolution
966+
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
967+
try testData.write(to: testFileURL)
968+
try FileManager.default.createSymbolicLink(at: link1URL, withDestinationURL: testFileURL)
969+
let resolvedURL_B = link1URL.resolvingSymlinksInPath().standardized.absoluteURL
970+
XCTAssertEqual(resolvedURL_B.path, testFileURL.path)
971+
try FileManager.default.removeItem(at: baseURL)
972+
973+
// C) Check recursive symbolic linking resolution
974+
//
975+
// Note: The symbolic link creation order is important as in some platforms like Windows
976+
// symlinks can only be created pointing to existing targets.
977+
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
978+
try testData.write(to: testFileURL)
979+
try FileManager.default.createSymbolicLink(at: link2URL, withDestinationURL: testFileURL)
980+
try FileManager.default.createSymbolicLink(at: link1URL, withDestinationURL: link2URL)
981+
let resolvedURL_C = link1URL.resolvingSymlinksInPath().standardized.absoluteURL
982+
XCTAssertEqual(resolvedURL_C.path, testFileURL.path)
983+
984+
// C-2) And that FileManager.destinationOfSymbolicLink(atPath:) does not recursively resolves them
985+
let destinationOfSymbolicLink1 = try FileManager.default.destinationOfSymbolicLink(atPath: link1URL.path)
986+
let destinationOfSymbolicLink1URL = URL(fileURLWithPath: destinationOfSymbolicLink1).standardized.absoluteURL
987+
XCTAssertEqual(destinationOfSymbolicLink1URL.path, link2URL.path)
988+
try FileManager.default.removeItem(at: baseURL)
989+
990+
#if !os(Windows)
991+
// D) Check infinite recursion loops are stopped and the function returns the intial symlink
992+
//
993+
// Note: This cannot be tested on platforms which only support creating symlinks pointing to existing targets.
994+
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
995+
try FileManager.default.createSymbolicLink(at: link1URL, withDestinationURL: link2URL)
996+
try FileManager.default.createSymbolicLink(at: link2URL, withDestinationURL: link3URL)
997+
try FileManager.default.createSymbolicLink(at: link3URL, withDestinationURL: link1URL)
998+
let resolvedURL_D = link1URL.resolvingSymlinksInPath()
999+
XCTAssertEqual(resolvedURL_D.lastPathComponent, link1URL.lastPathComponent)
1000+
#endif
1001+
}
1002+
}
9371003

9381004
func test_homedirectoryForUser() {
9391005
let filemanger = FileManager.default
@@ -1849,6 +1915,7 @@ VIDEOS=StopgapVideos
18491915
("test_subpathsOfDirectoryAtPath", test_subpathsOfDirectoryAtPath),
18501916
("test_copyItemAtPathToPath", test_copyItemAtPathToPath),
18511917
("test_linkItemAtPathToPath", testExpectedToFailOnAndroid(test_linkItemAtPathToPath, "Android doesn't allow hard links")),
1918+
("test_resolvingSymlinksInPath", test_resolvingSymlinksInPath),
18521919
("test_homedirectoryForUser", test_homedirectoryForUser),
18531920
("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser),
18541921
("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath),

0 commit comments

Comments
 (0)