Skip to content

Commit e80613a

Browse files
committed
Fix FileSystem read/writeFileContents to use Data API
- using the Data API fixes issues with long filenames on Windows - fix error handling to map NSErrors to FileSystemError
1 parent c4309ae commit e80613a

File tree

3 files changed

+194
-82
lines changed

3 files changed

+194
-82
lines changed

Sources/TSCBasic/FileSystem.swift

Lines changed: 178 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,79 @@ public extension FileSystemError {
109109
self.init(.ioError(code: errno), path)
110110
}
111111
}
112+
113+
init(error: POSIXError, _ path: AbsolutePath) {
114+
switch error.code {
115+
case .ENOENT:
116+
self.init(.noEntry, path)
117+
case .EACCES:
118+
self.init(.invalidAccess, path)
119+
case .EISDIR:
120+
self.init(.isDirectory, path)
121+
case .ENOTDIR:
122+
self.init(.notDirectory, path)
123+
case .EEXIST:
124+
self.init(.alreadyExistsAtDestination, path)
125+
default:
126+
self.init(.ioError(code: error.code.rawValue), path)
127+
}
128+
}
129+
}
130+
131+
// MARK: - NSError to FileSystemError Mapping
132+
extension FileSystemError {
133+
/// Maps NSError codes to appropriate FileSystemError kinds
134+
/// This centralizes error mapping logic and ensures consistency across file operations
135+
///
136+
/// - Parameters:
137+
/// - error: The NSError to map
138+
/// - path: The file path associated with the error
139+
/// - Returns: A FileSystemError with appropriate semantic mapping
140+
static func from(nsError error: NSError, path: AbsolutePath) -> FileSystemError {
141+
// First, check for POSIX errors in the underlying error chain
142+
// POSIX errors provide more precise semantic information
143+
if let posixError = error.userInfo[NSUnderlyingErrorKey] as? POSIXError {
144+
return FileSystemError(error: posixError, path)
145+
}
146+
147+
// Handle Cocoa domain errors with proper semantic mapping
148+
guard error.domain == NSCocoaErrorDomain else {
149+
// For non-Cocoa errors, preserve the original error information
150+
return FileSystemError(.ioError(code: Int32(error.code)), path)
151+
}
152+
153+
// Map common Cocoa error codes to semantic FileSystemError kinds
154+
switch error.code {
155+
// File not found errors
156+
case NSFileReadNoSuchFileError, NSFileNoSuchFileError:
157+
return FileSystemError(.noEntry, path)
158+
159+
// Permission denied errors
160+
case NSFileReadNoPermissionError, NSFileWriteNoPermissionError:
161+
return FileSystemError(.invalidAccess, path)
162+
163+
// File already exists errors
164+
case NSFileWriteFileExistsError:
165+
return FileSystemError(.alreadyExistsAtDestination, path)
166+
167+
// Read-only volume errors
168+
case NSFileWriteVolumeReadOnlyError:
169+
return FileSystemError(.invalidAccess, path)
170+
171+
// File corruption or invalid format errors
172+
case NSFileReadCorruptFileError:
173+
return FileSystemError(.ioError(code: Int32(error.code)), path)
174+
175+
// Directory-related errors
176+
case NSFileReadInvalidFileNameError:
177+
return FileSystemError(.notDirectory, path)
178+
179+
default:
180+
// For any other Cocoa error, wrap it as an IO error preserving the original code
181+
// This ensures we don't lose diagnostic information
182+
return FileSystemError(.ioError(code: Int32(error.code)), path)
183+
}
184+
}
112185
}
113186

114187
/// Defines the file modes.
@@ -411,8 +484,15 @@ private struct LocalFileSystem: FileSystem {
411484
}
412485

