-
Notifications
You must be signed in to change notification settings - Fork 7
/
core.clj
287 lines (227 loc) · 8.66 KB
/
core.clj
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
275
276
277
278
279
280
281
282
283
284
285
286
287
(ns clj-cbor.core
"Core CBOR library API."
(:refer-clojure :exclude [spit slurp])
(:require
[clj-cbor.codec :as codec]
[clj-cbor.error :as error]
[clj-cbor.tags.clojure :as tags.clj]
[clj-cbor.tags.content :as tags.content]
[clj-cbor.tags.numbers :as tags.num]
[clj-cbor.tags.text :as tags.text]
[clj-cbor.tags.time :as tags.time]
[clojure.java.io :as io])
(:import
(java.io
ByteArrayOutputStream
DataInputStream
DataOutputStream
EOFException
InputStream
OutputStream)))
;; ## Codec Construction
(defn cbor-codec
"Construct a new CBOR codec with no configuration. Note that this does not
include **any** read and write handlers. See the `default-codec` and the
`default-read-handlers` and `default-write-handlers` vars.
Arguments may be a map or a sequence of key/value pairs. Valid options are:
- `:dispatch` function which is called to provide a dispatch value based on
the data to be encoded. (default: `class`)
- `:write-handlers` lookup function from dispatch values to handlers which
take some data to be encoded and return a transformed version of it
(typically a tagged value).
- `:read-handlers` lookup function from integer tags to handlers which take
the embedded item and return the parsed data value."
[& opts]
(merge
(codec/blank-codec)
(if (and (= 1 (count opts)) (map? (first opts)))
(first opts)
(apply hash-map opts))))
(def default-write-handlers
"Map of default write handlers to use, keyed by class.
The default choice of encoding for instants in time is the numeric epoch
representation (tag 1)."
(merge tags.clj/clojure-write-handlers
tags.content/content-write-handlers
tags.num/number-write-handlers
tags.time/epoch-time-write-handlers
tags.time/epoch-date-write-handlers
tags.text/text-write-handlers))
(def default-read-handlers
"Map of default tag handlers to use, keyed by tag.
The default choice of representation for instants in time is
`java.time.Instant`."
(merge tags.clj/clojure-read-handlers
tags.content/content-read-handlers
tags.num/number-read-handlers
tags.time/instant-read-handlers
tags.time/local-date-read-handlers
tags.text/text-read-handlers))
(def default-codec
"Default CBOR codec to use when none is specified."
(cbor-codec
:write-handlers default-write-handlers
:read-handlers default-read-handlers))
(defn dispatch-superclasses
"Construct a codec dispatch function which will return the named classes
whenever one of their instances is encountered.
This lets you use a single superclass to match all of its subclasses. The
classes are tested in the order given; if none match, this returns the
value's own class."
[& classes]
(let [cache (atom {})]
(fn dispatch
[x]
(or (get @cache (class x))
(let [result (loop [classes classes]
(if (seq classes)
(let [cls (first classes)]
(if (instance? cls x)
cls
(recur (rest classes))))
(class x)))]
(swap! cache assoc (class x) result)
result)))))
;; ## Encoding Functions
(defn- data-output-stream
"Coerce the argument to a `DataOutputStream`."
^DataOutputStream
[output]
(condp instance? output
DataOutputStream
output
OutputStream
(DataOutputStream. output)
(throw (IllegalArgumentException.
(str "Cannot coerce argument to an OutputStream: "
(pr-str output))))))
(defn encode
"Encode a single value as CBOR data.
Writes the value bytes to the provided output stream, or returns the value
as a byte array if no output is given. The `default-codec` is used to encode
the value if none is provided."
([value]
(encode default-codec value))
([encoder value]
(let [buffer (ByteArrayOutputStream.)]
(with-open [output (data-output-stream buffer)]
(encode encoder output value))
(.toByteArray buffer)))
([encoder output value]
(let [data-output (data-output-stream output)]
(codec/write-value encoder data-output value))))
(defn encode-seq
"Encode a sequence of values as CBOR data. This eagerly consumes the
input sequence.
Writes the value bytes to the provided output stream, or returns the value
as a byte array if no output is given. The `default-codec` is used to encode
the value if none is provided."
([values]
(encode-seq default-codec values))
([encoder values]
(let [buffer (ByteArrayOutputStream.)]
(with-open [output (data-output-stream buffer)]
(encode-seq encoder output values))
(.toByteArray buffer)))
([encoder output values]
(let [data-output (data-output-stream output)]
(transduce (map (partial encode encoder data-output)) + 0 values))))
;; ## Decoding Functions
(defn- data-input-stream
"Coerce the argument to a `DataInputStream`."
[input]
(condp instance? input
DataInputStream
input
InputStream
(DataInputStream. input)
(DataInputStream. (io/input-stream input))))
(defn- maybe-read-header
"Attempts to read a header byte from the input stream. If there is no more
input, the `guard` value is returned."
[^DataInputStream input guard]
(try
(.readUnsignedByte input)
(catch EOFException _
guard)))
(defn- try-read-value
"Attemtps to read the rest of a CBOR value from the input stream. If the
input ends during the read, the error handler is called with an
`end-of-input` error."
[decoder input header]
(try
(codec/read-value* decoder input header)
(catch EOFException _
(error/*handler*
:clj-cbor.codec/end-of-input
"Input data ended while parsing a CBOR value."
{:header header}))))
(defn decode
"Decode a single CBOR value from the input.
This uses the given codec or the `default-codec` if none is provided. If at
the end of the input, this returns `eof-guard` or nil.
The input must be an input stream or something coercible to one like a file
or byte array. Note that coercion will produce a `BufferedInputStream` if the
argument is not already a stream, so repeated reads will probably not behave
as expected! If you need incremental parsing, make sure you pass in something
that is already an `InputStream`."
([input]
(decode default-codec input))
([decoder input]
(decode decoder input nil))
([decoder input eof-guard]
(let [input (data-input-stream input)
header (maybe-read-header input eof-guard)]
(if (identical? header eof-guard)
eof-guard
(try-read-value decoder input header)))))
(defn decode-seq
"Decode a sequence of CBOR values from the input.
This uses the given codec or the `default-codec` if none is provided. The
returned sequence is lazy, so take care that the input stream is not closed
before the entries are realized.
The input must be an input stream or something coercible to one - see
`decode` for usage notes."
([input]
(decode-seq default-codec input))
([decoder input]
(let [eof-guard (Object.)
data-input (data-input-stream input)
read-data! #(decode decoder data-input eof-guard)]
(take-while
#(not (identical? eof-guard %))
(repeatedly read-data!)))))
;; ## Utility Functions
(defn spit
"Opens an output stream to `f`, writes `value` to it, then closes the stream.
Options may include `:append` to write to the end of the file instead of
truncating."
[f value & opts]
(with-open [out ^OutputStream (apply io/output-stream f opts)]
(encode default-codec out value)))
(defn spit-all
"Opens an output stream to `f`, writes each element in `values` to it, then
closes the stream.
Options may include `:append` to write to the end of the file instead of
truncating."
[f values & opts]
(with-open [out ^OutputStream (apply io/output-stream f opts)]
(encode-seq default-codec out values)))
(defn slurp
"Opens an input stream from `f`, reads the first value from it, then closes
the stream."
[f & opts]
(with-open [in ^InputStream (apply io/input-stream f opts)]
(decode default-codec in)))
(defn slurp-all
"Opens an input stream from `f`, reads all values from it, then closes the
stream."
[f & opts]
(with-open [in ^InputStream (apply io/input-stream f opts)]
(doall (decode-seq default-codec in))))
(defn self-describe
"Wraps a value with a self-describing CBOR tag. This will cause the first few
bytes of the data to be `D9D9F7`, which serves as a distinguishing header for
format detection."
[value]
(tags.content/format-self-described value))