Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/FoundationEssentials/Data/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ target_sources(FoundationEssentials PRIVATE
Data+Reading.swift
Data+Searching.swift
Data+Writing.swift
Data+WritingOptions.swift
DataProtocol.swift
FileSystemRepresentable.swift
Pointers+DataProtocol.swift)
117 changes: 117 additions & 0 deletions Sources/FoundationEssentials/Data/Data+WritingOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

// Data.WritingOptions is not a true OptionSet - the file protection constants act as an enum (exactly zero or one must be used)
// while the remaining options (.atomic, .withoutOverwriting) act as an option set (any number - or none - may be selected).
// Note: .atomic and .withoutOverwriting are mutually exclusive in practice, but that is enforced by receivers of Data.WritingOptions and not enforced in the option set itself as this may not apply to future options and is supported by their raw values
// Below are implementations for all SetAlgebra functions that implement correct logic for the file protection enum.
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
extension Data.WritingOptions {

@inline(__always)
@_alwaysEmitIntoClient
private var fileProtectionPart: RawValue {
self.rawValue & 0xf0000000
}

// All non-file protection options use the remaining bits
@inline(__always)
@_alwaysEmitIntoClient
private var optionsPart: RawValue {
self.rawValue & ~0xf0000000
}

@inline(__always)
@_alwaysEmitIntoClient
public func contains(_ member: Data.WritingOptions) -> Bool {
if member.fileProtectionPart != 0 {
// If member specifies a file protection level, self must have the exact same level
return self.fileProtectionPart == member.fileProtectionPart && (self.optionsPart & member.optionsPart) == member.optionsPart
} else {
// No file protection in member: check only the option bits
return (self.optionsPart & member.optionsPart) == member.optionsPart
}
}

@_alwaysEmitIntoClient
public mutating func insert(_ newMember: Data.WritingOptions) -> (inserted: Bool, memberAfterInsert: Data.WritingOptions) {
let inserted = !self.contains(newMember)
self.formUnion(newMember)
return (inserted, newMember)
}

@_alwaysEmitIntoClient
public mutating func remove(_ member: Data.WritingOptions) -> Data.WritingOptions? {
// Remove the file protection if self has the same protection level as member
let removeProtection = self.fileProtectionPart == member.fileProtectionPart

let result = (removeProtection ? self.fileProtectionPart : 0) | (self.optionsPart & member.optionsPart)
self = Self(rawValue: (removeProtection ? 0 : self.fileProtectionPart) | (self.optionsPart & ~member.optionsPart))
if result != 0 {
return Self(rawValue: result)
} else {
return nil
}
}

@_alwaysEmitIntoClient
public mutating func formUnion(_ other: Data.WritingOptions) {
// It is not possible to combine two different file protection levels; we must select one to keep.
// To preserve the invariant that x.contains(e) implies x.union(y).contains(e), we keep self's protection.
// If self has no protection, use other's.
let newProtection = self.fileProtectionPart != 0 ? self.fileProtectionPart : other.fileProtectionPart
self = Self(rawValue: newProtection | (self.optionsPart | other.optionsPart))
}

@_alwaysEmitIntoClient
public mutating func formIntersection(_ other: Data.WritingOptions) {
let newProtection: RawValue
if self.fileProtectionPart == other.fileProtectionPart {
// Same protection (or both unspecified): keep it
newProtection = self.fileProtectionPart
} else {
// Different protection levels with no common value: drop the protection
newProtection = 0
}
self = Self(rawValue: newProtection | (self.optionsPart & other.optionsPart))
}

@_alwaysEmitIntoClient
public mutating func formSymmetricDifference(_ other: Data.WritingOptions) {
var newProtection: RawValue
if self.fileProtectionPart == other.fileProtectionPart {
// Same protection (or both unspecified): remove it
newProtection = 0
} else if self.fileProtectionPart == 0 {
// File protection only present in other, use that value
newProtection = other.fileProtectionPart
} else if other.fileProtectionPart == 0 {
// File protection only present in self, use that value
newProtection = self.fileProtectionPart
} else {
// Two concrete file protection values. We cannot preserve both, so we drop both
newProtection = 0
}
self = Self(rawValue: newProtection | (self.optionsPart ^ other.optionsPart))
}

@_alwaysEmitIntoClient
public func isSubset(of other: Data.WritingOptions) -> Bool {
// If self specifies a file protection, other must have the exact same protection
// (a specific protection level is not a subset of a different protection level)
if self.fileProtectionPart != 0 && self.fileProtectionPart != other.fileProtectionPart {
return false
}

return (self.optionsPart & ~other.optionsPart) == 0
}
}
93 changes: 93 additions & 0 deletions Tests/FoundationEssentialsTests/DataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