413486
func getFileInfo(_ path: AbsolutePath) throws -> FileInfo {
414-
let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString)
415-
return FileInfo(attrs)
487+
do {
488+
let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString)
489+
return FileInfo(attrs)
490+
} catch let error as NSError {
491+
throw FileSystemError.from(nsError: error, path: path)
492+
} catch {
493+
// Handle any other error types (e.g., Swift errors)
494+
throw FileSystemError(.unknownOSError, path)
495+
}
416496
}
417497

418498
func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool {
@@ -473,21 +553,15 @@ private struct LocalFileSystem: FileSystem {
473553
}
474554

475555
func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
476-
#if canImport(Darwin)
477-
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
478-
#else
479556
do {
480557
return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
481558
} catch let error as NSError {
482-
// Fixup error from corelibs-foundation.
483-
if error.code == CocoaError.fileReadNoSuchFile.rawValue, !error.userInfo.keys.contains(NSLocalizedDescriptionKey) {
484-
var userInfo = error.userInfo
485-
userInfo[NSLocalizedDescriptionKey] = "The folder “\(path.basename)” doesn’t exist."
486-
throw NSError(domain: error.domain, code: error.code, userInfo: userInfo)
487-
}
488-
throw error
559+
// Convert NSError to FileSystemError with proper semantic mapping
560+
throw FileSystemError.from(nsError: error, path: path)
561+
} catch {
562+
// Handle any other error types (e.g., Swift errors)
563+
throw FileSystemError(.unknownOSError, path)
489564
}
490-
#endif
491565
}
492566

493567
func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
@@ -496,81 +570,78 @@ private struct LocalFileSystem: FileSystem {
496570

497571
do {
498572
try FileManager.default.createDirectory(atPath: path.pathString, withIntermediateDirectories: recursive, attributes: [:])
573+
} catch let error as NSError {
574+
if isDirectory(path) {
575+
// `createDirectory` failed but we have a directory now. This might happen if the directory is created
576+
// by another process between the check above and the call to `createDirectory`.
577+
// Since we have the expected end result, this is fine.
578+
return
579+
}
580+
throw FileSystemError.from(nsError: error, path: path)
499581
} catch {
500582
if isDirectory(path) {
501583
// `createDirectory` failed but we have a directory now. This might happen if the directory is created
502-
// by another process between the check above and the call to `createDirectory`.
584+
// by another process between the check above and the call to `createDirectory`.
503585
// Since we have the expected end result, this is fine.
504586
return
505587
}
506-
throw error
588+
// Handle any other error types (e.g., Swift errors)
589+
throw FileSystemError(.unknownOSError, path)
507590
}
508591
}
509592

510593
func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
511594
let destString = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
512-
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
595+
do {
596+
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
597+
} catch let error as NSError {
598+
throw FileSystemError.from(nsError: error, path: path)
599+
} catch {
600+
// Handle any other error types (e.g., Swift errors)
601+
throw FileSystemError(.unknownOSError, path)
602+
}
513603
}
514604

