p2p-cljs / src / ird / p2p / impl / bytes.cljs
670 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
(ns ird.p2p.impl.bytes
  "Byte helpers for the rest of the SDK. Bytes never cross a public
  namespace boundary as JS objects: they travel as strings — standard
  base64 (padded, `+/=`: inside envelopes), URL-safe base64 (unpadded,
  `-_`: identifiers, keys, secrets) or lowercase hex (vectors, IVs). This
  namespace is the only place that turns those strings into Buffers and
  back, so it is also the one applications reach for when they have raw
  bytes to hand to `ird.p2p` (`utf8->b64`, `b64->utf8`, ...).

  Internal interop namespace: js/ and Buffer live here (Node 18 globals)."
  (:require [clojure.string :as str]))

(defn ->buffer
  "A Buffer from BYTES (Uint8Array, Buffer or ArrayBuffer)."
  [bytes]
  (if (instance? js/ArrayBuffer bytes)
    (js/Buffer.from bytes)
    (js/Buffer.from bytes)))

(defn buffer->b64
  "Standard base64 of BYTES (Uint8Array, Buffer or ArrayBuffer)."
  [bytes]
  (.toString (->buffer bytes) "base64"))

(defn b64->buffer
  "Decode standard base64 S into a Buffer. Lenient like Node: pass the
  string through `valid-b64?` first when strictness matters."
  [s]
  (js/Buffer.from s "base64"))

(defn buffer->u8
  "A fresh Uint8Array copy of BYTES (Buffer/Uint8Array)."
  [bytes]
  (js/Uint8Array.from (->buffer bytes)))

(defn b64->u8
  "Decode standard base64 S into a Uint8Array."
  [s]
  (buffer->u8 (b64->buffer s)))

(defn u8->b64 [u8] (buffer->b64 u8))

(defn valid-b64?
  "True when S is well-formed standard base64 (padding required)."
  [s]
  (and (string? s)
       (zero? (mod (count s) 4))
       (boolean (re-matches #"[A-Za-z0-9+/]*={0,2}" s))))

(defn valid-b64url?
  "True when S is well-formed URL-safe base64 without padding."
  [s]
  (and (string? s)
       (not= 1 (mod (count s) 4))
       (boolean (re-matches #"[A-Za-z0-9_-]*" s))))

(defn b64->b64url
  "Standard base64 → URL-safe unpadded."
  [s]
  (-> s (str/replace "+" "-") (str/replace "/" "_") (str/replace #"=+$" "")))

(defn b64url->b64
  "URL-safe base64 (padding optional) → standard padded base64. Returns nil
  when S is not URL-safe base64."
  [s]
  (when (valid-b64url? s)
    (let [t (-> s (str/replace "-" "+") (str/replace "_" "/") (str/replace #"=+$" ""))
          rem (mod (count t) 4)]
      (if (zero? rem) t (str t (apply str (repeat (- 4 rem) "=")))))))

(defn b64url-byte-length
  "Number of bytes S (URL-safe base64) decodes to; nil when malformed."
  [s]
  (when-let [b (b64url->b64 s)]
    (.-length (b64->buffer b))))

(defn byte-length
  "Number of bytes B64 (standard base64) decodes to."
  [b64]
  (.-length (b64->buffer b64)))

(defn hex->b64
  "Lowercase/uppercase hex → standard base64; nil when malformed."
  [hex]
  (when (and (string? hex) (even? (count hex)) (re-matches #"[0-9a-fA-F]*" hex))
    (buffer->b64 (js/Buffer.from hex "hex"))))

(defn b64->hex
  "Standard base64 → lowercase hex."
  [b64]
  (.toString (b64->buffer b64) "hex"))

(defn utf8->b64
  "UTF-8 bytes of string S as standard base64."
  [s]
  (buffer->b64 (js/Buffer.from s "utf8")))

(defn b64->utf8
  "Decode standard base64 B64 as UTF-8 text; nil when the bytes are not
  valid UTF-8."
  [b64]
  (try
    (.decode (js/TextDecoder. "utf-8" #js {:fatal true}) (b64->buffer b64))
    (catch :default _ nil)))

(defn utf8-byte-length
  "Number of UTF-8 bytes in string S."
  [s]
  (js/Buffer.byteLength s "utf8"))

(defn concat-b64
  "Standard base64 of the concatenation of PARTS (each standard base64)."
  [& parts]
  (buffer->b64 (js/Buffer.concat (to-array (map b64->buffer parts)))))

(defn slice-b64
  "Standard base64 of bytes [START, END) of B64 (END defaults to the end)."
  ([b64 start] (buffer->b64 (.subarray (b64->buffer b64) start)))
  ([b64 start end] (buffer->b64 (.subarray (b64->buffer b64) start end))))

(defn byte-at
  "The unsigned byte at index I of B64, or nil past the end."
  [b64 i]
  (let [b (b64->buffer b64)]
    (when (< i (.-length b)) (aget b i))))

(defn bytes->b64
  "Standard base64 of a seq of byte values (0..255)."
  [byte-values]
  (buffer->b64 (js/Buffer.from (to-array byte-values))))

(defn b64->bytes
  "Vector of byte values of B64."
  [b64]
  (vec (array-seq (b64->buffer b64))))

(defn random-b64
  "N cryptographically random bytes as standard base64."
  [n]
  (let [u8 (js/Uint8Array. n)]
    (.getRandomValues js/crypto u8)
    (u8->b64 u8)))

(defn random-hex
  "N cryptographically random bytes as lowercase hex."
  [n]
  (b64->hex (random-b64 n)))

(defn random-b64url
  "N cryptographically random bytes as URL-safe base64."
  [n]
  (b64->b64url (random-b64 n)))

(defn int->hex
  "Non-negative integer N as lowercase hex, zero-padded to WIDTH digits.
  Exact up to 2^53."
  [n width]
  (let [s (.toString n 16)]
    (str (apply str (repeat (- width (count s)) "0")) s)))

(defn hex->int
  "Hex string → non-negative integer (exact up to 2^53)."
  [hex]
  (js/parseInt hex 16))

(defn seq<
  "Compare two mailbox sequence tokens (decimal strings of arbitrary size)
  as integers: true when A < B. Uses BigInt, never a double."
  [a b]
  (< (js/BigInt (str a)) (js/BigInt (str b))))

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-cljs.git