apple / swift-corelibs-foundation

The Foundation Project, providing core utilities, internationalization, and OS independence
swift.org
Apache License 2.0
5.27k stars 1.13k forks source link

FileManager.replaceItemAt(_:withItemAt:) doesn't appear to function #4655

Open Lukasa opened 1 year ago

Lukasa commented 1 year ago

Here's a simple test file:

import Foundation

func main() {
    print("start")
    let temporaryFolder = FileManager.default
           .temporaryDirectory
           .appendingPathComponent(UUID().uuidString, isDirectory: true)
       try! FileManager.default.createDirectory(at: temporaryFolder, withIntermediateDirectories: true)
    print("making temp folder \(temporaryFolder)")

    try! FileManager.default.createDirectory(at: temporaryFolder, withIntermediateDirectories: true)
    print("mde folder \(temporaryFolder)")

    let identityCertificatePath = temporaryFolder.appendingPathComponent("identity.p12")
    let newCertificate = temporaryFolder.appendingPathComponent("identity_new.p12")

    print("copying in files")
    try! String("identity").write(toFile: identityCertificatePath.path, atomically: true, encoding: .utf8)
    try! String("new").write(toFile: newCertificate.path, atomically: true, encoding: .utf8)

    print("Copied in files \(identityCertificatePath) \(newCertificate)")
    try! FileManager.default.replaceItemAt(identityCertificatePath, withItemAt: newCertificate)

    print("done")
}

main()

When run in Ubuntu 20.04 with Swift 5.7, this prints the following output:

start
making temp folder file:///tmp/4B5A4D7C-4747-468F-B68D-1303C38D8B41/
mde folder file:///tmp/4B5A4D7C-4747-468F-B68D-1303C38D8B41/
copying in files
Copied in files file:///tmp/4B5A4D7C-4747-468F-B68D-1303C38D8B41/identity.p12 file:///tmp/4B5A4D7C-4747-468F-B68D-1303C38D8B41/identity_new.p12
test/test.swift:25: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=4 "The file doesn’t exist."
Current stack trace:
0    libswiftCore.so                    0x0000ffffbaacaf8c _swift_stdlib_reportFatalErrorInFile + 128
1    libswiftCore.so                    0x0000ffffba7ca958 <unavailable> + 1419608
2    libswiftCore.so                    0x0000ffffba7c96e0 _assertionFailure(_:_:file:line:flags:) + 304
3    libswiftCore.so                    0x0000ffffba81d190 Dictionary.init<A>(_:uniquingKeysWith:) + 0
4    test                               0x0000aaaab282268c <unavailable> + 9868
5    test                               0x0000aaaab2821ab0 <unavailable> + 6832
6    libc.so.6                          0x0000ffffba4ccd28 __libc_start_main + 232
7    test                               0x0000aaaab2821874 <unavailable> + 6260
Trace/breakpoint trap

On Ubuntu 18.04 with Swift 5.7 this produces:

start
making temp folder file:///tmp/E99A55EF-CC74-4008-AE06-51B589950BBB/
mde folder file:///tmp/E99A55EF-CC74-4008-AE06-51B589950BBB/
copying in files
Copied in files file:///tmp/E99A55EF-CC74-4008-AE06-51B589950BBB/identity.p12 file:///tmp/E99A55EF-CC74-4008-AE06-51B589950BBB/identity_new.p12
test/test.swift:25: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=512 "(null)"
Current stack trace:
0    libswiftCore.so                    0x00007f34de75fdd0 _swift_stdlib_reportFatalErrorInFile + 112
1    libswiftCore.so                    0x00007f34de42a91c <unavailable> + 1444124
2    libswiftCore.so                    0x00007f34de42a738 <unavailable> + 1443640
3    libswiftCore.so                    0x00007f34de429220 _assertionFailure(_:_:file:line:flags:) + 419
4    libswiftCore.so                    0x00007f34de47e1c0 swift_unexpectedError + 748
5    test                               0x00005634cad3e9ce <unavailable> + 10702
6    test                               0x00005634cad3da59 <unavailable> + 6745
7    libc.so.6                          0x00007f34ddefaba0 __libc_start_main + 231
8    test                               0x00005634cad3d79a <unavailable> + 6042

In both cases the test fails.

This appears to be for different reasons on both platforms. If we find a copy of renameat2 then we will call renameat2 with the flag RENAME_EXCHANGE. This is a strange choice: no other platform appears to implement replaceItemAt(_:with:) with the semantic of exchanging the two files, not even the other code paths in this file. If that's not present, we call renameat and then, if the .usingNewMetadataOnly option is not present, we will attempt to set the attributes to reapply on the old path, which renameat just deleted.

In general there appear to be a number of bugs in rename handling that have slipped through the net here. Given that the exchange semantic of renameat2 is not maintained in any other code path or platform, I propose deleting that code altogether and falling back to regular old renameat, as well as fixing the issues in the implementation.

fred-sch commented 1 month ago

Any update on this?

I just ran into this today wasting some time trying to find out why replaceItemAt(_:withItemAt:) deletes the target item but leaves us with the item we want to move to the other path...

Can reproduce this in docker on swift:5.9-jammy, swift:5.10-jammy and swiftlang/swift:nightly-6.0-jammy

import Foundation

let testDirectory = "/tmp/test"

FileManager.default.createDirectory(atPath: testDirectory, withIntermediateDirectories: true)

print(FileManager.default.contentsOfDirectory(atPath: testDirectory))
// []

let target="\(testDirectory)/replace_me.txt" 
let source = "\(testDirectory)/will_replace_you.txt" 
FileManager.default.createFile(atPath: target, contents: nil)
FileManager.default.createFile(atPath: source, contents: nil)

print(FileManager.default.contentsOfDirectory(atPath: testDirectory))
// [replace_me.txt", "will_replace_you.txt"]

do {
    try FileManager.default.replaceItemAt(URL(fileURLWithPath: target), withItemAt: URL(fileURLWithPath: source))
} catch {
    print(error)
    // Error Domain=NSCocoaErrorDomain Code=4 "The file doesn’t exist."
}

print(FileManager.default.contentsOfDirectory(atPath: testDirectory))
// ["will_replace_you.txt"]
Lukasa commented 1 month ago

PR at #4656.