···1010 abstract member Get : Cid -> Async<byte[] option>
1111 abstract member Put : byte[] -> Async<Cid>
1212 abstract member Has : Cid -> Async<bool>
1313+ abstract member GetAllCidsAndData : unit -> Async<(Cid * byte[]) list>
13141415 /// In-memory implementation of IBlockStore for testing
1516 type MemoryBlockStore() =
1616- let store = ConcurrentDictionary<string, byte[]>()
1717+ let store = ConcurrentDictionary<string, (Cid * byte[])>()
17181819 let cidKey (cid : Cid) =
1920 System.Convert.ToBase64String(cid.Bytes)
···2122 interface IBlockStore with
2223 member _.Get(cid : Cid) = async {
2324 let key = cidKey cid
2424- let success, data = store.TryGetValue(key)
2525- return if success then Some data else None
2525+2626+ match store.TryGetValue(key) with
2727+ | true, (_, data) -> return Some data
2828+ | false, _ -> return None
2629 }
27302831 member _.Put(data : byte[]) = async {
2932 let hash = Crypto.sha256 data
3033 let cid = Cid.FromHash hash
3134 let key = cidKey cid
3232- store.[key] <- data
3535+ store.[key] <- (cid, data)
3336 return cid
3437 }
3538···3740 let key = cidKey cid
3841 return store.ContainsKey(key)
3942 }
4343+4444+ member _.GetAllCidsAndData() = async { return store.Values |> Seq.toList }
40454146 /// Get the number of blocks stored (for testing)
4247 member _.Count = store.Count
+85
PDSharp.Core/Car.fs
···11+namespace PDSharp.Core
22+33+open System
44+open System.IO
55+66+/// CARv1 (Content Addressable aRchives) writer module
77+/// Implements the CAR format per https://ipld.io/specs/transport/car/carv1/
88+module Car =
99+ /// Encode an unsigned integer as LEB128 varint
1010+ let encodeVarint (value : int) : byte[] =
1111+ if value < 0 then
1212+ failwith "Varint value must be non-negative"
1313+ elif value = 0 then
1414+ [| 0uy |]
1515+ else
1616+ use ms = new MemoryStream()
1717+ let mutable v = value
1818+1919+ while v > 0 do
2020+ let mutable b = byte (v &&& 0x7F)
2121+ v <- v >>> 7
2222+2323+ if v > 0 then
2424+ b <- b ||| 0x80uy
2525+2626+ ms.WriteByte(b)
2727+2828+ ms.ToArray()
2929+3030+ /// Create CAR header as DAG-CBOR encoded bytes
3131+ /// Header format: { version: 1, roots: [CID, ...] }
3232+ let createHeader (roots : Cid list) : byte[] =
3333+ let headerMap =
3434+ Map.ofList [ ("roots", box (roots |> List.map box)); ("version", box 1) ]
3535+3636+ DagCbor.encode headerMap
3737+3838+ /// Encode a single block section: varint(len) | CID bytes | block data
3939+ let encodeBlock (cid : Cid) (data : byte[]) : byte[] =
4040+ let cidBytes = cid.Bytes
4141+ let sectionLen = cidBytes.Length + data.Length
4242+ let varintBytes = encodeVarint sectionLen
4343+4444+ use ms = new MemoryStream()
4545+ ms.Write(varintBytes, 0, varintBytes.Length)
4646+ ms.Write(cidBytes, 0, cidBytes.Length)
4747+ ms.Write(data, 0, data.Length)
4848+ ms.ToArray()
4949+5050+ /// Create a complete CARv1 file from roots and blocks
5151+ /// CAR format: [varint | header] [varint | CID | block]...
5252+ let createCar (roots : Cid list) (blocks : (Cid * byte[]) seq) : byte[] =
5353+ use ms = new MemoryStream()
5454+5555+ let headerBytes = createHeader roots
5656+ let headerVarint = encodeVarint headerBytes.Length
5757+ ms.Write(headerVarint, 0, headerVarint.Length)
5858+ ms.Write(headerBytes, 0, headerBytes.Length)
5959+6060+ for cid, data in blocks do
6161+ let blockSection = encodeBlock cid data
6262+ ms.Write(blockSection, 0, blockSection.Length)
6363+6464+ ms.ToArray()
6565+6666+ /// Create a CAR from a single root with an async block fetcher
6767+ let createCarAsync (roots : Cid list) (getBlock : Cid -> Async<byte[] option>) (allCids : Cid seq) = async {
6868+ use ms = new MemoryStream()
6969+7070+ let headerBytes = createHeader roots
7171+ let headerVarint = encodeVarint headerBytes.Length
7272+ ms.Write(headerVarint, 0, headerVarint.Length)
7373+ ms.Write(headerBytes, 0, headerBytes.Length)
7474+7575+ for cid in allCids do
7676+ let! dataOpt = getBlock cid
7777+7878+ match dataOpt with
7979+ | Some data ->
8080+ let blockSection = encodeBlock cid data
8181+ ms.Write(blockSection, 0, blockSection.Length)
8282+ | None -> ()
8383+8484+ return ms.ToArray()
8585+ }
+41
PDSharp.Core/Cid.fs
···43434444 sb.ToString()
45454646+ let FromString (s : string) : byte[] option =
4747+ if String.IsNullOrEmpty s then
4848+ Some [||]
4949+ else
5050+ try
5151+ let bits = s.Length * 5
5252+ let bytes = Array.zeroCreate<byte> (bits / 8)
5353+ let mutable buffer = 0
5454+ let mutable bitsInBuffer = 0
5555+ let mutable byteIndex = 0
5656+5757+ for c in s do
5858+ let idx = alphabet.IndexOf(Char.ToLowerInvariant c)
5959+6060+ if idx < 0 then
6161+ failwith "Invalid base32 character"
6262+6363+ buffer <- buffer <<< 5 ||| idx
6464+ bitsInBuffer <- bitsInBuffer + 5
6565+6666+ if bitsInBuffer >= 8 then
6767+ bitsInBuffer <- bitsInBuffer - 8
6868+6969+ if byteIndex < bytes.Length then
7070+ bytes.[byteIndex] <- byte ((buffer >>> bitsInBuffer) &&& 0xFF)
7171+ byteIndex <- byteIndex + 1
7272+7373+ Some bytes
7474+ with _ ->
7575+ None
7676+4677/// Basic CID implementation for AT Protocol (CIDv1 + dag-cbor + sha2-256)
4778///
4879/// Constants for ATProto defaults:
···6596 cidBytes.[3] <- 0x20uy
6697 Array.Copy(hash, 0, cidBytes, 4, 32)
6798 Cid cidBytes
9999+100100+ static member TryParse(s : string) : Cid option =
101101+ if String.IsNullOrWhiteSpace s then
102102+ None
103103+ elif s.StartsWith("b") then
104104+ match Base32Encoding.FromString(s.Substring(1)) with
105105+ | Some bytes when bytes.Length = 36 -> Some(Cid bytes)
106106+ | _ -> None
107107+ else
108108+ None
6810969110 override this.ToString() =
70111 "b" + Base32Encoding.ToString(this.Bytes)