@@ -80,33 +80,107 @@ public struct FileSystemError: Error, Equatable, Sendable {
80
80
/// The absolute path to the file associated with the error, if available.
81
81
public let path : AbsolutePath ?
82
82
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 ) {
84
87
self . kind = kind
85
88
self . path = path
86
- }
87
- }
88
-
89
- extension FileSystemError : CustomNSError {
90
- public var errorUserInfo : [ String : Any ] {
91
- return [ NSLocalizedDescriptionKey: " \( self ) " ]
89
+ self . localizedMessage = localizedMessage
92
90
}
93
91
}
94
92
95
93
public extension FileSystemError {
96
- init ( errno: Int32 , _ path: AbsolutePath ) {
94
+ init ( errno: Int32 , _ path: AbsolutePath , localizedMessage : String ? = nil ) {
97
95
switch errno {
98
96
case TSCLibc . EACCES:
99
- self . init ( . invalidAccess, path)
97
+ self . init ( . invalidAccess, path, localizedMessage : localizedMessage )
100
98
case TSCLibc . EISDIR:
101
- self . init ( . isDirectory, path)
99
+ self . init ( . isDirectory, path, localizedMessage : localizedMessage )
102
100
case TSCLibc . ENOENT:
103
- self . init ( . noEntry, path)
101
+ self . init ( . noEntry, path, localizedMessage : localizedMessage )
104
102
case TSCLibc . ENOTDIR:
105
- self . init ( . notDirectory, path)
103
+ self . init ( . notDirectory, path, localizedMessage : localizedMessage )
106
104
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
+
108
180
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)
110
184
}
111
185
}
112
186
}
@@ -411,8 +485,15 @@ private struct LocalFileSystem: FileSystem {
411
485
}
412
486
413
487
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
+ }
416
497
}
417
498
418
499
func hasAttribute( _ name: FileSystemAttribute , _ path: AbsolutePath ) -> Bool {
@@ -473,21 +554,20 @@ private struct LocalFileSystem: FileSystem {
473
554
}
474
555
475
556
func getDirectoryContents( _ path: AbsolutePath ) throws -> [ String ] {
476
- #if canImport(Darwin)
477
- return try FileManager . default. contentsOfDirectory ( atPath: path. pathString)
478
- #else
479
557
do {
480
558
return try FileManager . default. contentsOfDirectory ( atPath: path. pathString)
481
559
} 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 {
484
561
var userInfo = error. userInfo
485
562
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 )
487
564
}
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)
489
570
}
490
- #endif
491
571
}
492
572
493
573
func createDirectory( _ path: AbsolutePath , recursive: Bool ) throws {
@@ -496,81 +576,78 @@ private struct LocalFileSystem: FileSystem {
496
576
497
577
do {
498
578
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)
499
587
} catch {
500
588
if isDirectory ( path) {
501
589
// `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`.
503
591
// Since we have the expected end result, this is fine.
504
592
return
505
593
}
506
- throw error
594
+ // Handle any other error types (e.g., Swift errors)
595
+ throw FileSystemError ( . unknownOSError, path)
507
596
}
508
597
}
509
598
510
599
func createSymbolicLink( _ path: AbsolutePath , pointingAt destination: AbsolutePath , relative: Bool ) throws {
511
600
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
+ }
513
609
}
514
610
515
611
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) ) )
537
616
}
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)
539
622
}
540
-
541
- return data. bytes
542
623
}
543
624
544
625
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) )
561
629
}
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)
563
635
}
564
636
}
565
637
566
638
func writeFileContents( _ path: AbsolutePath , bytes: ByteString , atomically: Bool ) throws {
567
- // Perform non-atomic writes using the fast path.
568
639
if !atomically {
569
640
return try writeFileContents ( path, bytes: bytes)
570
641
}
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)
574
651
}
575
652
}
576
653
@@ -588,18 +665,26 @@ private struct LocalFileSystem: FileSystem {
588
665
func chmod( _ mode: FileMode , path: AbsolutePath , options: Set < FileMode . Option > ) throws {
589
666
guard exists ( path) else { return }
590
667
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
+ }
596
674
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
+ }
603
688
}
604
689
605
690
try setMode ( path: path. pathString)
@@ -624,14 +709,28 @@ private struct LocalFileSystem: FileSystem {
624
709
guard exists ( sourcePath) else { throw FileSystemError ( . noEntry, sourcePath) }
625
710
guard !exists( destinationPath)
626
711
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
+ }
628
720
}
629
721
630
722
func move( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
631
723
guard exists ( sourcePath) else { throw FileSystemError ( . noEntry, sourcePath) }
632
724
guard !exists( destinationPath)
633
725
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
+ }
635
734
}
636
735
637
736
func withLock< T> ( on path: AbsolutePath , type: FileLock . LockType , blocking: Bool , _ body: ( ) throws -> T ) throws -> T {
@@ -648,10 +747,17 @@ private struct LocalFileSystem: FileSystem {
648
747
}
649
748
650
749
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
+ }
655
761
}
656
762
}
657
763
0 commit comments