diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index 4d078ec6c..9bb8f0bbb 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -780,6 +780,24 @@ extension _FileManagerImpl { throw CocoaError.errorWithFilePath(.featureUnsupported, path) } + var attributesToSet: DWORD? + if let mode { + let existingAttributes = GetFileAttributesW($0) + guard existingAttributes != INVALID_FILE_ATTRIBUTES else { + throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true) + } + let isReadOnly = (existingAttributes & FILE_ATTRIBUTE_READONLY) != 0 + let requestedReadOnly = (mode & UInt(_S_IWRITE)) == 0 + if isReadOnly && !requestedReadOnly { + guard SetFileAttributesW($0, existingAttributes & ~FILE_ATTRIBUTE_READONLY) else { + throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false) + } + } else if !isReadOnly && requestedReadOnly { + // Make the file read-only later after setting other attributes + attributesToSet = existingAttributes | FILE_ATTRIBUTE_READONLY + } + } + if let modification = attributes[.modificationDate] as? Date { let seconds = modification.timeIntervalSince1601 @@ -803,6 +821,13 @@ extension _FileManagerImpl { throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false) } } + + // Finally, make the file read-only if requested + if let attributesToSet { + guard SetFileAttributesW($0, attributesToSet) else { + throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false) + } + } } #else try fileManager.withFileSystemRepresentation(for: path) { fileSystemRepresentation in diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 7ead66838..8c651d491 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -545,13 +545,12 @@ final class FileManagerTests : XCTestCase { } func testFileAccessAtPath() throws { -#if os(Windows) - throw XCTSkip("Windows filesystems do not conform to POSIX semantics") -#else + #if !os(Windows) guard getuid() != 0 else { // Root users can always access anything, so this test will not function when run as root throw XCTSkip("This test is not available when running as the root user") } + #endif try FileManagerPlayground { File("000", attributes: [.posixPermissions: 0o000]) @@ -563,18 +562,29 @@ final class FileManagerTests : XCTestCase { File("666", attributes: [.posixPermissions: 0o666]) File("777", attributes: [.posixPermissions: 0o777]) }.test { + #if os(Windows) + // All files are readable on Windows + let readable = ["000", "111", "222", "333", "444", "555", "666", "777"] + // None of these files are executable on Windows + let executable: [String] = [] + #else let readable = ["444", "555", "666", "777"] - let writable = ["222", "333", "666", "777"] let executable = ["111", "333", "555", "777"] + #endif + let writable = ["222", "333", "666", "777"] for number in 0...7 { let file = "\(number)\(number)\(number)" XCTAssertEqual($0.isReadableFile(atPath: file), readable.contains(file), "'\(file)' failed readable check") XCTAssertEqual($0.isWritableFile(atPath: file), writable.contains(file), "'\(file)' failed writable check") XCTAssertEqual($0.isExecutableFile(atPath: file), executable.contains(file), "'\(file)' failed executable check") + #if os(Windows) + // Only writable files are deletable on Windows + XCTAssertEqual($0.isDeletableFile(atPath: file), writable.contains(file), "'\(file)' failed deletable check") + #else XCTAssertTrue($0.isDeletableFile(atPath: file), "'\(file)' failed deletable check") + #endif } } -#endif } func testFileSystemAttributesAtPath() throws { @@ -654,16 +664,21 @@ final class FileManagerTests : XCTestCase { File("foo", attributes: [.posixPermissions : UInt16(0o644)]) }.test { let attributes = try $0.attributesOfItem(atPath: "foo") -#if !os(Windows) + // Ensure the unconventional UInt16 was accepted as input + #if os(Windows) + XCTAssertEqual(attributes[.posixPermissions] as? UInt, 0o600) + #else XCTAssertEqual(attributes[.posixPermissions] as? UInt, 0o644) + #endif + #if FOUNDATION_FRAMEWORK // Where we have NSNumber, ensure that we can get the value back as an unconventional Double value XCTAssertEqual(attributes[.posixPermissions] as? Double, Double(0o644)) // Ensure that the file type can be converted to a String when it is an ObjC enum XCTAssertEqual(attributes[.type] as? String, FileAttributeType.typeRegular.rawValue) #endif -#endif + // Ensure that the file type can be converted to a FileAttributeType when it is an ObjC enum and in swift-foundation XCTAssertEqual(attributes[.type] as? FileAttributeType, .typeRegular)