This repository has been archived by the owner on May 9, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
core.cljc
274 lines (212 loc) · 6.92 KB
/
core.cljc
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
(ns multihash.core
"Core multihash type definition and helper methods."
(:require
[alphabase.base58 :as b58]
[alphabase.bytes :as bytes]
[alphabase.hex :as hex])
#?(:clj (:import
(clojure.lang ILookup IMeta IObj)
java.io.InputStream)))
;; ## Hash Function Algorithms
(def ^:const algorithm-codes
"Map of information about the available content hashing algorithms."
{:sha1 0x11
:sha2-256 0x12
:sha2-512 0x13
:sha3 0x14
:blake2b 0x40
:blake2s 0x41})
(defn app-code?
"True if the given code number is assigned to the application-specfic range.
Returns nil if the argument is not an integer."
[code]
(when (integer? code)
(< 0 code 0x10)))
(defn get-algorithm
"Looks up an algorithm by keyword name or code number. Returns `nil` if the
value does not map to any valid algorithm."
[value]
(cond
(keyword? value)
(when-let [code (get algorithm-codes value)]
{:code code, :name value})
(not (integer? value))
nil
(app-code? value)
{:code value, :name (keyword (str "app-" value))}
:else
(some #(when (= value (val %))
{:code value, :name (key %)})
algorithm-codes)))
;; ## Multihash Type
;; Multihash identifiers have two properties:
;;
;; - `code` is a numeric code for an algorithm entry in `algorithm-codes` or an
;; application-specific algorithm code.
;; - `hex-digest` is a string holding the hex-encoded algorithm output.
;;
;; Multihash values also support metadata.
(deftype Multihash
[^long code ^String hex-digest _meta]
Object
(toString
[this]
(str "hash:" (name (:name (get-algorithm code))) \: hex-digest))
#?(:clj java.io.Serializable)
#?(:cljs IEquiv)
(#?(:clj equals, :cljs -equiv)
[this that]
(cond
(identical? this that) true
(instance? Multihash that)
(and (= code (:code that))
(= hex-digest (:hex-digest that)))
:else false))
#?(:cljs IHash)
(#?(:clj hashCode, :cljs -hash)
[this]
(hash-combine code hex-digest))
#?(:clj Comparable, :cljs IComparable)
(#?(:clj compareTo, :cljs -compare)
[this that]
(cond
(= this that) 0
(< code (:code that)) -1
(> code (:code that)) 1
:else (compare hex-digest (:hex-digest that))))
ILookup
(#?(:clj valAt, :cljs -lookup)
[this k]
(#?(:clj .valAt, :cljs -lookup) this k nil))
(#?(:clj valAt, :cljs -lookup)
[this k not-found]
(case k
:code code
:algorithm (:name (get-algorithm code))
:length (/ (count hex-digest) 2)
:digest (hex/decode hex-digest)
:hex-digest hex-digest
not-found))
IMeta
(#?(:clj meta, :cljs -meta)
[this]
_meta)
#?(:clj IObj, :cljs IWithMeta)
(#?(:clj withMeta, :cljs -with-meta)
[this meta-map]
(Multihash. code hex-digest meta-map)))
(defn create
"Constructs a new Multihash identifier. Accepts either a numeric algorithm
code or a keyword name as the first argument. The digest may either by a byte
array or a hex string."
[algorithm digest]
(let [algo (get-algorithm algorithm)]
(when-not (integer? (:code algo))
(throw (ex-info
(str "Argument " (pr-str algorithm) " does not "
"represent a valid hash algorithm.")
{:algorithm algorithm})))
(let [hex-digest (if (string? digest) digest (hex/encode digest))
byte-len (/ (count hex-digest) 2)]
(when (< 127 byte-len)
(throw (ex-info (str "Digest length must be less than 128 bytes: "
byte-len)
{:length byte-len})))
(when-let [err (hex/validate hex-digest)]
(throw (ex-info err {:hex-digest hex-digest})))
(->Multihash (:code algo) hex-digest nil))))
;; ## Encoding and Decoding
(defn encode
"Encodes a multihash into a binary representation."
^bytes
[mhash]
(let [length (:length mhash)
encoded (bytes/byte-array (+ length 2))]
(bytes/set-byte encoded 0 (:code mhash))
(bytes/set-byte encoded 1 length)
(bytes/copy (:digest mhash) 0 encoded 2 length)
encoded))
(defn hex
"Encodes a multihash into a hexadecimal string."
^String
[mhash]
(when mhash
(hex/encode (encode mhash))))
(defn base58
"Encodes a multihash into a Base-58 string."
^String
[mhash]
(when mhash
(b58/encode (encode mhash))))
(defn decode-array
"Decodes a byte array directly into multihash. Throws `ex-info` with a `:type`
of `:multihash/bad-input` if the data is malformed or invalid."
[^bytes encoded]
(let [encoded-size (alength encoded)
min-size 3]
(when (< encoded-size min-size)
(throw (ex-info
(str "Cannot read multihash from byte array: " encoded-size
" is less than the minimum of " min-size)
{:type :multihash/bad-input}))))
(let [code (bytes/get-byte encoded 0)
length (bytes/get-byte encoded 1)
payload (- (alength encoded) 2)]
(when-not (pos? length)
(throw (ex-info
(str "Encoded length " length " is invalid")
{:type :multihash/bad-input})))
(when (< payload length)
(throw (ex-info
(str "Encoded digest length " length " exceeds actual "
"remaining payload of " payload " bytes")
{:type :multihash/bad-input})))
(let [digest (bytes/byte-array length)]
(bytes/copy encoded 2 digest 0 length)
(create code digest))))
#?(:clj
(defn- read-stream-digest
"Reads a byte digest array from an input stream. First reads a byte giving
the length of the digest data to read. Throws an ex-info if the length is
invalid or there is an error reading from the stream."
^bytes
[^InputStream input]
(let [length (.read input)]
(when-not (pos? length)
(throw (ex-info
(format "Byte %02x is not a valid digest length." length)
{:type :multihash/bad-input})))
(let [digest (byte-array length)]
(loop [offset 0
remaining length]
(let [n (.read input digest offset remaining)]
(if (< n remaining)
(recur (+ offset n) (- remaining n))
digest)))))))
(defprotocol Decodable
"This protocol provides a method for data sources which a multihash can be
read from."
(decode
[source]
"Attempts to read a multihash value from the data source."))
(extend-protocol Decodable
#?(:clj (class (byte-array 0))
:cljs js/Uint8Array)
(decode
[source]
(decode-array source))
#?(:clj java.lang.String
:cljs string)
(decode
[source]
(decode-array
(if (hex/valid? (str source))
(hex/decode (str source))
(b58/decode (str source)))))
#?@(:clj
[InputStream
(decode
[source]
(let [code (.read source)
digest (read-stream-digest source)]
(create code digest)))]))