Skip to content

Commit

Permalink
Initial commit 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
amosavian committed Jun 25, 2018
0 parents commit 8fc281a
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
28 changes: 28 additions & 0 deletions Package.swift
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"]),
]
)
150 changes: 150 additions & 0 deletions README.md
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

127 changes: 127 additions & 0 deletions Sources/ExtendedAttributes/ExtendedAttributes.swift
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 Tests/ExtendedAttributesTests/ExtendedAttributesTests.swift
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),
]
}
9 changes: 9 additions & 0 deletions Tests/ExtendedAttributesTests/XCTestManifests.swift
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
Loading

0 comments on commit 8fc281a

Please sign in to comment.