| Introduction |
| ------------------------------------------------------------ |
| |
| clojure-tlv is a type-length-value format implementation in |
| Clojure. |
| |
| |
| Installation |
| ------------------------------------------------------------ |
| |
| This library can be installed from Clojars[1]. |
| |
| Leiningen/Boot |
| -------------- |
| |
| [zcfux/clojure-tlv "0.1.0-SNAPSHOT"] |
| |
| |
| Package format |
| ------------------------------------------------------------ |
| |
| A package consists of the following fields: |
| |
| |
| Tag |
| --- |
| |
| The first byte of a package is called tag. The two leftmost |
| bits define the size of the length field. |
| |
| |
| * 00: the package neither provides a length nor a payload field |
| * 01: the length is stored in a single byte |
| * 10: the length is stored in a word |
| * 11: the length is stored in a double word |
| |
| The package type is stored in the other six bits of the tag. |
| |
| |
| Length |
| ------ |
| |
| This field indicates the payload length. It's stored in big |
| endian format. |
| |
| |
| Payload |
| ------- |
| |
| Variable-sized series of bytes. |
| |
| |
| Encoding |
| ------------------------------------------------------------ |
| |
| A package can be created with the encode function. It |
| expects a package type and a sequence. |
| |
| (require '[clojure-tlv.core :as tlv]) |
| |
| (tlv/encode 23 "hello world") |
| => (87 11 104 101 108 108 111 32 119 111 114 108 100) |
| |
| |
| Decoding |
| ------------------------------------------------------------ |
| |
| To decode packages clojure-tlv provides a pure functional |
| decoder. A callback function is applied to each found |
| package. |
| |
| (defn ->string |
| [p] |
| (String. (byte-array p))) |
| |
| (defn print-package |
| [t p] |
| (println (format |
| "type: %d, payload: %s" |
| t |
| (->string p)))) |
| |
| (-> (tlv/decoder print-package) |
| (tlv/decode (tlv/encode 23 "hello world"))) |
| |
| |
| Mapping types to keywords |
| ------------------------- |
| |
| Package types can be mapped to keywords automatically. |
| |
| (defmulti process-package (fn [t p] t)) |
| |
| (defmethod process-package :foo |
| [t p] |
| (println "I'm a foo => " (->string p))) |
| |
| (defmethod process-package :bar |
| [t p] |
| (println "I'm a bar => " (->string p))) |
| |
| (-> (tlv/decoder process-package :type-map {23 :foo |
| 42 :bar}) |
| (tlv/decode (tlv/encode 23 "foo")) |
| (tlv/decode (tlv/encode 42 "bar"))) |
| |
| |
| Session state |
| ------------- |
| |
| It's also possible to implement a stateful decoder by |
| setting an initial session state. The state is passed to the |
| decoder's callback function as third argument and set to |
| the return value. |
| |
| (assert (zero? (-> (tlv/decoder (fn [t p s] |
| (inc s)) |
| :session-state -1) |
| (tlv/decode (tlv/encode 5 "hello world")) |
| :session-state))) |
| |
| |
| Message size limit |
| ------------------ |
| |
| A payload size limit can be set when defining a decoder. If |
| a message exceeds the specified limit the decoder becomes |
| invalid. Any new data will be ignored. |
| |
| (let [decoder (-> (tlv/decoder (fn [t p s] |
| (inc s)) |
| :session-state -1 |
| :max-size 1) |
| (tlv/decode (tlv/encode 1 "a")) |
| (tlv/decode (tlv/encode 1 "bc")))] |
| (assert (zero? (:session-state decoder))) |
| (assert (tlv/failed? decoder)) |
| (assert (not (tlv/valid? decoder)))) |
| |
| |
| Nested packages |
| --------------- |
| |
| clojure-tlv offers a function to unpack nested packages. |
| |
| (defn unpack-container |
| [t p] |
| (when-let [children (tlv/unpack p)] |
| (println (clojure.string/join " " |
| (map #(->string (second %)) |
| children))))) |
| |
| (-> (tlv/decoder unpack-container) |
| (tlv/decode (tlv/encode 1 (concat (tlv/encode 2 "klaatu") |
| (tlv/encode 2 "barada") |
| (tlv/encode 2 "nikto"))))) |
| |
| |
| Async support |
| ------------- |
| |
| clojure-tlv provides a simple core.async wrapper. |
| |
| (require '[clojure-tlv.async :as async] |
| '[clojure.core.async :refer [>!! chan close!]]) |
| |
| (def c (-> (tlv/decoder print-package) |
| async/decoder->chan)) |
| |
| (>!! c (concat (tlv/encode 1 "foo") |
| (tlv/encode 2 "bar"))) |
| |
| Alternatively you can use the convenient decode-async macro. |
| |
| (def c (chan)) |
| |
| (async/decode-async c |
| ; decoder options |
| {:session-state 1 |
| :max-size 256} |
| |
| ; package received |
| ([t p s] |
| (println (format "package %d => %s" |
| s |
| (String. (byte-array p)))) |
| (inc s)) |
| |
| ; error (channel is not closed automatically) |
| ([e] |
| (println "error => " e) |
| (close! c))) |
| |
| (>!! c (tlv/encode 1 "foobar")) |
| |
| |
| References |
| ------------------------------------------------------------ |
| [1]: Clojars |
| |
| |
| Links |
| ------------------------------------------------------------ |
| master.zip |
| GitHub |