├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .spi.yml
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Package.swift
├── Sources
└── ExtendedAttributes
│ ├── Documentation.docc
│ └── Documentation.md
│ ├── ExtendedAttributes+Flags.swift
│ ├── ExtendedAttributes.swift
│ ├── SystemMetadata.swift
│ └── Utilities.swift
├── Tests
└── ExtendedAttributesTests
│ └── ExtendedAttributesTests.swift
├── license
└── readme.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | - push
4 | - pull_request
5 | jobs:
6 | test:
7 | runs-on: macos-14
8 | steps:
9 | - uses: actions/checkout@v4
10 | - run: sudo xcode-select -switch /Applications/Xcode_15.2.app
11 | - run: swift test
12 | lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: norio-nomura/action-swiftlint@3.2.1
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.build
2 | /Packages
3 | xcuserdata
4 | project.xcworkspace
5 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: ['ExtendedAttributes']
5 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | only_rules:
2 | - accessibility_trait_for_button
3 | - array_init
4 | - blanket_disable_command
5 | - block_based_kvo
6 | - class_delegate_protocol
7 | - closing_brace
8 | - closure_end_indentation
9 | - closure_parameter_position
10 | - closure_spacing
11 | - collection_alignment
12 | - colon
13 | - comma
14 | - comma_inheritance
15 | - compiler_protocol_init
16 | - computed_accessors_order
17 | - conditional_returns_on_newline
18 | - contains_over_filter_count
19 | - contains_over_filter_is_empty
20 | - contains_over_first_not_nil
21 | - contains_over_range_nil_comparison
22 | - control_statement
23 | - custom_rules
24 | - deployment_target
25 | - direct_return
26 | - discarded_notification_center_observer
27 | - discouraged_assert
28 | - discouraged_direct_init
29 | - discouraged_none_name
30 | - discouraged_object_literal
31 | - discouraged_optional_boolean
32 | - discouraged_optional_collection
33 | - duplicate_conditions
34 | - duplicate_enum_cases
35 | - duplicate_imports
36 | - duplicated_key_in_dictionary_literal
37 | - dynamic_inline
38 | - empty_collection_literal
39 | - empty_count
40 | - empty_enum_arguments
41 | - empty_parameters
42 | - empty_parentheses_with_trailing_closure
43 | - empty_string
44 | - empty_xctest_method
45 | - enum_case_associated_values_count
46 | - explicit_init
47 | - fallthrough
48 | - fatal_error_message
49 | - first_where
50 | - flatmap_over_map_reduce
51 | - for_where
52 | - generic_type_name
53 | - ibinspectable_in_extension
54 | - identical_operands
55 | - identifier_name
56 | - implicit_getter
57 | - implicit_return
58 | - inclusive_language
59 | - invalid_swiftlint_command
60 | - is_disjoint
61 | - joined_default_parameter
62 | - last_where
63 | - leading_whitespace
64 | - legacy_cggeometry_functions
65 | - legacy_constant
66 | - legacy_constructor
67 | - legacy_hashing
68 | - legacy_multiple
69 | - legacy_nsgeometry_functions
70 | - legacy_random
71 | - literal_expression_end_indentation
72 | - lower_acl_than_parent
73 | - mark
74 | - modifier_order
75 | - multiline_function_chains
76 | - multiline_literal_brackets
77 | - multiline_parameters
78 | - multiline_parameters_brackets
79 | - nimble_operator
80 | - no_extension_access_modifier
81 | - no_fallthrough_only
82 | - no_space_in_method_call
83 | - non_overridable_class_declaration
84 | - notification_center_detachment
85 | - ns_number_init_as_function_reference
86 | - nsobject_prefer_isequal
87 | - number_separator
88 | - opening_brace
89 | - operator_usage_whitespace
90 | - operator_whitespace
91 | - overridden_super_call
92 | - prefer_self_in_static_references
93 | - prefer_self_type_over_type_of_self
94 | - prefer_zero_over_explicit_init
95 | - private_action
96 | - private_outlet
97 | - private_subject
98 | - private_swiftui_state
99 | - private_unit_test
100 | - prohibited_super_call
101 | - protocol_property_accessors_order
102 | - reduce_boolean
103 | - reduce_into
104 | - redundant_discardable_let
105 | - redundant_nil_coalescing
106 | - redundant_objc_attribute
107 | - redundant_optional_initialization
108 | - redundant_set_access_control
109 | - redundant_string_enum_value
110 | - redundant_type_annotation
111 | - redundant_void_return
112 | - required_enum_case
113 | - return_arrow_whitespace
114 | - return_value_from_void_function
115 | - self_binding
116 | - self_in_property_initialization
117 | - shorthand_operator
118 | - shorthand_optional_binding
119 | - sorted_first_last
120 | - statement_position
121 | - static_operator
122 | - strong_iboutlet
123 | - superfluous_disable_command
124 | - superfluous_else
125 | - switch_case_alignment
126 | - switch_case_on_newline
127 | - syntactic_sugar
128 | - test_case_accessibility
129 | - toggle_bool
130 | - trailing_closure
131 | - trailing_comma
132 | - trailing_newline
133 | - trailing_semicolon
134 | - trailing_whitespace
135 | - unavailable_condition
136 | - unavailable_function
137 | - unneeded_break_in_switch
138 | - unneeded_override
139 | - unneeded_parentheses_in_closure_argument
140 | - unowned_variable_capture
141 | - untyped_error_in_catch
142 | - unused_closure_parameter
143 | - unused_control_flow_label
144 | - unused_enumerated
145 | - unused_optional_binding
146 | - unused_setter_value
147 | - valid_ibinspectable
148 | - vertical_parameter_alignment
149 | - vertical_whitespace_closing_braces
150 | - vertical_whitespace_opening_braces
151 | - void_function_in_ternary
152 | - void_return
153 | - xct_specific_matcher
154 | - xctfail_message
155 | - yoda_condition
156 | analyzer_rules:
157 | - capture_variable
158 | - unused_declaration
159 | - unused_import
160 | - typesafe_array_init
161 | for_where:
162 | allow_for_as_filter: true
163 | number_separator:
164 | minimum_length: 5
165 | identifier_name:
166 | max_length:
167 | warning: 100
168 | error: 100
169 | min_length:
170 | warning: 2
171 | error: 2
172 | allowed_symbols:
173 | - '_'
174 | excluded:
175 | - 'x'
176 | - 'y'
177 | - 'z'
178 | - 'a'
179 | - 'b'
180 | - 'x1'
181 | - 'x2'
182 | - 'y1'
183 | - 'y2'
184 | - 'z2'
185 | custom_rules:
186 | no_nsrect:
187 | regex: '\bNSRect\b'
188 | match_kinds: typeidentifier
189 | message: 'Use CGRect instead of NSRect'
190 | no_nssize:
191 | regex: '\bNSSize\b'
192 | match_kinds: typeidentifier
193 | message: 'Use CGSize instead of NSSize'
194 | no_nspoint:
195 | regex: '\bNSPoint\b'
196 | match_kinds: typeidentifier
197 | message: 'Use CGPoint instead of NSPoint'
198 | no_cgfloat:
199 | regex: '\bCGFloat\b'
200 | match_kinds: typeidentifier
201 | message: 'Use Double instead of CGFloat'
202 | no_cgfloat2:
203 | regex: '\bCGFloat\('
204 | message: 'Use Double instead of CGFloat'
205 | swiftui_state_private:
206 | regex: '@(ObservedObject|EnvironmentObject)\s+var'
207 | message: 'SwiftUI @ObservedObject/@EnvironmentObject properties should be private'
208 | swiftui_environment_private:
209 | regex: '@Environment\(\\\.\w+\)\s+var'
210 | message: 'SwiftUI @Environment properties should be private'
211 | final_class:
212 | regex: '^class [a-zA-Z\d]+[^{]+\{'
213 | message: 'Classes should be marked as final whenever possible. If you actually need it to be subclassable, just add `// swiftlint:disable:next final_class`.'
214 | no_alignment_center:
215 | regex: '\b\(alignment: .center\b'
216 | message: 'This alignment is the default.'
217 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "ExtendedAttributes",
6 | platforms: [
7 | .macOS(.v11),
8 | .iOS(.v14),
9 | .tvOS(.v14),
10 | .watchOS(.v7),
11 | .visionOS(.v1)
12 | ],
13 | products: [
14 | .library(
15 | name: "ExtendedAttributes",
16 | targets: [
17 | "ExtendedAttributes"
18 | ]
19 | )
20 | ],
21 | targets: [
22 | .target(
23 | name: "ExtendedAttributes"
24 | ),
25 | .testTarget(
26 | name: "ExtendedAttributesTests",
27 | dependencies: [
28 | "ExtendedAttributes"
29 | ]
30 | )
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/Sources/ExtendedAttributes/Documentation.docc/Documentation.md:
--------------------------------------------------------------------------------
1 | # ``ExtendedAttributes``
2 |
3 | Extended attributes allow storage of additional metadata beyond the standard filesystem attributes, such as custom data, security information, or system tags.
4 |
5 | ## Usage
6 |
7 | ```swift
8 | import ExtendedAttributes
9 |
10 | let fileURL = URL(fileURLWithPath: "/path/to/file")
11 | let data = try? fileURL.extendedAttributes.get("com.example.attribute")
12 | ```
13 |
14 | You can also use it to access system-specific metadata:
15 |
16 | ```swift
17 | import ExtendedAttributes
18 |
19 | let fileURL = URL(fileURLWithPath: "/path/to/file")
20 | let itemCreator = try? fileURL.systemMetadata.get(kMDItemCreator as String)
21 | ```
22 |
23 | ---
24 |
25 | [Learn more about extended attributes](https://en.wikipedia.org/wiki/Extended_file_attributes)
26 |
--------------------------------------------------------------------------------
/Sources/ExtendedAttributes/ExtendedAttributes+Flags.swift:
--------------------------------------------------------------------------------
1 | import Darwin
2 |
3 | extension ExtendedAttributes {
4 | public struct Flags: OptionSet {
5 | /**
6 | Declare that the attribute should not be exported. This is deliberately a bit vague, but this is used by `XATTR_OPERATION_INTENT_SHARE` to indicate not to preserve the attribute.
7 | */
8 | public static let noExport = Self(rawValue: XATTR_FLAG_NO_EXPORT)
9 |
10 | /**
11 | Declares the attribute to be tied to the contents of the file (or vice versa), such that it should be re-created when the contents of the file change. Examples might include cryptographic keys, checksums, saved position or search information, and text encoding.
12 |
13 | This property causes the attribute to be preserved for copy and share, but not for safe save. In a safe save, the attriubte exists on the original, and will not be copied to the new version.
14 | */
15 | public static let contentDependent = Self(rawValue: XATTR_FLAG_CONTENT_DEPENDENT)
16 |
17 | /**
18 | Declares that the attribute is never to be copied, for any intention type.
19 | */
20 | public static let neverPreserve = Self(rawValue: XATTR_FLAG_NEVER_PRESERVE)
21 |
22 | /**
23 | Declares that the attribute is to be synced, used by the `XATTR_OPERATION_ITENT_SYNC` intention. Syncing tends to want to minimize the amount of metadata synced around, hence the default behavior is for the attribute NOT to be synced, even if it would else be preserved for the `XATTR_OPERATION_ITENT_COPY` intention.
24 | */
25 | public static let syncable = Self(rawValue: XATTR_FLAG_SYNCABLE)
26 |
27 | /**
28 | Declares that the attribute should only be copied if the intention is `XATTR_OPERATION_INTENT_BACKUP`. That intention is distinct from the `XATTR_OPERATION_INTENT_SYNC` intention in that there is no desire to minimize the amount of metadata being moved.
29 | */
30 | public static let onlyBackup = Self(rawValue: XATTR_FLAG_ONLY_BACKUP)
31 |
32 | public var rawValue: xattr_flags_t
33 |
34 | public init(rawValue: xattr_flags_t) {
35 | self.rawValue = rawValue
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ExtendedAttributes/ExtendedAttributes.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import System
3 |
4 | /**
5 | Manage extended attributes.
6 |
7 | Use the ``Foundation/URL/extendedAttributes`` property on a URL to access this class.
8 |
9 | ```swift
10 | import ExtendedAttributes
11 |
12 | let fileURL = URL(fileURLWithPath: "/path/to/file")
13 | let data = try? fileURL.extendedAttributes.get("com.example.attribute")
14 | ```
15 | */
16 | public final class ExtendedAttributes {
17 | private let url: URL
18 |
19 | init(url: URL) {
20 | self.url = url
21 | }
22 |
23 | /**
24 | Retrieves the value of the extended attribute specified by the given name.
25 |
26 | - Parameter name: The name of the attribute.
27 | - Returns: The attribute value or `nil` if it does not exist.
28 | - Throws: An error if the file is not accessible or the operation fails.
29 | */
30 | public func get(_ name: String) throws -> Data? {
31 | do {
32 | return try _get(name)
33 | } catch Errno.attributeNotFound {
34 | return nil
35 | }
36 | }
37 |
38 | /**
39 | Sets the value of the extended attribute specified by the given name.
40 |
41 | - Parameters:
42 | - name: The name of the attribute.
43 | - data: The data to be written as the value of the attribute.
44 | - flags: Optional flags to specify behavior of the attribute setting.
45 | - Throws: An error if the file is not accessible or the operation fails.
46 | */
47 | public func set(_ name: String, data: Data, flags: Flags? = nil) throws {
48 | try checkIfFileURL()
49 |
50 | let finalName = if let flags {
51 | try Self.nameWithFlags(name, flags: flags)
52 | } else {
53 | name
54 | }
55 |
56 | try url.withUnsafeFileSystemRepresentation { fileSystemPath in
57 | try Errno.wrap {
58 | data.withUnsafeBytes {
59 | setxattr(fileSystemPath, finalName, $0.baseAddress, data.count, 0, 0)
60 | }
61 | }
62 | }
63 | }
64 |
65 | /**
66 | Checks whether the file has an extended attribute with the given name.
67 |
68 | - Parameter name: The name of the attribute.
69 | - Returns: `true` if the attribute exists, otherwise `false`.
70 | - Throws: An error if the file is not accessible or the operation fails.
71 | */
72 | public func has(_ name: String) throws -> Bool {
73 | try checkIfFileURL()
74 |
75 | return try url.withUnsafeFileSystemRepresentation { fileSystemPath in
76 | let size = getxattr(fileSystemPath, name, nil, 0, 0, 0)
77 |
78 | if size >= 0 {
79 | return true
80 | }
81 |
82 | let error = Errno(rawValue: errno)
83 |
84 | if error == .attributeNotFound {
85 | return false
86 | }
87 |
88 | throw error
89 | }
90 | }
91 |
92 | /**
93 | Removes the extended attribute specified by the given name.
94 |
95 | - Parameter name: The name of the attribute.
96 | - Throws: An error if the file is not accessible or the operation fails. It does not throw if the attribute does not exist.
97 | */
98 | public func remove(_ name: String) throws {
99 | do {
100 | try _remove(name)
101 | } catch Errno.attributeNotFound {}
102 | }
103 |
104 | /**
105 | Retrieves all extended attribute names of the file.
106 |
107 | - Parameter withFlags: Specifies whether the names should include flags (e.g., `com.apple.metadata:kMDItemCreator#S`).
108 | - Returns: An array of attribute names.
109 | - Throws: An error if the file is not accessible or the operation fails.
110 | */
111 | public func allNames(withFlags: Bool) throws -> [String] {
112 | try checkIfFileURL()
113 |
114 | let data: Data = try url.withUnsafeFileSystemRepresentation { fileSystemPath in
115 | let size = listxattr(fileSystemPath, nil, 0, 0)
116 |
117 | guard size >= 0 else {
118 | throw Errno(rawValue: errno)
119 | }
120 |
121 | var data = Data(count: size)
122 |
123 | let length = data.withUnsafeMutableBytes {
124 | listxattr(fileSystemPath, $0.baseAddress, size, 0)
125 | }
126 |
127 | guard length >= 0 else {
128 | throw Errno(rawValue: errno)
129 | }
130 |
131 | return data
132 | }
133 |
134 | var names = data.split(separator: 0).compactMap(\.toString)
135 |
136 | if !withFlags {
137 | names = try names.map { try Self.nameWithoutFlags($0) }
138 | }
139 |
140 | return names
141 | }
142 |
143 | private func checkIfFileURL() throws {
144 | guard url.isFileURL else {
145 | throw CocoaError(.fileNoSuchFile)
146 | }
147 | }
148 |
149 | private func _get(_ name: String) throws -> Data {
150 | try checkIfFileURL()
151 |
152 | return try url.withUnsafeFileSystemRepresentation { fileSystemPath in
153 | let size = getxattr(fileSystemPath, name, nil, 0, 0, 0)
154 |
155 | guard size >= 0 else {
156 | throw Errno(rawValue: errno)
157 | }
158 |
159 | var data = Data(count: size)
160 |
161 | let byteCount = data.withUnsafeMutableBytes {
162 | getxattr(fileSystemPath, name, $0.baseAddress, size, 0, 0)
163 | }
164 |
165 | guard byteCount >= 0 else {
166 | throw Errno(rawValue: errno)
167 | }
168 |
169 | return data
170 | }
171 | }
172 |
173 | private func _remove(_ name: String) throws {
174 | try checkIfFileURL()
175 |
176 | try url.withUnsafeFileSystemRepresentation { fileSystemPath in
177 | try Errno.wrap {
178 | removexattr(fileSystemPath, name, 0)
179 | }
180 | }
181 | }
182 |
183 | // TODO: Add subscript when Swift supports throwing setters.
184 | // `try url.extendedAttributes["foo"] = "bar".toData
185 | }
186 |
187 | extension ExtendedAttributes {
188 | // Docs: https://www.manpagez.com/man/3/xattr_flags_from_name/
189 |
190 | static func flagsFromName(_ name: String) -> Flags {
191 | Flags(rawValue: xattr_flags_from_name(name))
192 | }
193 |
194 | static func nameWithoutFlags(_ name: String) throws -> String {
195 | guard let newName = xattr_name_without_flags(name) else {
196 | throw Errno(rawValue: errno)
197 | }
198 |
199 | defer {
200 | newName.deallocate()
201 | }
202 |
203 | return String(cString: newName)
204 | }
205 |
206 | static func nameWithFlags(_ name: String, flags: Flags) throws -> String {
207 | guard let newName = xattr_name_with_flags(name, flags.rawValue) else {
208 | throw Errno(rawValue: errno)
209 | }
210 |
211 | defer {
212 | newName.deallocate()
213 | }
214 |
215 | return String(cString: newName)
216 | }
217 | }
218 |
219 | extension ExtendedAttributes {
220 | /**
221 | Retrieves the value of the extended attribute specified by the given name and deserializes its value into the specified type.
222 |
223 | - Parameter name: The name of the attribute.
224 | - Parameter type: The type to deserialize the attribute value into.
225 | - Returns: The attribute value or `nil` if it does not exist.
226 | - Note: The system usually stores extended attributes as property lists, but other extended attributes may be stored as strings.
227 |
228 | ```swift
229 | let isProtected = try? attributes.getPropertyListSerializedValue("com.apple.rootless", type: Bool.self) ?? false
230 | ```
231 | */
232 | public func getPropertyListSerializedValue(
233 | _ name: String,
234 | type: T.Type
235 | ) throws -> T? {
236 | try checkIfFileURL()
237 |
238 | guard let data = try get(name) else {
239 | return nil
240 | }
241 |
242 | let value = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
243 |
244 | guard let result = value as? T else {
245 | throw CocoaError(.propertyListReadCorrupt)
246 | }
247 |
248 | return result
249 | }
250 |
251 | /**
252 | Sets the value of the extended attribute specified by the given name by serializing the given data into a property list.
253 |
254 | - Parameter name: The name of the attribute.
255 | - Parameter value: The value to serialize into a property list.
256 | - Parameter flags: Optional flags to apply when setting the attribute.
257 |
258 | ```swift
259 | try attributes.setPropertyListSerializedValue("com.apple.rootless", value: true)
260 | ```
261 | */
262 | public func setPropertyListSerializedValue(
263 | _ name: String,
264 | value: some Any,
265 | flags: Flags? = nil
266 | ) throws {
267 | try checkIfFileURL()
268 |
269 | guard PropertyListSerialization.propertyList(value, isValidFor: .binary) else {
270 | throw CocoaError(.propertyListWriteInvalid)
271 | }
272 |
273 | let data = try PropertyListSerialization.data(fromPropertyList: value, format: .binary, options: 0)
274 | try set(name, data: data, flags: flags)
275 | }
276 | }
277 |
278 | extension URL {
279 | /**
280 | Provides convenient access to the extended attributes of the file/folder at the URL.
281 |
282 | See ``ExtendedAttributes`` for the API documentation.
283 | */
284 | public var extendedAttributes: ExtendedAttributes { .init(url: self) }
285 | }
286 |
--------------------------------------------------------------------------------
/Sources/ExtendedAttributes/SystemMetadata.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import System
3 |
4 | /**
5 | Manage system-specific metadata.
6 |
7 | Use the ``Foundation/URL/systemMetadata`` property on a URL to access this class.
8 |
9 | ```swift
10 | import ExtendedAttributes
11 |
12 | let fileURL = URL(fileURLWithPath: "/path/to/file")
13 | let itemCreator = try? fileURL.systemMetadata.get(kMDItemCreator as String)
14 | ```
15 |
16 | - [Supported metadata names](https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys)
17 | - [Better descriptions for the names](http://helios.de/support/manuals/indexsrvUB2-e/search_metadata.html)
18 |
19 | *System-specific metadata are [extended attributes](https://en.wikipedia.org/wiki/Extended_file_attributes) used on Apple's platforms. They are encoded as property lists and their names are namespaced under `com.apple.metadata:`. This class handles all that for you.*
20 | */
21 | public final class SystemMetadata {
22 | private let extendedAttributes: ExtendedAttributes
23 |
24 | init(url: URL) {
25 | self.extendedAttributes = url.extendedAttributes
26 | }
27 |
28 | /**
29 | Retrieves the value of the metadata item specified by the given name.
30 |
31 | - Parameter name: The name of the metadata item.
32 | - Returns: The metadata item value or `nil` if it does not exist.
33 | - Throws: An error if the file is not accessible or the operation fails.
34 | */
35 | public func get(_ name: String, type: T.Type) throws -> T? {
36 | try extendedAttributes.getPropertyListSerializedValue("com.apple.metadata:\(name)", type: type)
37 | }
38 |
39 | // - flags: Optional flags to specify behavior of the metadata item.
40 | /**
41 | Sets the value of the metadata item specified by the given name.
42 |
43 | - Parameters:
44 | - name: The name of the metadata item.
45 | - value: The value to be written.
46 | - Throws: An error if the file is not accessible or the operation fails.
47 |
48 | The following metadata names show up in the Finder "Get Info" window:
49 | - `kMDItemDescription`
50 | - `kMDItemHeadline`
51 | - `kMDItemInstructions`
52 | - `kMDItemWhereFroms`
53 | - `kMDItemKeywords`
54 | */
55 | public func set(
56 | _ name: String,
57 | value: some Any
58 | // Finder does not yet support the flags so we leave them out for now: https://eclecticlight.co/2020/11/02/controlling-metadata-tricks-with-persistence/
59 | //flags: ExtendedAttributes.Flags? = nil
60 | ) throws {
61 | try extendedAttributes.setPropertyListSerializedValue(
62 | "com.apple.metadata:\(name)",
63 | value: value
64 | // flags: flags
65 | )
66 | }
67 |
68 | /**
69 | Checks whether the file has a metadata item with the given name.
70 |
71 | - Parameter name: The name of the metadata item.
72 | - Returns: `true` if the metadata item exists, otherwise `false`.
73 | - Throws: An error if the file is not accessible or the operation fails.
74 | */
75 | public func has(_ name: String) throws -> Bool {
76 | try extendedAttributes.has("com.apple.metadata:\(name)")
77 | }
78 |
79 | /**
80 | Removes the metadata item specified by the given name.
81 |
82 | - Parameter name: The name of the metadata item.
83 | - Throws: An error if the file is not accessible or the operation fails. It does not throw if the metadata item does not exist.
84 | */
85 | public func remove(_ name: String) throws {
86 | try extendedAttributes.remove("com.apple.metadata:\(name)")
87 | }
88 | }
89 |
90 | extension URL {
91 | /**
92 | Provides convenient access to system-specific metadata of the file/folder at the URL.
93 |
94 | See ``SystemMetadata`` for the API documentation.
95 | */
96 | public var systemMetadata: SystemMetadata { .init(url: self) }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/ExtendedAttributes/Utilities.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import System
3 |
4 | extension System.Errno {
5 | static func wrap(_ action: () -> Int32) throws {
6 | guard action() == 0 else {
7 | throw Self(rawValue: errno)
8 | }
9 | }
10 | }
11 |
12 | extension Data {
13 | var toString: String? { String(data: self, encoding: .utf8) }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/ExtendedAttributesTests/ExtendedAttributesTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | import ExtendedAttributes
4 |
5 | final class ExtendedAttributesTests: XCTestCase {
6 | private var testFileURL: URL!
7 |
8 | override func setUp() {
9 | super.setUp()
10 | let temporaryFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
11 | try? Data("Test".utf8).write(to: temporaryFileURL)
12 | testFileURL = temporaryFileURL
13 | }
14 |
15 | override func tearDown() {
16 | try? FileManager.default.removeItem(at: testFileURL)
17 | testFileURL = nil
18 | super.tearDown()
19 | }
20 |
21 | func testGet() throws {
22 | let attributeName = "testAttribute"
23 | let attributeValue = "Test Value".toData
24 | try testFileURL.extendedAttributes.set(attributeName, data: attributeValue)
25 |
26 | let fetchedValue = try testFileURL.extendedAttributes.get(attributeName)
27 | XCTAssertEqual(fetchedValue, attributeValue, "Fetched attribute value should match the set value.")
28 | }
29 |
30 | func testSet() throws {
31 | let attributeName = "testAttributeSet"
32 | let attributeValue = "Another Test Value".toData
33 | try testFileURL.extendedAttributes.set(attributeName, data: attributeValue)
34 |
35 | let fetchedValue = try testFileURL.extendedAttributes.get(attributeName)
36 | XCTAssertEqual(fetchedValue, attributeValue, "Set attribute value should be retrievable.")
37 | }
38 |
39 | func testHas() throws {
40 | let attributeName = "testAttributeHas"
41 | try testFileURL.extendedAttributes.set(attributeName, data: Data())
42 |
43 | let exists = try testFileURL.extendedAttributes.has(attributeName)
44 | XCTAssertTrue(exists, "Attribute should exist after being set.")
45 | }
46 |
47 | func testRemove() throws {
48 | let attributeName = "testAttributeRemove"
49 | try testFileURL.extendedAttributes.set(attributeName, data: Data())
50 | try testFileURL.extendedAttributes.remove(attributeName)
51 |
52 | let exists = try testFileURL.extendedAttributes.has(attributeName)
53 | XCTAssertFalse(exists, "Attribute should not exist after being removed.")
54 | }
55 |
56 | func testAllNames() throws {
57 | let attributeName1 = "testAttribute1"
58 | let attributeName2 = "testAttribute2"
59 | try testFileURL.extendedAttributes.set(attributeName1, data: Data())
60 | try testFileURL.extendedAttributes.set(attributeName2, data: Data())
61 |
62 | let names = try testFileURL.extendedAttributes.allNames(withFlags: false)
63 | XCTAssertTrue(names.contains(attributeName1) && names.contains(attributeName2), "All names should include set attributes.")
64 | }
65 |
66 | func testGetPropertyListSerializedValue() throws {
67 | let attributeName = "testPropertyList"
68 | let attributeValue = ["Key": "Value"]
69 | try testFileURL.extendedAttributes.setPropertyListSerializedValue(attributeName, value: attributeValue)
70 |
71 | let fetchedValue = try testFileURL.extendedAttributes.getPropertyListSerializedValue(attributeName, type: [String: String].self)
72 | XCTAssertEqual(fetchedValue, attributeValue, "Fetched property list should match the set value.")
73 | }
74 |
75 | func testSetPropertyListSerializedValue() throws {
76 | let attributeName = "testPropertyListSet"
77 | let attributeValue = ["AnotherKey": "AnotherValue"]
78 | try testFileURL.extendedAttributes.setPropertyListSerializedValue(attributeName, value: attributeValue)
79 |
80 | let fetchedValue = try testFileURL.extendedAttributes.getPropertyListSerializedValue(attributeName, type: [String: String].self)
81 | XCTAssertEqual(fetchedValue, attributeValue, "Set property list should be retrievable.")
82 | }
83 | }
84 |
85 | final class SystemMetadataTests: XCTestCase {
86 | private var testFileURL: URL!
87 |
88 | override func setUp() {
89 | super.setUp()
90 | let temporaryFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
91 | try? Data("Test".utf8).write(to: temporaryFileURL)
92 | testFileURL = temporaryFileURL
93 | }
94 |
95 | override func tearDown() {
96 | try? FileManager.default.removeItem(at: testFileURL)
97 | testFileURL = nil
98 | super.tearDown()
99 | }
100 |
101 | func testMetadata() throws {
102 | let key = "kMDItemDescription"
103 | let value = "Test Description"
104 | try testFileURL.systemMetadata.set(key, value: value)
105 |
106 | let fetchedValue: String? = try testFileURL.systemMetadata.get(key, type: String.self)
107 | XCTAssertEqual(fetchedValue, value, "The fetched value should match the set value.")
108 | }
109 | }
110 |
111 | extension String {
112 | var toData: Data { Data(utf8) }
113 | }
114 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # ExtendedAttributes
2 |
3 | > Manage [extended attributes](https://en.wikipedia.org/wiki/Extended_file_attributes) in Swift
4 |
5 | ## Install
6 |
7 | Add the following to `Package.swift`:
8 |
9 | ```swift
10 | .package(url: "https://github.com/sindresorhus/ExtendedAttributes", from: "1.0.0")
11 | ```
12 |
13 | [Or add the package in Xcode.](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app)
14 |
15 | ## API
16 |
17 | See the [documentation](https://swiftpackageindex.com/sindresorhus/ExtendedAttributes/documentation/extendedattributes).
18 |
19 | ## Related
20 |
21 | - [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults
22 | - [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
23 | - [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift+archived%3Afalse&type=repositories)
24 |
--------------------------------------------------------------------------------