an atproto pds written in F# (.NET 9) 馃
pds
fsharp
giraffe
dotnet
atproto
1namespace PDSharp.Core
2
3open System
4open System.Text
5
6/// Minimal Base32 (RFC 4648 Lowercase)
7module Base32Encoding =
8 let private alphabet = "abcdefghijklmnopqrstuvwxyz234567"
9
10 let ToString (data : byte[]) : string =
11 if data.Length = 0 then
12 ""
13 else
14 let mutable i = 0
15 let mutable index = 0
16 let mutable digit = 0
17 let mutable currByte = 0
18 let mutable nextByte = 0
19 let sb = StringBuilder((data.Length + 7) * 8 / 5)
20
21 while i < data.Length do
22 currByte <- (int data.[i]) &&& 0xFF
23
24 if index > 3 then
25 if (i + 1) < data.Length then
26 nextByte <- (int data.[i + 1]) &&& 0xFF
27 else
28 nextByte <- 0
29
30 digit <- currByte &&& (0xFF >>> index)
31 index <- (index + 5) % 8
32 digit <- digit <<< index
33 digit <- digit ||| (nextByte >>> (8 - index))
34 i <- i + 1
35 else
36 digit <- currByte >>> 8 - (index + 5) &&& 0x1F
37 index <- (index + 5) % 8
38
39 if index = 0 then
40 i <- i + 1
41
42 sb.Append(alphabet.[digit]) |> ignore
43
44 sb.ToString()
45
46 let FromString (s : string) : byte[] option =
47 if String.IsNullOrEmpty s then
48 Some [||]
49 else
50 try
51 let bits = s.Length * 5
52 let bytes = Array.zeroCreate<byte> (bits / 8)
53 let mutable buffer = 0
54 let mutable bitsInBuffer = 0
55 let mutable byteIndex = 0
56
57 for c in s do
58 let idx = alphabet.IndexOf(Char.ToLowerInvariant c)
59
60 if idx < 0 then
61 failwith "Invalid base32 character"
62
63 buffer <- buffer <<< 5 ||| idx
64 bitsInBuffer <- bitsInBuffer + 5
65
66 if bitsInBuffer >= 8 then
67 bitsInBuffer <- bitsInBuffer - 8
68
69 if byteIndex < bytes.Length then
70 bytes.[byteIndex] <- byte ((buffer >>> bitsInBuffer) &&& 0xFF)
71 byteIndex <- byteIndex + 1
72
73 Some bytes
74 with _ ->
75 None
76
77/// Basic CID implementation for AT Protocol (CIDv1 + dag-cbor + sha2-256)
78///
79/// Constants for ATProto defaults:
80/// - Version 1 (0x01)
81/// - Codec: dag-cbor (0x71)
82/// - Hash: sha2-256 (0x12) - Length 32 (0x20)
83[<Struct>]
84type Cid =
85 val Bytes : byte[]
86 new(bytes : byte[]) = { Bytes = bytes }
87
88 static member FromHash(hash : byte[]) =
89 if hash.Length <> 32 then
90 failwith "Hash must be 32 bytes (sha2-256)"
91
92 let cidBytes = Array.zeroCreate<byte> 36
93 cidBytes.[0] <- 0x01uy
94 cidBytes.[1] <- 0x71uy
95 cidBytes.[2] <- 0x12uy
96 cidBytes.[3] <- 0x20uy
97 Array.Copy(hash, 0, cidBytes, 4, 32)
98 Cid cidBytes
99
100 static member TryParse(s : string) : Cid option =
101 if String.IsNullOrWhiteSpace s then
102 None
103 elif s.StartsWith("b") then
104 match Base32Encoding.FromString(s.Substring(1)) with
105 | Some bytes when bytes.Length = 36 -> Some(Cid bytes)
106 | _ -> None
107 else
108 None
109
110 override this.ToString() =
111 "b" + Base32Encoding.ToString(this.Bytes)