Skip to content

Commit

Permalink
Merge pull request #1 from uditha-atukorala/feature/codable
Browse files Browse the repository at this point in the history
Codable protocol implementation for Ids
  • Loading branch information
Uditha Atukorala authored Jun 13, 2022
2 parents a3ef3e7 + 32f61db commit adcdc0b
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 115 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ let package = Package(
],
path: "src"),
.testTarget(
name: "xidTests",
name: "tests",
dependencies: ["xid"],
path: "test"),
]
Expand Down
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ sortable property of the id.
## Usage

```swift
let id = NewXid()
let id: String = NewXid() // or let id: Id = NewXid()
print(id)
// Output: caia5ng890f0tr46f690
```
Expand All @@ -83,6 +83,48 @@ print(id.data as NSData)
// Output: {length = 12, bytes = 0x62a4a4a108481e0f9b83781f}
```

### Encoding and Decoding

The `Id` structure complies with `Codable` protocol and can be converted into and out of an external representation (e.g. JSON).

#### Decoding from JSON

```swift
struct User: Decodable {
var id: Id
var name: String
}

let data = """
{
"id": "caia5ng890f0tr00hgtg",
"name": "Jane Smith"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)

print(user.id)
// Output: caia5ng890f0tr00hgtg
```

#### Encoding into JSON
```swift
struct User: Encodable {
var id: Id
var name: String
}

let user = User(id: NewXid(), name: "Jane Smith")

let encoder = JSONEncoder()
let data = try encoder.encode(user)

print(String(data: data, encoding: .utf8)!)
// Output: {"id":"caia5ng890f0tr00hgtg","name":"Jane Smith"}
```


[^1]: https://www.mongodb.com/docs/manual/reference/method/ObjectId/
[^2]: https://datatracker.ietf.org/doc/html/rfc4648#section-7
Expand Down
141 changes: 104 additions & 37 deletions src/Id.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,22 @@
import Foundation

public struct Id: CustomStringConvertible {
private let alphabet = Data("0123456789abcdefghijklmnopqrstuv".utf8)
fileprivate let base32Alphabet = Data("0123456789abcdefghijklmnopqrstuv".utf8)

var bytes: Data

public var data: Data {
get { return bytes }
fileprivate let base32DecodeMap: Data = {
var map = Data(repeating: 0xff, count: 256)
for i in 0..<base32Alphabet.count {
map[Data.Index(base32Alphabet[i])] = UInt8(i)
}

public var description: String {
if bytes.count != 12 {
return ""
}
return map
}()

// base32hex encoding
var chars = Data(repeating: 0x00, count: 20)
chars[19] = alphabet[Data.Index((bytes[11] << 4) & 0x1f)]
chars[18] = alphabet[Data.Index((bytes[11] >> 1) & 0x1f)]
chars[17] = alphabet[Data.Index((bytes[11] >> 6) & 0x1f | (bytes[10] << 2) & 0x1F)]
chars[16] = alphabet[Data.Index(bytes[10]) >> 3]
chars[15] = alphabet[Data.Index(bytes[9] & 0x1f)]
chars[14] = alphabet[Data.Index((bytes[9] >> 5) | (bytes[8] << 3 ) & 0x1f)]
chars[13] = alphabet[Data.Index((bytes[8] >> 2) & 0x1f)]
chars[12] = alphabet[Data.Index(bytes[8] >> 7 | (bytes[7] << 1) & 0x1f)]
chars[11] = alphabet[Data.Index((bytes[7] >> 4) & 0x1f | (bytes[6] << 4) & 0x1f)]
chars[10] = alphabet[Data.Index((bytes[6] >> 1) & 0x1f)]
chars[9] = alphabet[Data.Index((bytes[6] >> 6) & 0x1f | (bytes[5] << 2) & 0x1f)]
chars[8] = alphabet[Data.Index(bytes[5] >> 3)]
chars[7] = alphabet[Data.Index(bytes[4] & 0x1f)]
chars[6] = alphabet[Data.Index(bytes[4] >> 5 | (bytes[3] << 3) & 0x1f)]
chars[5] = alphabet[Data.Index((bytes[3] >> 2) & 0x1f)]
chars[4] = alphabet[Data.Index(bytes[3] >> 7 | (bytes[2] << 1) & 0x1f)]
chars[3] = alphabet[Data.Index((bytes[2] >> 4) & 0x1f | (bytes[1] << 4) & 0x1f)]
chars[2] = alphabet[Data.Index((bytes[1] >> 1) & 0x1f)]
chars[1] = alphabet[Data.Index((bytes[1] >> 6) & 0x1f | (bytes[0] << 2) & 0x1f)]
chars[0] = alphabet[Data.Index(bytes[0] >> 3)]

if let str = String(bytes: chars, encoding: .utf8) {
return str
}

return ""
public struct Id {
var bytes: Data

public var data: Data {
get { return bytes }
}

public func counter() -> Int32 {
Expand Down Expand Up @@ -74,6 +49,98 @@ public struct Id: CustomStringConvertible {
}


extension Id {
public init(from: Data) throws {
if from.count != 20 {
throw XidError.invalidId
}

bytes = Data(repeating: 0x00, count: 12)
bytes[11] = base32DecodeMap[Data.Index(from[17])] << 6 | base32DecodeMap[Data.Index(from[18])] << 1 | base32DecodeMap[Data.Index(from[19])] >> 4
bytes[10] = base32DecodeMap[Data.Index(from[16])] << 3 | base32DecodeMap[Data.Index(from[17])] >> 2
bytes[9] = base32DecodeMap[Data.Index(from[14])] << 5 | base32DecodeMap[Data.Index(from[15])]
bytes[8] = base32DecodeMap[Data.Index(from[12])] << 7 | base32DecodeMap[Data.Index(from[13])] << 2 | base32DecodeMap[Data.Index(from[14])] >> 3
bytes[7] = base32DecodeMap[Data.Index(from[11])] << 4 | base32DecodeMap[Data.Index(from[12])] >> 1
bytes[6] = base32DecodeMap[Data.Index(from[9])] << 6 | base32DecodeMap[Data.Index(from[10])] << 1 | base32DecodeMap[Data.Index(from[11])] >> 4
bytes[5] = base32DecodeMap[Data.Index(from[8])] << 3 | base32DecodeMap[Data.Index(from[9])] >> 2
bytes[4] = base32DecodeMap[Data.Index(from[6])] << 5 | base32DecodeMap[Data.Index(from[7])]
bytes[3] = base32DecodeMap[Data.Index(from[4])] << 7 | base32DecodeMap[Data.Index(from[5])] << 2 | base32DecodeMap[Data.Index(from[6])] >> 3
bytes[2] = base32DecodeMap[Data.Index(from[3])] << 4 | base32DecodeMap[Data.Index(from[4])] >> 1
bytes[1] = base32DecodeMap[Data.Index(from[1])] << 6 | base32DecodeMap[Data.Index(from[2])] << 1 | base32DecodeMap[Data.Index(from[3])] >> 4
bytes[0] = base32DecodeMap[Data.Index(from[0])] << 3 | base32DecodeMap[Data.Index(from[1])] >> 2

// Validate that there are no padding in data that would cause the re-encoded id to not equal data.
var check = Data(repeating: 0x00, count: 4)
check[3] = base32Alphabet[Data.Index((bytes[11] << 4) & 0x1f)]
check[2] = base32Alphabet[Data.Index((bytes[11] >> 1) & 0x1f)]
check[1] = base32Alphabet[Data.Index((bytes[11] >> 6) & 0x1f | (bytes[10] << 2) & 0x1f)]
check[0] = base32Alphabet[Data.Index(bytes[10] >> 3)]

if check != from[16...19] {
throw XidError.decodeValidationFailure
}
}

public init(from: String) throws {
if from.count != 20 {
throw XidError.invalidIdStringLength(have: from.count, want: 20)
}

guard let data = from.data(using: .utf8) else {
throw XidError.invalidId
}

try self.init(from: data)
}
}

extension Id: CustomStringConvertible {
public var description: String {
if bytes.count != 12 {
return ""
}

// base32hex encoding
var chars = Data(repeating: 0x00, count: 20)
chars[19] = base32Alphabet[Data.Index((bytes[11] << 4) & 0x1f)]
chars[18] = base32Alphabet[Data.Index((bytes[11] >> 1) & 0x1f)]
chars[17] = base32Alphabet[Data.Index((bytes[11] >> 6) & 0x1f | (bytes[10] << 2) & 0x1f)]
chars[16] = base32Alphabet[Data.Index(bytes[10] >> 3)]
chars[15] = base32Alphabet[Data.Index(bytes[9] & 0x1f)]
chars[14] = base32Alphabet[Data.Index((bytes[9] >> 5) | (bytes[8] << 3 ) & 0x1f)]
chars[13] = base32Alphabet[Data.Index((bytes[8] >> 2) & 0x1f)]
chars[12] = base32Alphabet[Data.Index(bytes[8] >> 7 | (bytes[7] << 1) & 0x1f)]
chars[11] = base32Alphabet[Data.Index((bytes[7] >> 4) & 0x1f | (bytes[6] << 4) & 0x1f)]
chars[10] = base32Alphabet[Data.Index((bytes[6] >> 1) & 0x1f)]
chars[9] = base32Alphabet[Data.Index((bytes[6] >> 6) & 0x1f | (bytes[5] << 2) & 0x1f)]
chars[8] = base32Alphabet[Data.Index(bytes[5] >> 3)]
chars[7] = base32Alphabet[Data.Index(bytes[4] & 0x1f)]
chars[6] = base32Alphabet[Data.Index(bytes[4] >> 5 | (bytes[3] << 3) & 0x1f)]
chars[5] = base32Alphabet[Data.Index((bytes[3] >> 2) & 0x1f)]
chars[4] = base32Alphabet[Data.Index(bytes[3] >> 7 | (bytes[2] << 1) & 0x1f)]
chars[3] = base32Alphabet[Data.Index((bytes[2] >> 4) & 0x1f | (bytes[1] << 4) & 0x1f)]
chars[2] = base32Alphabet[Data.Index((bytes[1] >> 1) & 0x1f)]
chars[1] = base32Alphabet[Data.Index((bytes[1] >> 6) & 0x1f | (bytes[0] << 2) & 0x1f)]
chars[0] = base32Alphabet[Data.Index(bytes[0] >> 3)]

return String(bytes: chars, encoding: .utf8) ?? ""
}
}

extension Id: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
try self.init(from: try container.decode(String.self))
}
}

extension Id: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(String(describing: self))
}
}

extension Id: Equatable {
public static func == (lhs: Id, rhs: Id) -> Bool {
lhs.bytes == rhs.bytes
Expand Down
52 changes: 16 additions & 36 deletions src/Xid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,26 @@ import UIKit
#endif

public struct Xid {
private let counter = ManagedAtomic<Int32>(0)
private var _mid: Data?
private var _pid: Data?

var mid: Data {
mutating get {
if _mid == nil {
_mid = machineId()
}
private(set) static var counter: ManagedAtomic<Int32> = {
var i: Int32 = 0
let status = withUnsafeMutableBytes(of: &i) { ptr in
SecRandomCopyBytes(kSecRandomDefault, ptr.count, ptr.baseAddress!)
}

return _mid!
if status != errSecSuccess {
i = Int32.random(in: Int32.min...Int32.max)
}
}

var pid: Data {
mutating get {
if _pid == nil {
_pid = processId()
}
return ManagedAtomic<Int32>(i)
}()

return _pid!
}
}
private(set) lazy var mid: Data = {
machineId()
}()

public init() {
counter.store(random(), ordering: .relaxed)
}
private(set) lazy var pid: Data = {
processId()
}()

public mutating func next() -> Id {
var bytes = Data(repeating: 0x00, count: 12)
Expand All @@ -55,7 +48,7 @@ public struct Xid {
bytes[8] = pid[1]

// Increment, 3 bytes (big endian)
let i = counter.wrappingIncrementThenLoad(ordering: .relaxed)
let i = Xid.counter.wrappingIncrementThenLoad(ordering: .relaxed)
bytes[9] = UInt8((i & 0xff0000) >> 16)
bytes[10] = UInt8((i & 0x00ff00) >> 8)
bytes[11] = UInt8(i & 0x0000ff)
Expand Down Expand Up @@ -96,19 +89,6 @@ public struct Xid {
return Data(data[2...3])
}

func random() -> Int32 {
var i: Int32 = 0
let status = withUnsafeMutableBytes(of: &i) { ptr in
SecRandomCopyBytes(kSecRandomDefault, ptr.count, ptr.baseAddress!)
}

if status != errSecSuccess {
i = Int32.random(in: Int32.min...Int32.max)
}

return i
}

func timestamp() -> Data {
var n = UInt32(Date().timeIntervalSince1970).bigEndian
let data = Data(bytes: &n, count: MemoryLayout.size(ofValue: n))
Expand Down
5 changes: 5 additions & 0 deletions src/XidError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enum XidError: Error, Equatable {
case decodeValidationFailure
case invalidId
case invalidIdStringLength(have: Int, want: Int)
}
22 changes: 22 additions & 0 deletions src/helpers.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import Foundation

private var xid = Xid()

public func NewXid() -> Id {
xid.next()
}

public func NewXid() -> String {
String(describing: xid.next())
}

public func NewXid(bytes: Data) throws -> Id {
if bytes.count != 12 {
throw XidError.invalidId
}

return Id(bytes: bytes)
}

public func NewXid(from: Data) throws -> Id {
try Id(from: from)
}

public func NewXid(from: String) throws -> Id {
try Id(from: from)
}
Loading

0 comments on commit adcdc0b

Please sign in to comment.