Skip to content

Commit 83cd313

Browse files
committed
Add IPLD and conversion document
1 parent 1c8c8d0 commit 83cd313

3 files changed

Lines changed: 266 additions & 2 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Conversion for `common/ipld.ts`
2+
3+
## `export const cborEncode = cborCodec.encode`
4+
5+
This is redundant and will not be converted. Calling the equivalent method directly is good enough.
6+
7+
## `export const cborDecode = cborCodec.decode`
8+
9+
This is redundant and will not be converted. Calling the equivalent method directly is good enough.
10+
11+
## `export const dataToCborBlock = async (data: unknown)`
12+
13+
This has been converted.
14+
15+
## `export const cidForCbor = async (data: unknown): Promise<CID>`
16+
17+
This is redundant and will not be converted. Simply getting the `cid` property of `makeCBORBlock(from:)` is enough.
18+
19+
## `export const isValidCid = async (cidStr: string): Promise<boolean>`
20+
21+
This has been converted.
22+
23+
## `export const cborBytesToRecord = (bytes: Uint8Array,): Record<string, unknown>`
24+
25+
This has been converted. However, nothing is using this method, so it will be commented out until further notice.
26+
27+
## `export const verifyCidForBytes = async (cid: CID, bytes: Uint8Array)`
28+
29+
This has been converted.
30+
31+
## `export const sha256ToCid = (hash: Uint8Array, codec: number): CID`
32+
33+
This is redundant and will not be converted. Use the following instead:
34+
35+
```swift
36+
let data = // data object...
37+
try await MultihashFactory.shared.register(SHA256Multihash())
38+
let hash = try await MultihashFactory.shared.hash(using: "sha2-256", data: data)
39+
40+
let cid = try await CID(version: .v1, data: hash.digest)
41+
```
42+
43+
## `export const sha256RawToCid = (hash: Uint8Array): CID`
44+
45+
This is redundant and will not be converted. Use `CID(version: CIDVersion = .v1, data: Data) async throws` instead.
46+
47+
## `export const parseCidFromBytes = (cidBytes: Uint8Array): CID`
48+
49+
This has been converted.
50+
51+
## `export class VerifyCidTransform extends Transform`
52+
53+
This has been converted, but as a method instead of a class.
54+
55+
## `const asError = (err: unknown): Error`
56+
57+
This is unneeded and will not be converted.
58+
59+
## `export class VerifyCidError extends Error`
60+
61+
This is unneeded and will not be converted.

Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ let package = Package(
2323
],
2424
dependencies: [
2525
.package(url: "https://github.com/ATProtoKit/MultiformatsKit.git", .upToNextMajor(from: "0.3.0")),
26-
.package(url: "https://github.com/nnabeyang/swift-cbor", from: "0.0.4")
26+
.package(url: "https://github.com/nnabeyang/swift-cbor", from: "0.0.4"),
27+
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.12.2")
2728
],
2829
targets: [
2930
// Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -33,7 +34,8 @@ let package = Package(
3334
dependencies: [
3435
"ATCommonWeb",
3536
.product(name: "MultiformatsKit", package: "multiformatskit"),
36-
.product(name: "SwiftCbor", package: "swift-cbor")
37+
.product(name: "SwiftCbor", package: "swift-cbor"),
38+
.product(name: "Crypto", package: "swift-crypto")
3739
]
3840
),
3941
.target(
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//
2+
// IPLD.swift
3+
// ATCommonTools
4+
//
5+
// Created by Christopher Jr Riley on 2025-06-19.
6+
//
7+
8+
import Foundation
9+
import SwiftCbor
10+
import MultiformatsKit
11+
import ATCommonWeb
12+
import Crypto
13+
14+
/// A namespace for IPLD-related methods.
15+
public enum IPLD {
16+
17+
/// Converts a `Data` object to a CBOR block.
18+
///
19+
/// - Parameter value: The `Data` value to convert.
20+
/// - Returns: A tuple, which contains the original data value, the encoded CBOR block, and the CID of
21+
/// the hashed value.
22+
///
23+
/// - Throws: An error if CBOR encoding fails, data hashing fails, or the method fails to create
24+
/// a `CID` object.
25+
public static func makeCBORBlock(from data: Data) async throws -> (hashedValue: Data, cborBytes: Data, cid: CID) {
26+
let cborEncoder = CborEncoder()
27+
28+
let bytes = try cborEncoder.encode(data)
29+
30+
try await MultihashFactory.shared.register(SHA256Multihash())
31+
let hash = try await MultihashFactory.shared.hash(using: "sha2-256", data: bytes)
32+
33+
let cid = try await CID(version: .v1, data: hash.digest)
34+
35+
return (hashedValue: data, cborBytes: bytes, cid: cid)
36+
}
37+
38+
/// Determines whether the string representation of the CID is valid.
39+
///
40+
/// - `true` if it's valid, or `false` if it isn't.
41+
public static func isCIDStringValid(cidString: String) async -> Bool {
42+
do {
43+
_ = try CID.decode(from: cidString)
44+
return true
45+
} catch {
46+
return false
47+
}
48+
}
49+
50+
// /// Creates a dictionary from CBOR encoded data.
51+
// ///
52+
// /// - Parameter cborBytes: The CBOR encoded data to convert.
53+
// /// - Returns: A `[String: Data]` dictionary.
54+
// public static func makeDictionary(from cborBytes: Data) throws -> [String: Data] {
55+
// let cborDecoder = CborDecoder()
56+
//
57+
// let data = try cborDecoder.decode([String: Data].self, from: cborBytes)
58+
// return data
59+
// }
60+
61+
/// Verifies whether the given CID corresponds to the provided `Data` object.
62+
///
63+
/// - Parameters:
64+
/// - cid: The `CID` object to verify.
65+
/// - data: The raw byte data to compare against the CID.
66+
///
67+
/// - Throws: An error if the CID does not match the digest of the bytes.
68+
public static func verify(cid: CID, correspondsTo data: Data) async throws {
69+
try await MultihashFactory.shared.register(SHA256Multihash())
70+
let hash = try await MultihashFactory.shared.hash(using: "sha2-256", data: data)
71+
72+
let expectedCID = try await CID(version: .v1, data: hash.digest)
73+
74+
guard try cid.encode() == expectedCID.encode() else {
75+
throw CIDError.invalidCID(message: "The provided CID does not match the data. Expected: \(expectedCID), Actual: \(cid).")
76+
}
77+
}
78+
79+
/// Parses the `CID` object from the `Data` object.
80+
///
81+
/// - Parameter data: The `Data` object to convert.
82+
/// - Returns: A `CID` object from the `Data` object.
83+
///
84+
/// - Throws: An error if the version number, codec, hash type, or hash length is unsupported.
85+
public static func parseCID(from data: Data) async throws -> CID {
86+
let uInt8array = [UInt8](data)
87+
88+
let version = uInt8array[0]
89+
guard version == 0x01 else {
90+
throw CIDError.invalidCID(message: "CID version is not supported. Expected: '0x01', Actual: \(version).")
91+
}
92+
93+
let codec = uInt8array[1]
94+
guard codec == 0x55 || codec == 0x71 else {
95+
throw CIDError.invalidCID(message: "Unsupported codec. Expected: '0x55' or '0x71', Actual: \(codec).")
96+
}
97+
98+
let hashType = uInt8array[2]
99+
guard hashType == 0x12 else {
100+
throw CIDError.invalidCID(message: "Unsupported hash type. Expected: '0x12', Actual: \(hashType).")
101+
}
102+
103+
let hashLength = uInt8array[3]
104+
guard hashLength == 32 else {
105+
throw CIDError.invalidCID(message: "Unsupported hash length. Expected: '32', Actual: \(hashLength).")
106+
}
107+
108+
let remainingData = Data((uInt8array[4..<uInt8array.count]))
109+
110+
return try await CID(version: .v1, data: remainingData)
111+
}
112+
113+
// public static func verifyCIDTransformStream(
114+
// input: AsyncStream<Data>,
115+
// expectedCID: CID
116+
// ) -> AsyncThrowingStream<Data, Error> {
117+
// var hasher = SHA256()
118+
//
119+
// return AsyncThrowingStream { continuation in
120+
// do {
121+
// for try await chunk in input {
122+
// hasher.update(data: chunk)
123+
// continuation.yield(chunk)
124+
// }
125+
//
126+
// let digest = hasher.finalize()
127+
// let actualCID = try await CID(version: .v1, data: Data(digest))
128+
//
129+
// guard actualCID == expectedCID else {
130+
// throw CIDError.invalidCID(
131+
// message: "The provided CID does not match the data. Expected: \(expectedCID), Actual: \(actualCID)."
132+
// )
133+
// }
134+
//
135+
// continuation.finish()
136+
// } catch {
137+
// continuation.finish(throwing: error)
138+
// }
139+
// }
140+
// }
141+
142+
/// Returns a validated stream of `Data` chunks, ensuring the final stream contents match the given CID.
143+
///
144+
/// - Parameters:
145+
/// - stream: The input `AsyncStream<Data>` representing a sequence of raw data chunks.
146+
/// - expectedCID: The expected `CID` against which the final SHA-256 digest will be compared.
147+
///
148+
/// - Returns: An `AsyncThrowingStream<Data, Error>` that yields the same chunks as the input stream.
149+
/// If the hash of the complete stream does not match `expectedCID`, the stream will terminate with
150+
/// a `CIDError.invalidCID`.
151+
public static func verifyCIDStream(
152+
for stream: AsyncStream<Data>,
153+
expectedCID: CID
154+
) -> AsyncThrowingStream<Data, Error> {
155+
AsyncThrowingStream { continuation in
156+
Task {
157+
var hasher = SHA256()
158+
do {
159+
for try await chunk in stream {
160+
hasher.update(data: chunk)
161+
continuation.yield(chunk)
162+
}
163+
164+
let actualCID = try await CID(version: .v1, data: Data(hasher.finalize()))
165+
166+
if actualCID != expectedCID {
167+
throw CIDError.invalidCID(message: "The provided CID does not match the data. Expected: \(expectedCID), Actual: \(actualCID).")
168+
}
169+
170+
continuation.finish()
171+
} catch {
172+
continuation.finish(throwing: error)
173+
}
174+
}
175+
}
176+
}
177+
178+
}
179+
180+
/// A class that simulates a Transform stream which passes data through
181+
/// and verifies the CID at the end.
182+
public class VerifyCIDTransform {
183+
184+
/// The SHA256 hash/
185+
private var hasher = SHA256()
186+
187+
/// The expected `CID` object.
188+
private let expectedCid: CID
189+
190+
/// Any data that's being collected.
191+
private var collectedData = Data()
192+
193+
/// Initializes a `VerifyCIDTransform`.
194+
///
195+
/// - Parameter cid: A `CID` object.
196+
public init(cid: CID) {
197+
self.expectedCid = cid
198+
}
199+
200+
201+
}

0 commit comments

Comments
 (0)