@@ -80,33 +80,107 @@ public struct FileSystemError: Error, Equatable, Sendable {
8080 /// The absolute path to the file associated with the error, if available.
8181 public let path: AbsolutePath?
8282
83- public init(_ kind: Kind, _ path: AbsolutePath? = nil) {
83+ /// A localized message describing the error, if available.
84+ public let localizedMessage: String?
85+
86+ public init(_ kind: Kind, _ path: AbsolutePath? = nil, localizedMessage: String? = nil) {
8487 self.kind = kind
8588 self.path = path
86- }
87- }
88-
89- extension FileSystemError: CustomNSError {
90- public var errorUserInfo: [String : Any] {
91- return [NSLocalizedDescriptionKey: "\(self)"]
89+ self.localizedMessage = localizedMessage
9290 }
9391}
9492
9593public extension FileSystemError {
96- init(errno: Int32, _ path: AbsolutePath) {
94+ init(errno: Int32, _ path: AbsolutePath, localizedMessage: String? = nil ) {
9795 switch errno {
9896 case TSCLibc.EACCES:
99- self.init(.invalidAccess, path)
97+ self.init(.invalidAccess, path, localizedMessage: localizedMessage )
10098 case TSCLibc.EISDIR:
101- self.init(.isDirectory, path)
99+ self.init(.isDirectory, path, localizedMessage: localizedMessage )
102100 case TSCLibc.ENOENT:
103- self.init(.noEntry, path)
101+ self.init(.noEntry, path, localizedMessage: localizedMessage )
104102 case TSCLibc.ENOTDIR:
105- self.init(.notDirectory, path)
103+ self.init(.notDirectory, path, localizedMessage: localizedMessage )
106104 case TSCLibc.EEXIST:
107- self.init(.alreadyExistsAtDestination, path)
105+ self.init(.alreadyExistsAtDestination, path, localizedMessage: localizedMessage)
106+ default:
107+ self.init(.ioError(code: errno), path, localizedMessage: localizedMessage)
108+ }
109+ }
110+
111+ init(error: POSIXError, _ path: AbsolutePath, localizedMessage: String? = nil) {
112+ switch error.code {
113+ case .ENOENT:
114+ self.init(.noEntry, path, localizedMessage: localizedMessage)
115+ case .EACCES:
116+ self.init(.invalidAccess, path, localizedMessage: localizedMessage)
117+ case .EISDIR:
118+ self.init(.isDirectory, path, localizedMessage: localizedMessage)
119+ case .ENOTDIR:
120+ self.init(.notDirectory, path, localizedMessage: localizedMessage)
121+ case .EEXIST:
122+ self.init(.alreadyExistsAtDestination, path, localizedMessage: localizedMessage)
123+ default:
124+ self.init(.ioError(code: error.code.rawValue), path, localizedMessage: localizedMessage)
125+ }
126+ }
127+ }
128+
129+ // MARK: - NSError to FileSystemError Mapping
130+ extension FileSystemError {
131+ /// Maps NSError codes to appropriate FileSystemError kinds
132+ /// This centralizes error mapping logic and ensures consistency across file operations
133+ ///
134+ /// - Parameters:
135+ /// - error: The NSError to map
136+ /// - path: The file path associated with the error
137+ /// - Returns: A FileSystemError with appropriate semantic mapping
138+ static func from(nsError error: NSError, path: AbsolutePath) -> FileSystemError {
139+ // Extract localized description from NSError
140+ let localizedMessage = error.localizedDescription.isEmpty ? nil : error.localizedDescription
141+
142+ // First, check for POSIX errors in the underlying error chain
143+ // POSIX errors provide more precise semantic information
144+ if let posixError = error.userInfo[NSUnderlyingErrorKey] as? POSIXError {
145+ return FileSystemError(error: posixError, path, localizedMessage: localizedMessage)
146+ }
147+
148+ // Handle Cocoa domain errors with proper semantic mapping
149+ guard error.domain == NSCocoaErrorDomain else {
150+ // For non-Cocoa errors, preserve the original error information
151+ return FileSystemError(.ioError(code: Int32(error.code)), path, localizedMessage: localizedMessage)
152+ }
153+
154+ // Map common Cocoa error codes to semantic FileSystemError kinds
155+ switch error.code {
156+ // File not found errors
157+ case NSFileReadNoSuchFileError, NSFileNoSuchFileError:
158+ return FileSystemError(.noEntry, path, localizedMessage: localizedMessage)
159+
160+ // Permission denied errors
161+ case NSFileReadNoPermissionError, NSFileWriteNoPermissionError:
162+ return FileSystemError(.invalidAccess, path, localizedMessage: localizedMessage)
163+
164+ // File already exists errors
165+ case NSFileWriteFileExistsError:
166+ return FileSystemError(.alreadyExistsAtDestination, path, localizedMessage: localizedMessage)
167+
168+ // Read-only volume errors
169+ case NSFileWriteVolumeReadOnlyError:
170+ return FileSystemError(.invalidAccess, path, localizedMessage: localizedMessage)
171+
172+ // File corruption or invalid format errors
173+ case NSFileReadCorruptFileError:
174+ return FileSystemError(.ioError(code: Int32(error.code)), path, localizedMessage: localizedMessage)
175+
176+ // Directory-related errors
177+ case NSFileReadInvalidFileNameError:
178+ return FileSystemError(.notDirectory, path, localizedMessage: localizedMessage)
179+
108180 default:
109- self.init(.ioError(code: errno), path)
181+ // For any other Cocoa error, wrap it as an IO error preserving the original code
182+ // This ensures we don't lose diagnostic information
183+ return FileSystemError(.ioError(code: Int32(error.code)), path, localizedMessage: localizedMessage)
110184 }
111185 }
112186}
@@ -411,8 +485,15 @@ private struct LocalFileSystem: FileSystem {
411485 }
412486
413487 func getFileInfo(_ path: AbsolutePath) throws -> FileInfo {
414- let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString)
415- return FileInfo(attrs)
488+ do {
489+ let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString)
490+ return FileInfo(attrs)
491+ } catch let error as NSError {
492+ throw FileSystemError.from(nsError: error, path: path)
493+ } catch {
494+ // Handle any other error types (e.g., Swift errors)
495+ throw FileSystemError(.unknownOSError, path)
496+ }
416497 }
417498
418499 func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool {
@@ -473,21 +554,20 @@ private struct LocalFileSystem: FileSystem {
473554 }
474555
475556 func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
476- #if canImport(Darwin)
477- return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
478- #else
479557 do {
480558 return try FileManager.default.contentsOfDirectory(atPath: path.pathString)
481559 } catch let error as NSError {
482- // Fixup error from corelibs-foundation.
483- if error.code == CocoaError.fileReadNoSuchFile.rawValue, !error.userInfo.keys.contains(NSLocalizedDescriptionKey) {
560+ if error.code == CocoaError.fileReadNoSuchFile.rawValue {
484561 var userInfo = error.userInfo
485562 userInfo[NSLocalizedDescriptionKey] = "The folder “\(path.basename)” doesn’t exist."
486- throw NSError(domain: error.domain, code: error.code, userInfo: userInfo)
563+ throw FileSystemError.from(nsError: NSError(domain: error.domain, code: error.code, userInfo: userInfo), path: path )
487564 }
488- throw error
565+ // Convert NSError to FileSystemError with proper semantic mapping
566+ throw FileSystemError.from(nsError: error, path: path)
567+ } catch {
568+ // Handle any other error types (e.g., Swift errors)
569+ throw FileSystemError(.unknownOSError, path)
489570 }
490- #endif
491571 }
492572
493573 func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
@@ -496,81 +576,78 @@ private struct LocalFileSystem: FileSystem {
496576
497577 do {
498578 try FileManager.default.createDirectory(atPath: path.pathString, withIntermediateDirectories: recursive, attributes: [:])
579+ } catch let error as NSError {
580+ if isDirectory(path) {
581+ // `createDirectory` failed but we have a directory now. This might happen if the directory is created
582+ // by another process between the check above and the call to `createDirectory`.
583+ // Since we have the expected end result, this is fine.
584+ return
585+ }
586+ throw FileSystemError.from(nsError: error, path: path)
499587 } catch {
500588 if isDirectory(path) {
501589 // `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`.
590+ // by another process between the check above and the call to `createDirectory`.
503591 // Since we have the expected end result, this is fine.
504592 return
505593 }
506- throw error
594+ // Handle any other error types (e.g., Swift errors)
595+ throw FileSystemError(.unknownOSError, path)
507596 }
508597 }
509598
510599 func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
511600 let destString = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString
512- try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
601+ do {
602+ try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString)
603+ } catch let error as NSError {
604+ throw FileSystemError.from(nsError: error, path: path)
605+ } catch {
606+ // Handle any other error types (e.g., Swift errors)
607+ throw FileSystemError(.unknownOSError, path)
608+ }
513609 }
514610
515611 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
612+ do {
613+ let dataContent = try Data(contentsOf: URL(fileURLWithPath: path.pathString))
614+ return dataContent.withUnsafeBytes { bytes in
615+ ByteString(Array(bytes.bindMemory(to: UInt8.self)))
537616 }
538- data.send(tmpBuffer[0..<n])
617+ } catch let error as NSError {
618+ throw FileSystemError.from(nsError: error, path: path)
619+ } catch {
620+ // Handle any other error types (e.g., Swift errors)
621+ throw FileSystemError(.unknownOSError, path)
539622 }
540-
541- return data.bytes
542623 }
543624
544625 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)
626+ do {
627+ try bytes.withData {
628+ try $0.write(to: URL(fileURLWithPath: path.pathString))
561629 }
562- break
630+ } catch let error as NSError {
631+ throw FileSystemError.from(nsError: error, path: path)
632+ } catch {
633+ // Handle any other error types (e.g., Swift errors)
634+ throw FileSystemError(.unknownOSError, path)
563635 }
564636 }
565637
566638 func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
567- // Perform non-atomic writes using the fast path.
568639 if !atomically {
569640 return try writeFileContents(path, bytes: bytes)
570641 }
571-
572- try bytes.withData {
573- try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
642+ do {
643+ try bytes.withData {
644+ try $0.write(to: URL(fileURLWithPath: path.pathString), options: .atomic)
645+ }
646+ } catch let error as NSError {
647+ throw FileSystemError.from(nsError: error, path: path)
648+ } catch {
649+ // Handle any other error types (e.g., Swift errors)
650+ throw FileSystemError(.unknownOSError, path)
574651 }
575652 }
576653
@@ -588,18 +665,26 @@ private struct LocalFileSystem: FileSystem {
588665 func chmod(_ mode: FileMode, path: AbsolutePath, options: Set<FileMode.Option>) throws {
589666 guard exists(path) else { return }
590667 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- }
668+ do {
669+ let attrs = try FileManager.default.attributesOfItem(atPath: path)
670+ // Skip if only files should be changed.
671+ if options.contains(.onlyFiles) && attrs[.type] as? FileAttributeType != .typeRegular {
672+ return
673+ }
596674
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)
675+ // Compute the new mode for this file.
676+ let currentMode = attrs[.posixPermissions] as! Int16
677+ let newMode = mode.setMode(currentMode)
678+ guard newMode != currentMode else { return }
679+ try FileManager.default.setAttributes([.posixPermissions : newMode],
680+ ofItemAtPath: path)
681+ } catch let error as NSError {
682+ let absolutePath = try AbsolutePath(validating: path)
683+ throw FileSystemError.from(nsError: error, path: absolutePath)
684+ } catch {
685+ let absolutePath = try AbsolutePath(validating: path)
686+ throw FileSystemError(.unknownOSError, absolutePath)
687+ }
603688 }
604689
605690 try setMode(path: path.pathString)
@@ -624,14 +709,28 @@ private struct LocalFileSystem: FileSystem {
624709 guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) }
625710 guard !exists(destinationPath)
626711 else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
627- try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL)
712+ do {
713+ try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL)
714+ } catch let error as NSError {
715+ throw FileSystemError.from(nsError: error, path: destinationPath)
716+ } catch {
717+ // Handle any other error types (e.g., Swift errors)
718+ throw FileSystemError(.unknownOSError, destinationPath)
719+ }
628720 }
629721
630722 func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
631723 guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) }
632724 guard !exists(destinationPath)
633725 else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) }
634- try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
726+ do {
727+ try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
728+ } catch let error as NSError {
729+ throw FileSystemError.from(nsError: error, path: destinationPath)
730+ } catch {
731+ // Handle any other error types (e.g., Swift errors)
732+ throw FileSystemError(.unknownOSError, destinationPath)
733+ }
635734 }
636735
637736 func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
@@ -648,10 +747,17 @@ private struct LocalFileSystem: FileSystem {
648747 }
649748
650749 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]
750+ do {
751+ let result = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: path.asURL, create: false)
752+ let resultPath = try AbsolutePath(validating: result.path)
753+ // Foundation returns a path that is unique every time, so we return both that path, as well as its parent.
754+ return [resultPath, resultPath.parentDirectory]
755+ } catch let error as NSError {
756+ throw FileSystemError.from(nsError: error, path: path)
757+ } catch {
758+ // Handle any other error types (e.g., Swift errors)
759+ throw FileSystemError(.unknownOSError, path)
760+ }
655761 }
656762}
657763
0 commit comments