515605
func readFileContents(_ path: AbsolutePath) throws -> ByteString {
516-
// Open the file.
517-
guard let fp = fopen(path.pathString, "rb") else {
518-
throw FileSystemError(errno: errno, path)
519-
}
520-
defer { fclose(fp) }
521-
522-
// Read the data one block at a time.
523-
let data = BufferedOutputByteStream()
524-
var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12)
525-
while true {
526-
let n = fread(&tmpBuffer, 1, tmpBuffer.count, fp)
527-
if n < 0 {
528-
if errno == EINTR { continue }
529-
throw FileSystemError(.ioError(code: errno), path)
530-
}
531-
if n == 0 {
532-
let errno = ferror(fp)
533-
if errno != 0 {
534-
throw FileSystemError(.ioError(code: errno), path)
535-
}
536-
break
606+
do {
607+
let dataContent = try Data(contentsOf: URL(fileURLWithPath: path.pathString))
608+
return dataContent.withUnsafeBytes { bytes in
609+
ByteString(Array(bytes.bindMemory(to: UInt8.self)))
537610
}
538-
data.send(tmpBuffer[0..<n])
611+
} catch let error as NSError {
612+
throw FileSystemError.from(nsError: error, path: path)
613+
} catch {
614+
// Handle any other error types (e.g., Swift errors)
615+
throw FileSystemError(.unknownOSError, path)
539616
}
540-
541-
return data.bytes
542617
}
543618

544619
func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws {
545-
// Open the file.
546-
guard let fp = fopen(path.pathString, "wb") else {
547-
throw FileSystemError(errno: errno, path)
548-
}
549-
defer { fclose(fp) }
550-
551-
// Write the data in one chunk.
552-
var contents = bytes.contents
553-
while true {
554-
let n = fwrite(&contents, 1, contents.count, fp)
555-
if n < 0 {
556-
if errno == EINTR { continue }
557-
throw FileSystemError(.ioError(code: errno), path)
558-
}
559-
if n != contents.count {
560-
throw FileSystemError(.mismatchedByteCount(expected: contents.count, actual: n), path)
620+
do {
621+
try bytes.withData {
622+
try $0.write(to: URL(fileURLWithPath: path.pathString))
561623
}
562-
break
624+
} catch let error as NSError {
625+
throw FileSystemError.from(nsError: error, path: path)
626+
} catch {
627+
// Handle any other error types (e.g., Swift errors)
628+
throw FileSystemError(.unknownOSError, path)
563629
}
564630
}
565631

566632
func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
567-
// Perform non-atomic writes using the fast path.
568633
if !atomically {
569634
return try writeFileContents(path, bytes: bytes)
570635
}
571-
572-
try bytes.withData {
573-
try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
636+
do {
637+
try bytes.withData {
638+
try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
639+
}
640+
} catch let error as NSError {
641+
throw FileSystemError.from(nsError: error, path: path)
642+
} catch {
643+
// Handle any other error types (e.g., Swift errors)
644+
throw FileSystemError(.unknownOSError, path)
574645
}
575646
}
576647

@@ -588,18 +659,26 @@ private struct LocalFileSystem: FileSystem {
588659
func chmod(_ mode: FileMode, path: AbsolutePath, options: Set<FileMode.Option>) throws {
589660
guard exists(path) else { return }
590661
func setMode(path: String) throws {
591-
let attrs = try FileManager.default.attributesOfItem(atPath: path)
592-
// Skip if only files should be changed.
593-
if options.contains(.onlyFiles) && attrs[.type] as? FileAttributeType != .typeRegular {
594-
return
595-
}
662+
do {
663+
let attrs = try FileManager.default.attributesOfItem(atPath: path)
664+
// Skip if only files should be changed.
665+
if options.contains(.onlyFiles) && attrs[.type] as? FileAttributeType != .typeRegular {
666+
return
667+
}
596668

597-
// Compute the new mode for this file.
598-
let currentMode = attrs[.posixPermissions] as! Int16
599-
let newMode = mode.setMode(currentMode)
600-
guard newMode != currentMode else { return }
601-
try FileManager.default.setAttributes([.posixPermissions : newMode],
602-
ofItemAtPath: path)
669+
// Compute the new mode for this file.
670+
let currentMode = attrs[.posixPermissions] as! Int16
671+
let newMode = mode.setMode(currentMode)
672+
guard newMode != currentMode else { return }
673+
try FileManager.default.setAttributes([.posixPermissions : newMode],
674+
ofItemAtPath: path)
675+
} catch let error as NSError {
676+
let absolutePath = try AbsolutePath(validating: path)
677+
throw FileSystemError.from(nsError: error, path: absolutePath)
678+
} catch {
679+
let absolutePath = try AbsolutePath(validating: path)
680+
throw FileSystemError(.unknownOSError, absolutePath)
681+
}
603682
}
604683

605684
try setMode(path: path.pathString)
@@ -624,14 +703,28 @@ private struct LocalFileSystem: FileSystem {
624703
guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) }
625704
guard !exists(destinationPath)
626705
else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
627-
try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL)
706+
do {
707+
try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL)
708+
} catch let error as NSError {
709+
throw FileSystemError.from(nsError: error, path: destinationPath)
710+
} catch {
711+
// Handle any other error types (e.g., Swift errors)
712+
throw FileSystemError(.unknownOSError, destinationPath)
713+
}
628714
}
629715

630716
func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
631717
guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) }
632718
guard !exists(destinationPath)
633719
else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
634-
try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
720+
do {
721+
try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
722+
} catch let error as NSError {
723+
throw FileSystemError.from(nsError: error, path: destinationPath)
724+
} catch {
725+
// Handle any other error types (e.g., Swift errors)
726+
throw FileSystemError(.unknownOSError, destinationPath)
727+
}
635728
}
636729

