Skip to content

Commit 9064c7f

Browse files
committed
Add a .hidden file attribute
Windows has a specific attribute to mark a file as hidden as opposed to prefixing ".". This allows setting and getting the attribute. On POSIX, it will return based on the "." prefix and setting it does nothing.
1 parent e1071a9 commit 9064c7f

File tree

2 files changed

+52
-5
lines changed

2 files changed

+52
-5
lines changed

Foundation/FileManager.swift

Lines changed: 31 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,22 @@ open class FileManager : NSObject {
406410
newAccessDate = providedDate
407411
}
408412
#endif
409-
413+
case .hidden:
414+
#if os(Windows)
415+
let attrs = try windowsFileAttributes(atPath: path).dwFileAttributes
416+
guard let isHidden = attributeValues[attribute] as? Bool else {
417+
fatalError("Can't set \(attribute) to \(attributeValues[attribute] as Any?)")
418+
}
419+
420+
let hiddenAttrs = isHidden
421+
? attrs | DWORD(FILE_ATTRIBUTE_HIDDEN)
422+
: attrs & DWORD(bitPattern: ~FILE_ATTRIBUTE_HIDDEN)
423+
guard path.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, hiddenAttrs) }) else {
424+
fatalError("Couldn't set \(path) to be hidden")
425+
}
426+
#else
427+
break
428+
#endif
410429
case .immutable: fallthrough
411430
case ._userImmutable:
412431
prepareToSetOrUnsetFlag(UF_IMMUTABLE)
@@ -537,7 +556,8 @@ open class FileManager : NSObject {
537556

538557
#if os(Windows)
539558
result[.deviceIdentifier] = NSNumber(value: UInt64(s.st_rdev))
540-
let type = FileAttributeType(attributes: try windowsFileAttributes(atPath: path), atPath: path)
559+
let attributes = try windowsFileAttributes(atPath: path)
560+
let type = FileAttributeType(attributes: attributes, atPath: path)
541561
#else
542562
if let pwd = getpwuid(s.st_uid), pwd.pointee.pw_name != nil {
543563
let name = String(cString: pwd.pointee.pw_name)
@@ -553,6 +573,13 @@ open class FileManager : NSObject {
553573
#endif
554574
result[.type] = type
555575

576+
#if os(Windows)
577+
let attrs = attributes.dwFileAttributes
578+
result[.hidden] = attrs & DWORD(FILE_ATTRIBUTE_HIDDEN) != 0
579+
#else
580+
result[.hidden] = path.first == "."
581+
#endif
582+
556583
if type == .typeBlockSpecial || type == .typeCharacterSpecial {
557584
result[.deviceIdentifier] = NSNumber(value: UInt64(s.st_rdev))
558585
}
@@ -1143,6 +1170,7 @@ public struct FileAttributeKey : RawRepresentable, Equatable, Hashable {
11431170
public static let extensionHidden = FileAttributeKey(rawValue: "NSFileExtensionHidden")
11441171
public static let hfsCreatorCode = FileAttributeKey(rawValue: "NSFileHFSCreatorCode")
11451172
public static let hfsTypeCode = FileAttributeKey(rawValue: "NSFileHFSTypeCode")
1173+
public static let hidden = FileAttributeKey(rawValue: "NSFileHidden")
11461174
public static let immutable = FileAttributeKey(rawValue: "NSFileImmutable")
11471175
public static let appendOnly = FileAttributeKey(rawValue: "NSFileAppendOnly")
11481176
public static let creationDate = FileAttributeKey(rawValue: "NSFileCreationDate")
@@ -1169,6 +1197,7 @@ public struct FileAttributeKey : RawRepresentable, Equatable, Hashable {
11691197
.extensionHidden,
11701198
.hfsCreatorCode,
11711199
.hfsTypeCode,
1200+
.hidden,
11721201
.immutable,
11731202
.appendOnly,
11741203
.creationDate,

TestFoundation/TestFileManager.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,13 @@ 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
@@ -555,10 +557,26 @@ class TestFileManager : XCTestCase {
555557

556558
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs1, withIntermediateDirectories: true, attributes: nil))
557559
XCTAssertNotNil(try? fm.createDirectory(atPath: subDirs2, withIntermediateDirectories: true, attributes: nil))
558-
for filename in [itemPath1, itemPath2, itemPath3, hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
560+
for filename in [itemPath1, itemPath2, itemPath3] {
559561
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
560562
}
561563

564+
for filename in [ hiddenItem1, hiddenItem2, hiddenItem3, hiddenItem4] {
565+
XCTAssertTrue(fm.createFile(atPath: filename, contents: Data(), attributes: nil), "Cant create file '\(filename)'")
566+
do {
567+
try fm.setAttributes([.hidden: true], ofItemAtPath: filename)
568+
} catch {
569+
XCTFail("Couldn't make \(filename) a hidden file")
570+
}
571+
}
572+
573+
do {
574+
try fm.setAttributes([.hidden: true], ofItemAtPath: hiddenDir1)
575+
try fm.setAttributes([.hidden: true], ofItemAtPath: hiddenDir2)
576+
} catch {
577+
XCTFail("Couldn't make \(hiddenDir1) and \(hiddenDir2) hidden directories")
578+
}
579+
562580
if let foundItems = directoryItems(options: []) {
563581
XCTAssertEqual(foundItems.count, fileLevels.count)
564582
for (name, level) in foundItems {

0 commit comments

Comments
 (0)