@@ -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
0 commit comments