637730
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
@@ -648,10 +741,17 @@ private struct LocalFileSystem: FileSystem {
648741
}
649742

650743
func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] {
651-
let result = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: path.asURL, create: false)
652-
let path = try AbsolutePath(validating: result.path)
653-
// Foundation returns a path that is unique every time, so we return both that path, as well as its parent.
654-
return [path, path.parentDirectory]
744+
do {
745+
let result = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: path.asURL, create: false)
746+
let resultPath = try AbsolutePath(validating: result.path)
747+
// Foundation returns a path that is unique every time, so we return both that path, as well as its parent.
748+
return [resultPath, resultPath.parentDirectory]
749+
} catch let error as NSError {
750+
throw FileSystemError.from(nsError: error, path: path)
751+
} catch {
752+
// Handle any other error types (e.g., Swift errors)
753+
throw FileSystemError(.unknownOSError, path)
754+
}
655755
}
656756
}
657757

Sources/TSCBasic/PathShims.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,27 @@ public func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath {
7171
/// Creates a new, empty directory at `path`. If needed, any non-existent ancestor paths are also created. If there is
7272
/// already a directory at `path`, this function does nothing (in particular, this is not considered to be an error).
7373
public func makeDirectories(_ path: AbsolutePath) throws {
74-
try FileManager.default.createDirectory(atPath: path.pathString, withIntermediateDirectories: true, attributes: [:])
74+
do {
75+
try FileManager.default.createDirectory(atPath: path.pathString, withIntermediateDirectories: true, attributes: [:])
76+
} catch let error as NSError {
77+
throw FileSystemError.from(nsError: error, path: path)
78+
} catch {
79+
throw FileSystemError(.unknownOSError, path)
80+
}
7581
}
7682

7783
/// Creates a symbolic link at `path` whose content points to `dest`. If `relative` is true, the symlink contents will
7884
/// be a relative path, otherwise it will be absolute.
7985
@available(*, deprecated, renamed: "localFileSystem.createSymbolicLink")
8086
public func createSymlink(_ path: AbsolutePath, pointingAt dest: AbsolutePath, relative: Bool = true) throws {
8187
let destString = relative ? dest.relative(to: path.parentDirectory).pathString : dest.pathString
82-
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
88+
do {
89+
try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
90+
} catch let error as NSError {
91+
throw FileSystemError.from(nsError: error, path: path)
92+
} catch {
93+
throw FileSystemError(.unknownOSError, path)
94+
}
8395
}
8496

8597
/**

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class FileSystemTests: XCTestCase {
313313
XCTAssertEqual(try! fs.readFileContents(filePath), "Hello, new world!")
314314

315315
// Check read/write of a directory.
316-
XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), filePath.parentDirectory)) {
316+
XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) {
317317
_ = try fs.readFileContents(filePath.parentDirectory)
318318
}
319319
XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) {
@@ -327,7 +327,7 @@ class FileSystemTests: XCTestCase {
327327
#else
328328
let root = AbsolutePath("/")
329329
#endif
330-
XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), root)) {
330+
XCTAssertThrows(FileSystemError(.isDirectory, root)) {
331331
_ = try fs.readFileContents(root)
332332

333333
}

0 commit comments

Comments
 (0)