import Testing

#if canImport(TestSupport)
import TestSupport
#endif

#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
Expand Down Expand Up @@ -2766,6 +2770,64 @@ extension DataTests {
}
}
}

@Test func writingOptionsSetAlgebra() {
var elements: [Data.WritingOptions] = [
.atomic, .withoutOverwriting,
.noFileProtection, .completeFileProtection,
.completeFileProtectionUnlessOpen, .completeFileProtectionUntilFirstUserAuthentication,
.fileProtectionMask
]
#if FOUNDATION_FRAMEWORK && !os(macOS)
elements.append(.completeFileProtectionWhenUserInactive)
#endif

Data.WritingOptions.validateConformance(
elements: elements,
groupings: [
[.atomic],
[.withoutOverwriting],
[.noFileProtection],
[.completeFileProtection],
[.completeFileProtectionUnlessOpen],
[.completeFileProtectionUntilFirstUserAuthentication],
[.noFileProtection, .atomic],
[.completeFileProtection, .atomic],
[.completeFileProtectionUnlessOpen, .withoutOverwriting],
[.completeFileProtectionUntilFirstUserAuthentication, .atomic],
]
)

#expect(Data.WritingOptions.completeFileProtection.contains(.completeFileProtection))
#expect(!Data.WritingOptions.completeFileProtection.contains(.noFileProtection))
#expect(!Data.WritingOptions.noFileProtection.contains(.completeFileProtection))
#expect(!Data.WritingOptions.completeFileProtectionUnlessOpen.contains(.noFileProtection))
#expect(!Data.WritingOptions.completeFileProtectionUnlessOpen.contains(.completeFileProtection))
#expect(Data.WritingOptions([.completeFileProtection, .atomic]).contains(.completeFileProtection))
#expect(Data.WritingOptions([.completeFileProtection, .atomic]).contains(.atomic))
#expect(!Data.WritingOptions([.completeFileProtection, .atomic]).contains(.noFileProtection))
#expect(!Data.WritingOptions([.completeFileProtection, .atomic]).contains(.withoutOverwriting))

#expect(Data.WritingOptions([.completeFileProtection, .atomic]).intersection(.noFileProtection) == [])
#expect(Data.WritingOptions([.noFileProtection, .withoutOverwriting]).intersection(.noFileProtection) == .noFileProtection)
#expect(Data.WritingOptions.atomic.intersection(.fileProtectionMask) == [])

// Verify that remove() works correctly
var opts: Data.WritingOptions = [.completeFileProtection, .atomic]
let removed = opts.remove(.completeFileProtection)
#expect(removed == .completeFileProtection)
#expect(opts == .atomic)

var opts2: Data.WritingOptions = [.completeFileProtection, .atomic]
let notRemoved = opts2.remove(.noFileProtection)
#expect(notRemoved == nil)
#expect(opts2 == [.completeFileProtection, .atomic])

var opts3: Data.WritingOptions = [.noFileProtection, .atomic, .withoutOverwriting]
let removedOpts = opts3.remove(.atomic)
#expect(removedOpts == .atomic)
#expect(opts3 == [.noFileProtection, .withoutOverwriting])
}
}

#if FOUNDATION_FRAMEWORK // FIXME: Re-enable tests once range(of:) is implemented
Expand Down Expand Up @@ -3179,3 +3241,34 @@ private func capacity(_ data: consuming Data) -> Int {
data._representation._storage.capacity
#endif
}

// MARK: - WritingOptions SetAlgebra Tests

extension Data.WritingOptions: TestableOptionSet {
public var _description: String {
let protectionPart = Self(rawValue: self.rawValue & Self.fileProtectionMask.rawValue)
let protectionString = switch protectionPart {
case .noFileProtection: "noProtection"
case .completeFileProtection: "complete"
case .completeFileProtectionUnlessOpen: "unlessOpen"
case .completeFileProtectionUntilFirstUserAuthentication: "untilFirstAuth"
#if FOUNDATION_FRAMEWORK && !os(macOS)
case .completeFileProtectionWhenUserInactive: "whenUserInactive"
#endif
case []: "<none>"
default: "unknown (0x\(String(protectionPart.rawValue, radix: 16)))"
}

var options = [String]()
if self.rawValue & Self.atomic.rawValue != 0 {
options.append("atomic")
}
if self.rawValue & Self.withoutOverwriting.rawValue != 0 {
options.append("withoutOverwriting")
}
if options.isEmpty {
options.append("<none>")
}
return "(protection: \(protectionString), options: \(options.joined(separator: ", ")))"
}
}
91 changes: 91 additions & 0 deletions Tests/FoundationEssentialsTests/TestableOptionSet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Testing

