Skip to content

Commit b58b051

Browse files
authored
Fully enable posixPermissions attribute on Windows (#733)
1 parent 359c2c4 commit b58b051

File tree

2 files changed

+47
-7
lines changed

2 files changed

+47
-7
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Files.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,24 @@ extension _FileManagerImpl {
780780
throw CocoaError.errorWithFilePath(.featureUnsupported, path)
781781
}
782782

783+
var attributesToSet: DWORD?
784+
if let mode {
785+
let existingAttributes = GetFileAttributesW($0)
786+
guard existingAttributes != INVALID_FILE_ATTRIBUTES else {
787+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
788+
}
789+
let isReadOnly = (existingAttributes & FILE_ATTRIBUTE_READONLY) != 0
790+
let requestedReadOnly = (mode & UInt(_S_IWRITE)) == 0
791+
if isReadOnly && !requestedReadOnly {
792+
guard SetFileAttributesW($0, existingAttributes & ~FILE_ATTRIBUTE_READONLY) else {
793+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
794+
}
795+
} else if !isReadOnly && requestedReadOnly {
796+
// Make the file read-only later after setting other attributes
797+
attributesToSet = existingAttributes | FILE_ATTRIBUTE_READONLY
798+
}
799+
}
800+
783801
if let modification = attributes[.modificationDate] as? Date {
784802
let seconds = modification.timeIntervalSince1601
785803

@@ -803,6 +821,13 @@ extension _FileManagerImpl {
803821
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
804822
}
805823
}
824+
825+
// Finally, make the file read-only if requested
826+
if let attributesToSet {
827+
guard SetFileAttributesW($0, attributesToSet) else {
828+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
829+
}
830+
}
806831
}
807832
#else
808833
try fileManager.withFileSystemRepresentation(for: path) { fileSystemRepresentation in

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -551,13 +551,12 @@ final class FileManagerTests : XCTestCase {
551551
}
552552

553553
func testFileAccessAtPath() throws {
554-
#if os(Windows)
555-
throw XCTSkip("Windows filesystems do not conform to POSIX semantics")
556-
#else
554+
#if !os(Windows)
557555
guard getuid() != 0 else {
558556
// Root users can always access anything, so this test will not function when run as root
559557
throw XCTSkip("This test is not available when running as the root user")
560558
}
559+
#endif
561560

562561
try FileManagerPlayground {
563562
File("000", attributes: [.posixPermissions: 0o000])
@@ -569,18 +568,29 @@ final class FileManagerTests : XCTestCase {
569568
File("666", attributes: [.posixPermissions: 0o666])
570569
File("777", attributes: [.posixPermissions: 0o777])
571570
}.test {
571+
#if os(Windows)
572+
// All files are readable on Windows
573+
let readable = ["000", "111", "222", "333", "444", "555", "666", "777"]
574+
// None of these files are executable on Windows
575+
let executable: [String] = []
576+
#else
572577
let readable = ["444", "555", "666", "777"]
573-
let writable = ["222", "333", "666", "777"]
574578
let executable = ["111", "333", "555", "777"]
579+
#endif
580+
let writable = ["222", "333", "666", "777"]
575581
for number in 0...7 {
576582
let file = "\(number)\(number)\(number)"
577583
XCTAssertEqual($0.isReadableFile(atPath: file), readable.contains(file), "'\(file)' failed readable check")
578584
XCTAssertEqual($0.isWritableFile(atPath: file), writable.contains(file), "'\(file)' failed writable check")
579585
XCTAssertEqual($0.isExecutableFile(atPath: file), executable.contains(file), "'\(file)' failed executable check")
586+
#if os(Windows)
587+
// Only writable files are deletable on Windows
588+
XCTAssertEqual($0.isDeletableFile(atPath: file), writable.contains(file), "'\(file)' failed deletable check")
589+
#else
580590
XCTAssertTrue($0.isDeletableFile(atPath: file), "'\(file)' failed deletable check")
591+
#endif
581592
}
582593
}
583-
#endif
584594
}
585595

586596
func testFileSystemAttributesAtPath() throws {
@@ -660,16 +670,21 @@ final class FileManagerTests : XCTestCase {
660670
File("foo", attributes: [.posixPermissions : UInt16(0o644)])
661671
}.test {
662672
let attributes = try $0.attributesOfItem(atPath: "foo")
663-
#if !os(Windows)
673+
664674
// Ensure the unconventional UInt16 was accepted as input
675+
#if os(Windows)
676+
XCTAssertEqual(attributes[.posixPermissions] as? UInt, 0o600)
677+
#else
665678
XCTAssertEqual(attributes[.posixPermissions] as? UInt, 0o644)
679+
#endif
680+
666681
#if FOUNDATION_FRAMEWORK
667682
// Where we have NSNumber, ensure that we can get the value back as an unconventional Double value
668683
XCTAssertEqual(attributes[.posixPermissions] as? Double, Double(0o644))
669684
// Ensure that the file type can be converted to a String when it is an ObjC enum
670685
XCTAssertEqual(attributes[.type] as? String, FileAttributeType.typeRegular.rawValue)
671686
#endif
672-
#endif
687+
673688
// Ensure that the file type can be converted to a FileAttributeType when it is an ObjC enum and in swift-foundation
674689
XCTAssertEqual(attributes[.type] as? FileAttributeType, .typeRegular)
675690

0 commit comments

Comments
 (0)