@@ -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