Skip to content

Commit 458ac09

Browse files
committed
Implement the _.hidden File Attributes on Windows
Windows has a specific attribute to mark a file as hidden as opposed to prefixing ".". This allows setting and getting the attribute.
1 parent 9d2691f commit 458ac09

File tree

2 files changed

+53
-6
lines changed

2 files changed

+53
-6
lines changed

Foundation/FileManager.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ internal func &(left: UInt32, right: mode_t) -> mode_t {
1313
}
1414
#endif
1515

16+
#if os(Windows)
17+
import MSVCRT
18+
#endif
19+
1620
#if !canImport(Darwin) && !os(FreeBSD)
1721
// The values do not matter as long as they are nonzero.
1822
fileprivate let UF_IMMUTABLE: Int32 = 1
@@ -406,7 +410,6 @@ open class FileManager : NSObject {
406410
newAccessDate = providedDate
407411
}
408412
#endif
409-
410413
case .immutable: fallthrough
411414
case ._userImmutable:
412415
prepareToSetOrUnsetFlag(UF_IMMUTABLE)
@@ -418,7 +421,21 @@ open class FileManager : NSObject {
418421
prepareToSetOrUnsetFlag(UF_APPEND)
419422

420423
case ._hidden:
424+
#if os(Windows)
425+
let attrs = try windowsFileAttributes(atPath: path).dwFileAttributes
426+
guard let isHidden = attributeValues[attribute] as? Bool else {
427+
fatalError("Can't set \(attribute) to \(attributeValues[attribute] as Any?)")
428+
}
429+
430+
let hiddenAttrs = isHidden
431+
? attrs | DWORD(FILE_ATTRIBUTE_HIDDEN)
432+
: attrs & DWORD(bitPattern: ~FILE_ATTRIBUTE_HIDDEN)
433+
guard path.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, hiddenAttrs) }) else {
434+
fatalError("Couldn't set \(path) to be hidden")
435+
}
436+
#else
421437
prepareToSetOrUnsetFlag(UF_HIDDEN)
438+
#endif
422439

423440
// FIXME: On Darwin, these can be set with setattrlist(); and of course chown/chgrp on other OSes.
424441
case .ownerAccountID: fallthrough
@@ -537,7 +554,8 @@ open class FileManager : NSObject {
537554

538555
#if os(Windows)
539556
result[.deviceIdentifier] = NSNumber(value: UInt64(s.st_rdev))
540-
let type = FileAttributeType(attributes: try windowsFileAttributes(atPath: path), atPath: path)
557+
let attributes = try windowsFileAttributes(atPath: path)
558+
let type = FileAttributeType(attributes: attributes, atPath: path)
541559
#else
542560
if let pwd = getpwuid(s.st_uid), pwd.pointee.pw_name != nil {
543561
let name = String(cString: pwd.pointee.pw_name)
@@ -572,6 +590,11 @@ open class FileManager : NSObject {
572590
result[.appendOnly] = NSNumber(value: true)
573591
}
574592
#endif
593+
594+
#if os(Windows)
595+
let attrs = attributes.dwFileAttributes
596+
result[._hidden] = attrs & DWORD(FILE_ATTRIBUTE_HIDDEN) != 0
597+
#endif
575598
result[.ownerAccountID] = NSNumber(value: UInt64(s.st_uid))
576599
result[.groupOwnerAccountID] = NSNumber(value: UInt64(s.st_gid))
577600

TestFoundation/TestFileManager.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,15 +496,18 @@ class TestFileManager : XCTestCase {
496496
func test_directoryEnumerator() {
497497
let fm = FileManager.default
498498
let basePath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)/"
499-
let subDirs1 = basePath + "subdir1/subdir2/.hiddenDir/subdir3/"
499+
let hiddenDir1 = basePath + "subdir1/subdir2/.hiddenDir/"
500+
let subDirs1 = hiddenDir1 + "subdir3/"
500501
let itemPath1 = basePath + "itemFile1"
501502
#if os(Windows)
502503
// Filenames ending with '.' are not valid on Windows, so don't bother testing them
503-
let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5/.subdir6.ext/subdir7.ext/"
504+
let hiddenDir2 = basePath + "subdir1/subdir2/subdir4.app/subdir5/.subdir6.ext/"
505+
let subDirs2 = hiddenDir2 + "subdir7.ext/"
504506
let itemPath2 = subDirs1 + "itemFile2"
505507
let itemPath3 = subDirs1 + "itemFile3.ext"
506508
#else
507-
let subDirs2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/subdir7.ext./"
509+
let hiddenDir2 = basePath + "subdir1/subdir2/subdir4.app/subdir5./.subdir6.ext/"
510+
let subDirs2 = hiddenDir2 + "subdir7.ext./"
508511
let itemPath2 = subDirs1 + "itemFile2."
509512
let itemPath3 = subDirs1 + "itemFile3.ext."
510513
#endif
@@ -555,10 +558,31 @@ class TestFileManager : XCTestCase {
555558

556559
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs1, withIntermediateDirectories: true, attributes: nil))
557560
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs2, withIntermediateDirectories: true, attributes: nil))
558-
for filename in [itemPath1, itemPath2, itemPath3, hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
561+
for filename in [itemPath1, itemPath2, itemPath3] {
559562
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
560563
}
561564

565+
var resourceValues = URLResourceValues()
566+
resourceValues.isHidden = true
567+
for filename in [ hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
568+
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
569+
do {
570+
var url = URL(fileURLWithPath: filename)
571+
try url.setResourceValues(resourceValues)
572+
} catch {
573+
XCTFail("Couldn't make \(filename) a hidden file")
574+
}
575+
}
576+
577+
do {
578+
var hiddenURL1 = URL(fileURLWithPath: hiddenDir1)
579+
var hiddenURL2 = URL(fileURLWithPath: hiddenDir2)
580+
try hiddenURL1.setResourceValues(resourceValues)
581+
try hiddenURL2.setResourceValues(resourceValues)
582+
} catch {
583+
XCTFail("Couldn't make \(hiddenDir1) and \(hiddenDir2) hidden directories")
584+
}
585+
562586
if let foundItems = directoryItems(options: []) {
563587
XCTAssertEqual(foundItems.count, fileLevels.count)
564588
for (name, level) in foundItems {

0 commit comments

Comments
 (0)