/// A protocol for OptionSet types that have non-standard SetAlgebra semantics (e.g. because they
/// encode an enum in some of their bits rather than using all bits as independent flags) and want
/// to validate their SetAlgebra conformance in tests.
public protocol TestableOptionSet: OptionSet where Element == Self {
/// A human-readable description of the value, used in test failure messages.
var _description: String { get }

/// Whether this type can satisfy the invariant:
/// x.contains(e) && y.contains(e) if and only if x.intersection(y).contains(e)
///
/// Types whose enum-in-bits encoding makes this invariant impossible to satisfy should
/// override this to return `false`.
static var supportsIntersectionContainsInvariant: Bool { get }
}

extension TestableOptionSet {
public static var supportsIntersectionContainsInvariant: Bool { true }

public static func validateConformance(elements: [Self], groupings: [Self], sourceLocation: SourceLocation = #_sourceLocation) {
let empty: Self = []

// S() == []
#expect(Self() == empty, "Invariant not held: S() == []", sourceLocation: sourceLocation)

for x in groupings + elements {
// x.intersection(x) == x
#expect(x.intersection(x) == x, "Invariant not held: x.intersection(x) == x for x = \(x._description)", sourceLocation: sourceLocation)

// x.intersection([]) == []
#expect(x.intersection(empty) == empty, "Invariant not held: x.intersection([]) == [] for x = \(x._description)", sourceLocation: sourceLocation)

// x.union(x) == x
#expect(x.union(x) == x, "Invariant not held: x.union(x) == x for x = \(x._description)", sourceLocation: sourceLocation)

// x.union([]) == x
#expect(x.union(empty) == x, "Invariant not held: x.union([]) == x for x = \(x._description)", sourceLocation: sourceLocation)

for y in groupings + elements {
for e in elements {
// x.contains(e) implies x.union(y).contains(e)
if x.contains(e) {
#expect(x.union(y).contains(e), "Invariant not held: x.contains(e) implies x.union(y).contains(e) for x = \(x._description), y = \(y._description), e = \(e._description)", sourceLocation: sourceLocation)
}

// x.union(y).contains(e) implies x.contains(e) || y.contains(e)
if x.union(y).contains(e) {
#expect(x.contains(e) || y.contains(e), "Invariant not held: x.union(y).contains(e) implies x.contains(e) || y.contains(e) for x = \(x._description), y = \(y._description), e = \(e._description)", sourceLocation: sourceLocation)
}

// x.contains(e) && y.contains(e) if and only if x.intersection(y).contains(e)
if Self.supportsIntersectionContainsInvariant {
#expect((x.contains(e) && y.contains(e)) == x.intersection(y).contains(e), "Invariant not held: x.contains(e) && y.contains(e) if and only if x.intersection(y).contains(e) for x = \(x._description), y = \(y._description), e = \(e._description)", sourceLocation: sourceLocation)
}
}

// x.isSubset(of: y) implies x.union(y) == y
if x.isSubset(of: y) {
#expect(x.union(y) == y, "Invariant not held: x.isSubset(of: y) implies x.union(y) == y for x = \(x._description), y = \(y._description)", sourceLocation: sourceLocation)
}

// x.isSuperset(of: y) implies x.union(y) == x
if x.isSuperset(of: y) {
#expect(x.union(y) == x, "Invariant not held: x.isSuperset(of: y) implies x.union(y) == x for x = \(x._description), y = \(y._description)", sourceLocation: sourceLocation)
}

// x.isSubset(of: y) if and only if y.isSuperset(of: x)
#expect(x.isSubset(of: y) == y.isSuperset(of: x), "Invariant not held: x.isSubset(of: y) if and only if y.isSuperset(of: x) for x = \(x._description), y = \(y._description)", sourceLocation: sourceLocation)

// x.isStrictSuperset(of: y) if and only if x.isSuperset(of: y) && x != y
#expect(x.isStrictSuperset(of: y) == (x.isSuperset(of: y) && x != y), "Invariant not held: x.isStrictSuperset(of: y) if and only if x.isSuperset(of: y) && x != y for x = \(x._description), y = \(y._description)", sourceLocation: sourceLocation)

// x.isStrictSubset(of: y) if and only if x.isSubset(of: y) && x != y
#expect(x.isStrictSubset(of: y) == (x.isSubset(of: y) && x != y), "Invariant not held: x.isStrictSubset(of: y) if and only if x.isSubset(of: y) && x != y for x = \(x._description), y = \(y._description)", sourceLocation: sourceLocation)
}
}
}
}
Loading