From d1ed95172f318dd965a12e0805055ca99795a13b Mon Sep 17 00:00:00 2001 From: Simon Evans Date: Sat, 31 Jul 2021 13:30:39 +0100 Subject: [PATCH] SR-14953: Data.write(to:) fails when writing to /dev/stdout - FileHandle.synchronize() would throw when calling fsync() on special files. Ignore EINVAL and EROFS if fsync() fails, so that the behaviour matches Darwin Foundation. --- Sources/Foundation/FileHandle.swift | 9 ++++++++- Tests/Foundation/Tests/TestFileHandle.swift | 7 +++++++ Tests/Foundation/Tests/TestNSData.swift | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/Foundation/FileHandle.swift b/Sources/Foundation/FileHandle.swift index a53bbafced..b18e351423 100644 --- a/Sources/Foundation/FileHandle.swift +++ b/Sources/Foundation/FileHandle.swift @@ -595,7 +595,14 @@ open class FileHandle : NSObject { throw _NSErrorWithWindowsError(GetLastError(), reading: false) } #else - guard fsync(_fd) >= 0 else { throw _NSErrorWithErrno(errno, reading: false) } + // Linux, macOS, OpenBSD return -1 and errno == EINVAL if trying to sync a special file, + // eg a fifo, character device etc which can be ignored. + // Additionally, Linux may return EROFS if tying to sync on a readonly filesystem, which also can be ignored. + // macOS can also return ENOTSUP for pipes but dont ignore it, so that the behaviour matches Darwin's Foundation. + if fsync(_fd) < 0 { + if errno == EINVAL || errno == EROFS { return } + throw _NSErrorWithErrno(errno, reading: false) + } #endif } diff --git a/Tests/Foundation/Tests/TestFileHandle.swift b/Tests/Foundation/Tests/TestFileHandle.swift index 07d5b291fb..34ff6ed0b0 100644 --- a/Tests/Foundation/Tests/TestFileHandle.swift +++ b/Tests/Foundation/Tests/TestFileHandle.swift @@ -613,6 +613,12 @@ class TestFileHandle : XCTestCase { } #endif + func testSynchronizeOnSpecialFile() throws { + // .synchronize() on a special file shouldnt fail + let fh = try XCTUnwrap(FileHandle(forWritingAtPath: "/dev/stdout")) + XCTAssertNoThrow(try fh.synchronize()) + } + static var allTests : [(String, (TestFileHandle) -> () throws -> ())] { var tests: [(String, (TestFileHandle) -> () throws -> ())] = [ ("testReadUpToCount", testReadUpToCount), @@ -632,6 +638,7 @@ class TestFileHandle : XCTestCase { ("test_waitForDataInBackgroundAndNotify", test_waitForDataInBackgroundAndNotify), /* ⚠️ */ ("test_readWriteHandlers", testExpectedToFail(test_readWriteHandlers, /* ⚠️ */ " sporadically times out")), + ("testSynchronizeOnSpecialFile", testSynchronizeOnSpecialFile), ] #if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT diff --git a/Tests/Foundation/Tests/TestNSData.swift b/Tests/Foundation/Tests/TestNSData.swift index 615cd1fdea..964848f4c3 100644 --- a/Tests/Foundation/Tests/TestNSData.swift +++ b/Tests/Foundation/Tests/TestNSData.swift @@ -237,6 +237,7 @@ class TestNSData: LoopbackServerTest { ("test_writeToURLOptions", test_writeToURLOptions), ("test_writeToURLPermissions", test_writeToURLPermissions), ("test_writeToURLPermissionsWithAtomic", test_writeToURLPermissionsWithAtomic), + ("test_writeToURLSpecialFile", test_writeToURLSpecialFile), ("test_edgeNoCopyDescription", test_edgeNoCopyDescription), ("test_initializeWithBase64EncodedDataGetsDecodedData", test_initializeWithBase64EncodedDataGetsDecodedData), ("test_initializeWithBase64EncodedDataWithNonBase64CharacterIsNil", test_initializeWithBase64EncodedDataWithNonBase64CharacterIsNil), @@ -623,6 +624,11 @@ class TestNSData: LoopbackServerTest { #endif } + func test_writeToURLSpecialFile() { + let url = URL(fileURLWithPath: "/dev/stdout") + XCTAssertNoThrow(try Data("Output to STDOUT\n".utf8).write(to: url)) + } + func test_emptyDescription() { let expected = "<>"