@@ -11,6 +11,7 @@ import {
1111 safeWriteText ,
1212 StagingPathError ,
1313 TargetExistsError ,
14+ UnsupportedFilesystemError ,
1415 TargetMovedError ,
1516 AncestorReplacedError ,
1617 OrphanedBackupError ,
@@ -1158,97 +1159,34 @@ describe("safeWriteText", () => {
11581159 expect ( fs . rename ) . toHaveBeenCalledWith ( expect . stringContaining ( "safeWriteText" ) , canonical )
11591160 } )
11601161
1161- it ( "falls back to an exclusive copy when the filesystem has no hard links" , async ( ) => {
1162- // FAT32/exFAT and some SMB mounts reject link(2); the create must still work, and
1163- // COPYFILE_EXCL keeps the no-replace verdict identical.
1162+ it ( "fails closed when the filesystem has no atomic no-replace primitive" , async ( ) => {
1163+ // FAT32/exFAT and some SMB mounts reject link(2). Publishing by an exclusive
1164+ // copy would put bytes at the final name that readers can observe mid-copy,
1165+ // and a crash or an EIO mid-copy leaves damaged bytes there with the
1166+ // 'already exists' verdict on retry. No atomic no-replace primitive exists
1167+ // on those volumes, so the guarded create refuses to publish at all.
11641168 const targetPath = "/tmp/test-dir/target.txt"
11651169 vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
11661170 vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( "ENOTSUP" ) , { code : "ENOTSUP" } ) )
11671171
1168- await safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } )
1169-
1170- expect ( fs . copyFile ) . toHaveBeenCalledWith (
1171- expect . stringContaining ( "safeWriteText" ) ,
1172- targetPath ,
1173- fsSync . constants . COPYFILE_EXCL ,
1174- )
1175- expect ( fs . rename ) . not . toHaveBeenCalled ( )
1176- } )
1177-
1178- it ( "fsyncs the copied target after the exclusive-copy fallback" , async ( ) => {
1179- // The copy creates a NEW inode at the target: the staged inode's pre-commit
1180- // fsync does not cover it and step 4b flushes only the parent directory.
1181- // Without this flush a crash on FAT32/exFAT/SMB can leave a zero-length or
1182- // partial target while the write reported success - and the retry then gets
1183- // the 'already exists' verdict on damaged bytes.
1184- const targetPath = "/tmp/test-dir/target.txt"
1185- vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
1186- vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( "ENOTSUP" ) , { code : "ENOTSUP" } ) )
1187- vi . mocked ( fsSync . openSync ) . mockReturnValue ( 1 )
1188-
1189- await safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } )
1190-
1191- // The copied target itself was opened and fsynced AFTER the copy landed,
1192- // with write access: Windows FlushFileBuffers requires GENERIC_WRITE, so a
1193- // read-only handle would fail the flush and report a false durability error
1194- // on bytes that are already published.
1195- expect ( fsSync . openSync ) . toHaveBeenCalledWith ( targetPath , "r+" )
1196- const copyOrder = vi . mocked ( fs . copyFile ) . mock . invocationCallOrder [ 0 ]
1197- const openCalls = vi . mocked ( fsSync . openSync ) . mock . calls
1198- const targetOpenIndex = openCalls . findIndex ( ( call ) => String ( call [ 0 ] ) === targetPath )
1199- expect ( targetOpenIndex ) . toBeGreaterThanOrEqual ( 0 )
1200- const targetOpenOrder = vi . mocked ( fsSync . openSync ) . mock . invocationCallOrder [ targetOpenIndex ]
1201- expect ( targetOpenOrder ) . toBeGreaterThan ( copyOrder )
1202- // ...and the write still reports success.
1203- } )
1204-
1205- it ( "reports a durability failure when the copied target cannot be fsynced" , async ( ) => {
1206- // The bytes are at the target but not known to be on disk: the same
1207- // durability verdict the directory fsync failure gets, never a swallowed
1208- // success.
1209- const targetPath = "/tmp/test-dir/target.txt"
1210- vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
1211- vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( "ENOTSUP" ) , { code : "ENOTSUP" } ) )
1212- vi . mocked ( fsSync . openSync ) . mockImplementation ( ( target ) => {
1213- if ( String ( target ) === targetPath ) throw new Error ( "EIO" )
1214- return 1
1215- } )
1216-
1217- await expect ( safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } ) ) . rejects . toThrow (
1218- PostCommitDurabilityError ,
1219- )
1220- } )
1221-
1222- it ( "removes the partially copied target when the exclusive copy fails mid-copy" , async ( ) => {
1223- // The copy writes directly to the final name: an EIO/ENOSPC mid-copy leaves a
1224- // partial file that readers would observe as content and a retry would report
1225- // as 'already exists' on damaged bytes. COPYFILE_EXCL guarantees this write
1226- // created the name, so the failure path must remove it.
1227- const targetPath = "/tmp/test-dir/target.txt"
1228- vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
1229- vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( "ENOTSUP" ) , { code : "ENOTSUP" } ) )
1230- vi . mocked ( fs . copyFile ) . mockRejectedValue ( Object . assign ( new Error ( "EIO" ) , { code : "EIO" } ) )
1231- vi . mocked ( fs . unlink ) . mockClear ( )
1232-
12331172 await expect (
12341173 safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } ) ,
1235- ) . rejects . toMatchObject ( { code : "EIO" } )
1174+ ) . rejects . toBeInstanceOf ( UnsupportedFilesystemError )
12361175
1237- // The partial target was removed - the name is absent again for the retry.
1238- expect ( vi . mocked ( fs . unlink ) . mock . calls . some ( ( call ) => String ( call [ 0 ] ) === targetPath ) ) . toBe ( true )
1239- } )
1240-
1241- it ( "refuses the write when the exclusive copy finds an existing target" , async ( ) => {
1242- const targetPath = "/tmp/test-dir/target.txt"
1243- vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
1244- vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( "EPERM" ) , { code : "EPERM" } ) )
1245- vi . mocked ( fs . copyFile ) . mockRejectedValue ( Object . assign ( new Error ( "EEXIST" ) , { code : "EEXIST" } ) )
1176+ // Nothing was published: no copy at the final name, no rename - the name
1177+ // stays absent for a retry on a capable volume.
1178+ // Every "no hard links here" code takes the same fail-closed path.
1179+ for ( const code of [ "EPERM" , "ENOTSUP" , "ENOSYS" ] ) {
1180+ vi . mocked ( fs . realpath ) . mockResolvedValue ( targetPath )
1181+ vi . mocked ( fs . link ) . mockRejectedValue ( Object . assign ( new Error ( code ) , { code } ) )
12461182
1247- await expect (
1248- safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } ) ,
1249- ) . rejects . toBeInstanceOf ( TargetExistsError )
1183+ await expect (
1184+ safeWriteText ( targetPath , "new data" , { failIfExist : true , platform : "linux" } ) ,
1185+ ) . rejects . toBeInstanceOf ( UnsupportedFilesystemError )
1186+ }
1187+ expect ( fs . copyFile ) . not . toHaveBeenCalled ( )
1188+ expect ( fs . rename ) . not . toHaveBeenCalled ( )
12501189 } )
1251-
12521190 it ( "still reports success when the staged copy cannot be unlinked after the link" , async ( ) => {
12531191 // An antivirus handle on Windows can make unlink fail with EBUSY after link(2)
12541192 // published the name. The content is readable at the target, so the write must
0 commit comments