├── .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 | --------------------------------------------------------------------------------