@@ -650,7 +650,7 @@ extension SystemFileHandle.SendableView {
650
650
}
651
651
652
652
@_spi ( Testing)
653
- public func _close( materialize: Bool ) -> Result < Void , FileSystemError > {
653
+ public func _close( materialize: Bool , failRenameat2WithEINVAL : Bool = false ) -> Result < Void , FileSystemError > {
654
654
let descriptor : FileDescriptor ? = self . lifecycle. withLockedValue { lifecycle in
655
655
switch lifecycle {
656
656
case let . open( descriptor) :
@@ -666,7 +666,7 @@ extension SystemFileHandle.SendableView {
666
666
}
667
667
668
668
// Materialize then close.
669
- let materializeResult = self . _materialize ( materialize, descriptor: descriptor)
669
+ let materializeResult = self . _materialize ( materialize, descriptor: descriptor, failRenameat2WithEINVAL : failRenameat2WithEINVAL )
670
670
671
671
return Result {
672
672
try descriptor. close ( )
@@ -778,7 +778,8 @@ extension SystemFileHandle.SendableView {
778
778
779
779
func _materialize(
780
780
_ materialize: Bool ,
781
- descriptor: FileDescriptor
781
+ descriptor: FileDescriptor ,
782
+ failRenameat2WithEINVAL: Bool
782
783
) -> Result < Void , FileSystemError > {
783
784
guard let materialization = self . materialization else { return . success( ( ) ) }
784
785
@@ -803,7 +804,7 @@ extension SystemFileHandle.SendableView {
803
804
804
805
case . rename:
805
806
if materialize {
806
- let renameResult : Result < Void , Errno >
807
+ var renameResult : Result < Void , Errno >
807
808
let renameFunction : String
808
809
#if canImport(Darwin)
809
810
renameFunction = " renamex_np "
@@ -817,19 +818,76 @@ extension SystemFileHandle.SendableView {
817
818
// ignored. However, they must still be provided to 'rename' in order to pass
818
819
// flags.
819
820
renameFunction = " renameat2 "
820
- renameResult = Syscall . rename (
821
- from: createdPath,
822
- relativeTo: . currentWorkingDirectory,
823
- to: desiredPath,
824
- relativeTo: . currentWorkingDirectory,
825
- flags: materialization. exclusive ? [ . exclusive] : [ ]
826
- )
821
+ if materialization. exclusive, failRenameat2WithEINVAL {
822
+ renameResult = . failure( . invalidArgument)
823
+ } else {
824
+ renameResult = Syscall . rename (
825
+ from: createdPath,
826
+ relativeTo: . currentWorkingDirectory,
827
+ to: desiredPath,
828
+ relativeTo: . currentWorkingDirectory,
829
+ flags: materialization. exclusive ? [ . exclusive] : [ ]
830
+ )
831
+ }
827
832
#endif
828
833
829
- // A file exists at the desired path and the user specified exclusive creation,
830
- // clear up by removing the file we did create.
831
- if materialization. exclusive, case . failure( . fileExists) = renameResult {
832
- _ = Libc . remove ( createdPath)
834
+ if materialization. exclusive {
835
+ switch renameResult {
836
+ case . failure( . fileExists) :
837
+ // A file exists at the desired path and the user specified exclusive
838
+ // creation, clear up by removing the file that we did create.
839
+ _ = Libc . remove ( createdPath)
840
+
841
+ case . failure( . invalidArgument) :
842
+ // If 'renameat2' failed on Linux with EINVAL then in all likelihood the
843
+ // 'RENAME_NOREPLACE' option isn't supported. As we're doing an exclusive
844
+ // create, check the desired path doesn't exist then do a regular rename.
845
+ #if canImport(Glibc) || canImport(Musl)
846
+ switch Syscall . stat ( path: desiredPath) {
847
+ case . failure( . noSuchFileOrDirectory) :
848
+ // File doesn't exist, do a 'regular' rename.
849
+ renameResult = Syscall . rename ( from: createdPath, to: desiredPath)
850
+
851
+ case . success:
852
+ // File exists so exclusive create isn't possible. Remove the file
853
+ // we did create then throw.
854
+ _ = Libc . remove ( createdPath)
855
+ let error = FileSystemError (
856
+ code: . fileAlreadyExists,
857
+ message: """
858
+ Couldn't open ' \( desiredPath) ', it already exists and the \
859
+ file was opened with the 'existingFile' option set to 'none'.
860
+ """ ,
861
+ cause: nil ,
862
+ location: . here( )
863
+ )
864
+ return . failure( error)
865
+
866
+ case . failure:
867
+ // Failed to stat the desired file for reasons unknown. Remove the file
868
+ // we did create then throw.
869
+ _ = Libc . remove ( createdPath)
870
+ let error = FileSystemError (
871
+ code: . unknown,
872
+ message: " Couldn't open ' \( desiredPath) '. " ,
873
+ cause: FileSystemError . rename (
874
+ " renameat2 " ,
875
+ errno: . invalidArgument,
876
+ oldName: createdPath,
877
+ newName: desiredPath,
878
+ location: . here( )
879
+ ) ,
880
+ location: . here( )
881
+ )
882
+ return . failure( error)
883
+ }
884
+ #else
885
+ ( ) // Not Linux, use the normal error flow.
886
+ #endif
887
+
888
+ case . success, . failure:
889
+ ( )
890
+ }
833
891
}
834
892
835
893
result = renameResult. mapError { errno in
@@ -1238,7 +1296,8 @@ extension SystemFileHandle {
1238
1296
}
1239
1297
}
1240
1298
1241
- static func syncOpenWithMaterialization(
1299
+ @_spi ( Testing)
1300
+ public static func syncOpenWithMaterialization(
1242
1301
atPath path: FilePath ,
1243
1302
mode: FileDescriptor . AccessMode ,
1244
1303
options originalOptions: FileDescriptor . OpenOptions ,
0 commit comments