-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8fc281a
Showing
7 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// swift-tools-version:4.0 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "ExtendedAttributes", | ||
products: [ | ||
// Products define the executables and libraries produced by a package, and make them visible to other packages. | ||
.library( | ||
name: "ExtendedAttributes", | ||
targets: ["ExtendedAttributes"]), | ||
], | ||
dependencies: [ | ||
// Dependencies declare other packages that this package depends on. | ||
// .package(url: /* package url */, from: "1.0.0"), | ||
], | ||
targets: [ | ||
// Targets are the basic building blocks of a package. A target can define a module or a test suite. | ||
// Targets can depend on other targets in this package, and on products in packages which this package depends on. | ||
.target( | ||
name: "ExtendedAttributes", | ||
dependencies: []), | ||
.testTarget( | ||
name: "ExtendedAttributesTests", | ||
dependencies: ["ExtendedAttributes"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# ExtendedAttributes | ||
|
||
This library handles file extended attributesby extending `URL` struct. | ||
|
||
<center> | ||
[![Swift Version][swift-image]][swift-url] | ||
[![Platform][platform-image]](#) | ||
[![License][license-image]][license-url] | ||
[![Release version][release-image]][release-url] | ||
</center> | ||
|
||
## Requirements | ||
|
||
- Swift 4.0 or higher | ||
- macOS, iOS, tvOS or Linux | ||
- XCode 9.0 | ||
|
||
## Installation | ||
|
||
First you must clone this project from github: | ||
|
||
```bash | ||
git clone https://github.com/amosavian/ExtendedAttributes | ||
``` | ||
|
||
Then you can either install manually by adding `Sources/ExtendedAttributes ` directory to your project | ||
or create a `xcodeproj` file and add it as a dynamic framework: | ||
|
||
```bash | ||
swift package generate-xcodeproj | ||
``` | ||
|
||
## Usage | ||
|
||
Extended attributes only work with urls that begins with `file:///`. | ||
|
||
### Listing | ||
|
||
To get which extended attributes are set for file: | ||
|
||
```swift | ||
do { | ||
print(try url.listExtendedAttributes()) | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
### Retrieving | ||
|
||
To check either a specific extended attribute exists or not: | ||
|
||
```swift | ||
if url.hasExtendedAttribute(forName: "eaName") { | ||
// Do something | ||
} | ||
``` | ||
|
||
To retrieve raw data for an extended attribute, simply use this code as template, Please note if extended attribute doesn't exist, it will throw an error. | ||
|
||
```swift | ||
do { | ||
let data = try url.extendedAttribute(forName: "eaName") | ||
print(data as NSData) | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
You can retrieve values of extended attributes if they are set with standard plist binary format. This can be `String`, `Int`/`NSNumber`, `Double`, `Bool`, `URL`, `Date`, `Array` or `Dictionary`. Arrays should not contain `nil` value. | ||
|
||
To retrieve raw data for an extended attribute, simply use this code as template: | ||
|
||
```swift | ||
do { | ||
let notes: String = try url.extendedAttributeValue(forName: "notes") | ||
print("Notes:", notes) | ||
let isDownloeded: Bool = try url.extendedAttributeValue(forName: "isdownloaded") | ||
print("isDownloaded:", isDownloeded) | ||
let originURL: URL = try url.extendedAttributeValue(forName: "originurl") | ||
print("Original url:", originurl) | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
or to list all values of a file: | ||
|
||
```swift | ||
do { | ||
for name in try url.listExtendedAttributes() { | ||
let value = try url.extendedAttributeValue(forName: name) | ||
print(name, ":" , value) | ||
} | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
### Setting attributes | ||
|
||
To set raw data for an extended attribute: | ||
|
||
```swift | ||
do { | ||
try url.setExtendedAttribute(data: Data(bytes: [0xFF, 0x20]), forName: "data") | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
To set a value for an extended attribute: | ||
|
||
```swift | ||
do { | ||
let dictionary: [String: Any] = ["name": "Amir", "age": 30] | ||
try url.setExtendedAttribute(value: dictionary, forName: "identity") | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
### Removing | ||
|
||
To remove an extended attribute: | ||
|
||
```swift | ||
do { | ||
try url.removeExtendedAttribute(forName: "identity") | ||
} catch { | ||
print(error.localizedDescription) | ||
} | ||
``` | ||
|
||
## Known issues | ||
|
||
Check [Issues](https://github.com/amosavian/ExtendedAttributes/issues) page. | ||
|
||
## Contribute | ||
|
||
We would love for you to contribute to ExtendedAttributes, check the [LICENSE][license-url] file for more info. | ||
|
||
[swift-image]: https://img.shields.io/badge/swift-4.0-orange.svg | ||
[swift-url]: https://swift.org/ | ||
[platform-image]: https://img.shields.io/badge/platform-macOS|iOS|tvOS|Linux-lightgray.svg | ||
[license-image]: https://img.shields.io/github/license/amosavian/ExtendedAttributes.svg | ||
[license-url]: LICENSE | ||
[release-url]: https://github.com/amosavian/ExtendedAttributes/releases | ||
[release-image]: https://img.shields.io/github/release/amosavian/ExtendedAttributes.svg | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// | ||
// ExtendedAttributes.swift | ||
// ExtendedAttributes | ||
// | ||
// Created by Amir Abbas Mousavian. | ||
// Copyright © 2018 Mousavian. Distributed under MIT license. | ||
// | ||
// Adopted from https://stackoverflow.com/a/38343753/5304286 | ||
|
||
import Foundation | ||
|
||
public extension URL { | ||
/// Checks extended attribute has value | ||
public func hasExtendedAttribute(forName name: String) -> Bool { | ||
guard isFileURL else { | ||
return false | ||
} | ||
|
||
return self.withUnsafeFileSystemRepresentation { fileSystemPath -> Bool in | ||
return getxattr(fileSystemPath, name, nil, 0, 0, 0) > 0 | ||
} | ||
} | ||
|
||
/// Get extended attribute. | ||
public func extendedAttribute(forName name: String) throws -> Data { | ||
try checkFileURL() | ||
let data = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> Data in | ||
// Determine attribute size: | ||
let length = getxattr(fileSystemPath, name, nil, 0, 0, 0) | ||
guard length >= 0 else { throw URL.posixError(errno) } | ||
|
||
// Create buffer with required size: | ||
var data = Data(count: length) | ||
|
||
// Retrieve attribute: | ||
if length > 0 { | ||
let result = data.withUnsafeMutableBytes { | ||
getxattr(fileSystemPath, name, $0, length, 0, 0) | ||
} | ||
guard result >= 0 else { throw URL.posixError(errno) } | ||
} | ||
|
||
return data | ||
} | ||
return data | ||
} | ||
|
||
/// Value of extended attribute. | ||
public func extendedAttributeValue<T>(forName name: String) throws -> T { | ||
try checkFileURL() | ||
let data = try extendedAttribute(forName: name) | ||
let value = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) | ||
guard let result = value as? T else { | ||
throw CocoaError(.propertyListReadCorrupt) | ||
} | ||
return result | ||
} | ||
|
||
/// Set extended attribute. | ||
public func setExtendedAttribute(data: Data, forName name: String) throws { | ||
try checkFileURL() | ||
try self.withUnsafeFileSystemRepresentation { fileSystemPath in | ||
let result = data.withUnsafeBytes { | ||
setxattr(fileSystemPath, name, $0, data.count, 0, 0) | ||
} | ||
guard result >= 0 else { throw URL.posixError(errno) } | ||
} | ||
} | ||
|
||
/// Set extended attribute. | ||
public func setExtendedAttribute<T>(value: T, forName name: String) throws { | ||
try checkFileURL() | ||
|
||
// In some cases, like when value contains nil, PropertyListSerialization would crash. | ||
guard PropertyListSerialization.propertyList(value, isValidFor: .binary) else { | ||
throw CocoaError(.propertyListWriteInvalid) | ||
} | ||
|
||
let data = try PropertyListSerialization.data(fromPropertyList: value, format: .binary, options:0) | ||
try setExtendedAttribute(data: data, forName: name) | ||
} | ||
|
||
/// Remove extended attribute. | ||
public func removeExtendedAttribute(forName name: String) throws { | ||
try checkFileURL() | ||
try self.withUnsafeFileSystemRepresentation { fileSystemPath in | ||
let result = removexattr(fileSystemPath, name, 0) | ||
guard result >= 0 else { throw URL.posixError(errno) } | ||
} | ||
} | ||
|
||
/// Get list of all extended attributes. | ||
public func listExtendedAttributes() throws -> [String] { | ||
try checkFileURL() | ||
let list = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in | ||
let length = listxattr(fileSystemPath, nil, 0, 0) | ||
guard length >= 0 else { throw URL.posixError(errno) } | ||
|
||
// Create buffer with required size: | ||
var data = Data(count: length) | ||
|
||
// Retrieve attribute list: | ||
let result = data.withUnsafeMutableBytes { | ||
listxattr(fileSystemPath, $0, length, 0) | ||
} | ||
guard result >= 0 else { throw URL.posixError(errno) } | ||
|
||
// Extract attribute names: | ||
let list = data.split(separator: 0).compactMap { | ||
String(data: Data($0), encoding: .utf8) | ||
} | ||
return list | ||
} | ||
return list | ||
} | ||
|
||
/// Helper function to create an NSError from a Unix errno. | ||
private static func posixError(_ err: Int32) -> POSIXError { | ||
return POSIXError(POSIXErrorCode(rawValue: err) ?? .EPERM) | ||
} | ||
|
||
private func checkFileURL() throws { | ||
guard isFileURL else { | ||
throw CocoaError(.fileNoSuchFile) | ||
} | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
Tests/ExtendedAttributesTests/ExtendedAttributesTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import XCTest | ||
@testable import ExtendedAttributes | ||
|
||
final class ExtendedAttributesTests: XCTestCase { | ||
func testExtendedAttributes() { | ||
// Use XCTAssert and related functions to verify your tests produce the correct | ||
// results. | ||
|
||
let url = URL(fileURLWithPath: NSTemporaryDirectory()) | ||
.appendingPathComponent(UUID().uuidString) | ||
|
||
do { | ||
try "Hello World!".data(using: .utf8)!.write(to: url) | ||
|
||
try url.setExtendedAttribute(data: Data(bytes: [0xFF, 0x20]), forName: "DataAttrib") | ||
XCTAssertTrue(url.hasExtendedAttribute(forName: "DataAttrib")) | ||
let data = try url.extendedAttribute(forName: "DataAttrib") | ||
XCTAssertEqual([UInt8](data), [0xFF, 0x20]) | ||
try url.removeExtendedAttribute(forName: "DataAttrib") | ||
XCTAssertFalse(url.hasExtendedAttribute(forName: "DataAttrib")) | ||
XCTAssertThrowsError(try url.extendedAttribute(forName: "DataAttrib")) | ||
|
||
let date = Date() | ||
try url.setExtendedAttribute(value: date, forName: "DateAttrib") | ||
XCTAssertEqual(try url.extendedAttributeValue(forName: "DateAttrib"), date) | ||
|
||
let str = "This is test." | ||
try url.setExtendedAttribute(value: str, forName: "StringAttrib") | ||
XCTAssertEqual(try url.extendedAttributeValue(forName: "StringAttrib"), str) | ||
|
||
try url.setExtendedAttribute(value: true, forName: "BoolAttrib") | ||
XCTAssertEqual(try url.extendedAttributeValue(forName: "BoolAttrib"), true) | ||
|
||
let num = 1453 | ||
try url.setExtendedAttribute(value: num, forName: "NumericAttrib") | ||
XCTAssertEqual(try url.extendedAttributeValue(forName: "NumericAttrib"), num) | ||
|
||
let array: [Int] = [1970, 622, -323] | ||
try url.setExtendedAttribute(value: array, forName: "ArrayAttrib") | ||
XCTAssertEqual(try url.extendedAttributeValue(forName: "ArrayAttrib"), array) | ||
|
||
let dictionary: [String: Any] = ["name": "Amir", "age": 30] | ||
try url.setExtendedAttribute(value: dictionary, forName: "DictionaryAttrib") | ||
let retrDic: [String: Any] = try url.extendedAttributeValue(forName: "DictionaryAttrib") | ||
XCTAssertEqual(retrDic["name"] as? String, dictionary["name"] as? String) | ||
XCTAssertEqual(retrDic["age"] as? Int, dictionary["age"] as? Int) | ||
|
||
let vnil: Int? = nil | ||
XCTAssertThrowsError(try url.setExtendedAttribute(value: vnil, forName: "NilAttrib")) | ||
|
||
XCTAssertEqual(try url.listExtendedAttributes(), [ | ||
"ArrayAttrib", | ||
"BoolAttrib", | ||
"DateAttrib", | ||
"DictionaryAttrib", | ||
"NumericAttrib", | ||
"StringAttrib", | ||
]) | ||
try FileManager.default.removeItem(at: url) | ||
} catch { | ||
XCTFail(error.localizedDescription) | ||
} | ||
} | ||
|
||
|
||
static var allTests = [ | ||
("testExtendedAttributes", testExtendedAttributes), | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import XCTest | ||
|
||
#if !canImport(Darwin) | ||
public func allTests() -> [XCTestCaseEntry] { | ||
return [ | ||
testCase(ExtendedAttributesTests.allTests), | ||
] | ||
} | ||
#endif |
Oops, something went wrong.