From 7cd5f83dce243067da50b31c354e7aeb7577c744 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 16:49:15 +0700 Subject: [PATCH 01/19] Bump deps --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 6473e1d..03daf3e 100644 --- a/project.clj +++ b/project.clj @@ -11,7 +11,7 @@ *assert* true} :dependencies [[org.clojure/clojure "1.4.0"] - [org.clojure/tools.reader "0.8.3"] + [org.clojure/tools.reader "0.8.4"] [com.taoensso/encore "1.3.1"] [org.iq80.snappy/snappy "0.3"] [org.tukaani/xz "1.5"]] From a6aba2c92a97defa8b6aa6914c52d79df2748361 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 14:20:38 +0700 Subject: [PATCH 02/19] Add experimental LZ4 compressors --- project.clj | 3 +- src/taoensso/nippy/compression.clj | 68 ++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/project.clj b/project.clj index 03daf3e..fb20648 100644 --- a/project.clj +++ b/project.clj @@ -14,7 +14,8 @@ [org.clojure/tools.reader "0.8.4"] [com.taoensso/encore "1.3.1"] [org.iq80.snappy/snappy "0.3"] - [org.tukaani/xz "1.5"]] + [org.tukaani/xz "1.5"] + [net.jpountz.lz4/lz4 "1.2.0"]] :test-paths ["test" "src"] :profiles diff --git a/src/taoensso/nippy/compression.clj b/src/taoensso/nippy/compression.clj index 3cc5f87..41e49b4 100644 --- a/src/taoensso/nippy/compression.clj +++ b/src/taoensso/nippy/compression.clj @@ -1,6 +1,6 @@ -(ns taoensso.nippy.compression - "Alpha - subject to change." +(ns taoensso.nippy.compression "Alpha - subject to change." {:author "Peter Taoussanis"} + (:require [taoensso.encore :as encore]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream DataInputStream DataOutputStream])) @@ -23,7 +23,7 @@ Write speed: very high. Read speed: very high. - A good general-purpose compressor for Redis." + A good general-purpose compressor." (->SnappyCompressor)) (deftype LZMA2Compressor [compression-level] @@ -57,5 +57,65 @@ Write speed: _very_ slow (also currently single-threaded). Read speed: slow. - A specialized compressor for large, low-write data." + A specialized compressor for large, low-write data in space-sensitive + environments." (->LZMA2Compressor 0)) + +(deftype LZ4Compressor [^net.jpountz.lz4.LZ4Compressor compressor + ^net.jpountz.lz4.LZ4SafeDecompressor decompressor] + ICompressor + (compress [_ ba] + (let [in-len (alength ^bytes ba) + max-out-len (.maxCompressedLength compressor in-len) + ba-out* (byte-array max-out-len) + out-len (.compress compressor ba 0 in-len ba-out* 0 max-out-len) + ba-out (java.util.Arrays/copyOf ba-out* out-len)] + ba-out)) + + (decompress [_ ba] + (let [in-len (alength ^bytes ba) + max-out-len in-len + ba-out* (byte-array (* max-out-len 3.0)) ; Nb over-sized! + out-len (.decompress decompressor ba 0 in-len ba-out* 0) + ba-out (java.util.Arrays/copyOf ba-out* out-len)] + ba-out))) + +(def ^:private ^net.jpountz.lz4.LZ4Factory lz4-factory + (net.jpountz.lz4.LZ4Factory/fastestInstance)) + +(def lz4-compressor + "Default net.jpountz.lz4 compressor: + Ratio: low. + Write speed: very high. + Read speed: very high. + + A good general-purpose compressor, competitive with Snappy." + (->LZ4Compressor (.fastCompressor lz4-factory) + (.safeDecompressor lz4-factory))) + +(def lz4hc-compressor "Like `lz4-compressor` but trades some speed for ratio." + (->LZ4Compressor (.highCompressor lz4-factory) + (.safeDecompressor lz4-factory))) + +(comment + (def ba-bench (.getBytes (apply str (repeatedly 1000 rand)) "UTF-8")) + (defn bench1 [compressor] + {:time (encore/bench 10000 {:nlaps-warmup 10000} + (->> ba-bench (compress compressor) (decompress compressor))) + :ratio (encore/round2 (/ (count (compress compressor ba-bench)) + (count ba-bench)))}) + + (println + {:snappy (bench1 snappy-compressor) + ;; :lzma (bench1 lzma2-compressor) ; Slow! + :lz4 (bench1 lz4-compressor) + :lz4hc (bench1 lz4hc-compressor)}) + + ;;; 2014 April 5, initial benchmarks + {:snappy {:time 2214 :ratio 0.848} + :lzma {:time 46684 :ratio 0.494} + :lz4 {:time 1327 :ratio 0.819} ; w/o uncompressed size prefix + :lz4hc {:time 5762 :ratio 0.763} ; '' + ;; :lz4 {:time 1404 :ratio 0.819} ; with uncompressed size prefix + ;; :lz4hc {:time 6028 :ratio 0.763} ; '' + }) From 5b9358acf91d5aac9eaf2a389c972f258db71d5d Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 16:18:51 +0700 Subject: [PATCH 03/19] LZ4: use fast decompressor --- src/taoensso/nippy/compression.clj | 52 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/taoensso/nippy/compression.clj b/src/taoensso/nippy/compression.clj index 41e49b4..6564385 100644 --- a/src/taoensso/nippy/compression.clj +++ b/src/taoensso/nippy/compression.clj @@ -61,24 +61,32 @@ environments." (->LZMA2Compressor 0)) -(deftype LZ4Compressor [^net.jpountz.lz4.LZ4Compressor compressor - ^net.jpountz.lz4.LZ4SafeDecompressor decompressor] +(deftype LZ4Compressor [^net.jpountz.lz4.LZ4Compressor compressor + ^net.jpountz.lz4.LZ4Decompressor decompressor] ICompressor (compress [_ ba] - (let [in-len (alength ^bytes ba) - max-out-len (.maxCompressedLength compressor in-len) - ba-out* (byte-array max-out-len) - out-len (.compress compressor ba 0 in-len ba-out* 0 max-out-len) - ba-out (java.util.Arrays/copyOf ba-out* out-len)] - ba-out)) + (let [len-decomp (alength ^bytes ba) + max-len-comp (.maxCompressedLength compressor len-decomp) + ba-comp* (byte-array max-len-comp) ; Over-sized + len-comp (.compress compressor ba 0 len-decomp ba-comp* 0 max-len-comp) + ;; + baos (ByteArrayOutputStream. (+ len-comp 4)) + dos (DataOutputStream. baos)] + (.writeInt dos len-decomp) ; Prefix with uncompressed length + (.write dos ba-comp* 0 len-comp) + (.toByteArray baos))) (decompress [_ ba] - (let [in-len (alength ^bytes ba) - max-out-len in-len - ba-out* (byte-array (* max-out-len 3.0)) ; Nb over-sized! - out-len (.decompress decompressor ba 0 in-len ba-out* 0) - ba-out (java.util.Arrays/copyOf ba-out* out-len)] - ba-out))) + (let [bais (ByteArrayInputStream. ba) + dis (DataInputStream. bais) + ;; + len-decomp (.readInt dis) + len-comp (- (alength ^bytes ba) 4) + ba-comp (byte-array len-comp) + _ (.readFully dis ba-comp 0 len-comp) + ba-decomp (byte-array len-decomp) + _ (.decompress decompressor ba-comp 0 ba-decomp 0 len-decomp)] + ba-decomp))) (def ^:private ^net.jpountz.lz4.LZ4Factory lz4-factory (net.jpountz.lz4.LZ4Factory/fastestInstance)) @@ -89,13 +97,16 @@ Write speed: very high. Read speed: very high. - A good general-purpose compressor, competitive with Snappy." + A good general-purpose compressor, competitive with Snappy. + + Thanks to Max Penet (@mpenet) for our first implementation, + Ref. https://github.com/mpenet/nippy-lz4" (->LZ4Compressor (.fastCompressor lz4-factory) - (.safeDecompressor lz4-factory))) + (.fastDecompressor lz4-factory))) (def lz4hc-compressor "Like `lz4-compressor` but trades some speed for ratio." (->LZ4Compressor (.highCompressor lz4-factory) - (.safeDecompressor lz4-factory))) + (.fastDecompressor lz4-factory))) (comment (def ba-bench (.getBytes (apply str (repeatedly 1000 rand)) "UTF-8")) @@ -114,8 +125,5 @@ ;;; 2014 April 5, initial benchmarks {:snappy {:time 2214 :ratio 0.848} :lzma {:time 46684 :ratio 0.494} - :lz4 {:time 1327 :ratio 0.819} ; w/o uncompressed size prefix - :lz4hc {:time 5762 :ratio 0.763} ; '' - ;; :lz4 {:time 1404 :ratio 0.819} ; with uncompressed size prefix - ;; :lz4hc {:time 6028 :ratio 0.763} ; '' - }) + :lz4 {:time 1363 :ratio 0.819} + :lz4hc {:time 6045 :ratio 0.763}}) From b7a454a9c82da2d053c4c793ab779586935e75f3 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 16:56:16 +0700 Subject: [PATCH 04/19] EXPERIMENTAL: Make LZ4 the default Nippy compressor (back-compatible for header'ed data) --- src/taoensso/nippy.clj | 62 +++++++++++++++++------------- src/taoensso/nippy/benchmarks.clj | 13 ++++++- src/taoensso/nippy/compression.clj | 3 +- test/taoensso/nippy/tests/main.clj | 6 ++- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/taoensso/nippy.clj b/src/taoensso/nippy.clj index 7d6c3e5..a83992d 100644 --- a/src/taoensso/nippy.clj +++ b/src/taoensso/nippy.clj @@ -6,7 +6,8 @@ [taoensso.encore :as encore] [taoensso.nippy (utils :as utils) - (compression :as compression :refer (snappy-compressor)) + (compression :as compression :refer (lz4-compressor + snappy-compressor)) (encryption :as encryption :refer (aes128-encryptor))]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream DataInputStream DataOutputStream Serializable ObjectOutputStream ObjectInputStream @@ -25,13 +26,20 @@ ;; * Nippy version check (=> supports changes to data schema over time). ;; * Encrypted &/or compressed data identification. ;; -(def ^:private ^:const head-version 1) +(def ^:private ^:const head-version 2) (def ^:private head-sig (.getBytes "NPY" "UTF-8")) (def ^:private ^:const head-meta "Final byte stores version-dependent metadata." - {(byte 0) {:version 1 :compressed? false :encrypted? false} + {;;; Nippy <= v2.6 with Snappy as default compressor + (byte 0) {:version 1 :compressed? false :encrypted? false} (byte 1) {:version 1 :compressed? true :encrypted? false} (byte 2) {:version 1 :compressed? false :encrypted? true} - (byte 3) {:version 1 :compressed? true :encrypted? true}}) + (byte 3) {:version 1 :compressed? true :encrypted? true} + + ;;; EXPERIMENTAL: Nippy >= v2.7 with LZ4 as default compressor + (byte 4) {:version 2 :compressed? false :encrypted? false} + (byte 5) {:version 2 :compressed? true :encrypted? false} + (byte 6) {:version 2 :compressed? false :encrypted? true} + (byte 7) {:version 2 :compressed? true :encrypted? true}}) (defmacro when-debug-mode [& body] (when #_true false `(do ~@body))) @@ -310,8 +318,6 @@ (comment (wrap-header (.getBytes "foo") {:compressed? true :encrypted? false})) -(declare assert-legacy-args) ; Deprecated - (defn freeze-to-out! "Low-level API. Serializes arg (any Clojure data type) to a DataOutput." [^DataOutput data-output x & _] @@ -321,12 +327,15 @@ "Serializes arg (any Clojure data type) to a byte array. For custom types extend the Clojure reader or see `extend-freeze`." ^bytes [x & [{:keys [password compressor encryptor skip-header?] - :or {compressor snappy-compressor + :or {compressor lz4-compressor encryptor aes128-encryptor} :as opts}]] - (when (:legacy-mode opts) ; Deprecated - (assert-legacy-args compressor password)) - (let [skip-header? (or skip-header? (:legacy-mode opts)) ; Deprecated + (let [;;; Legacy mode is deprecated + compressor (if-not (:legacy-mode opts) compressor snappy-compressor) + encryptor (if-not (:legacy-mode opts) encryptor nil) + ;; + skip-header? (or skip-header? (:legacy-mode opts) ; Deprecated + ) bas (ByteArrayOutputStream.) sout (DataOutputStream. bas)] (freeze-to-out! sout x) @@ -492,10 +501,10 @@ fallback (`{:headerless-meta nil}`) if you're certain you won't be thawing headerless data." [^bytes ba & [{:keys [password compressor encryptor headerless-meta] - :or {compressor snappy-compressor - encryptor aes128-encryptor + :or {compressor :auto + encryptor :auto headerless-meta ; Recommend set to nil when possible - {:version 1 + {:version 2 :compressed? true :encrypted? false}} :as opts}]] @@ -507,19 +516,26 @@ ex (fn [msg & [e]] (throw (Exception. (str "Thaw failed: " msg) e))) try-thaw-data - (fn [data-ba {:keys [compressed? encrypted?] :as _head-or-headerless-meta}] - (let [password (when encrypted? password) - compressor (when compressed? compressor)] + (fn [data-ba {:keys [compressed? encrypted? version] + :as _head-or-headerless-meta}] + (let [encryptor (when (and password encrypted?) + (if-not (identical? encryptor :auto) encryptor + aes128-encryptor)) + compressor (when compressed? + (if-not (identical? compressor :auto) compressor + (case (int version) + 1 snappy-compressor + 2 lz4-compressor)))] (try (let [ba data-ba - ba (if password (encryption/decrypt encryptor password ba) ba) - ba (if compressor (compression/decompress compressor ba) ba) + ba (if encryptor (encryption/decrypt encryptor password ba) ba) + ba (if compressor (compression/decompress compressor ba) ba) sin (DataInputStream. (ByteArrayInputStream. ba))] (thaw-from-in! sin)) (catch Exception e (cond - password (if head-meta (ex "Wrong password/encryptor?" e) + encryptor (if head-meta (ex "Wrong password/encryptor?" e) (ex "Unencrypted data?" e)) compressor (if head-meta (ex "Encrypted data or wrong compressor?" e) (ex "Uncompressed data?" e)) @@ -614,7 +630,7 @@ (let [compressed? (.readBoolean in) ba (read-bytes in)] (thaw ba {:compressor compression/lzma2-compressor - :headerless-meta {:version 1 + :headerless-meta {:version 2 :compressed? compressed? :encrypted? false}}))) @@ -739,12 +755,6 @@ (def thaw-from-stream! "DEPRECATED: Use `thaw-from-in!` instead." thaw-from-in!) -(defn- assert-legacy-args [compressor password] - (when password - (throw (AssertionError. "Encryption not supported in legacy mode."))) - (when (and compressor (not= compressor snappy-compressor)) - (throw (AssertionError. "Only Snappy compressor supported in legacy mode.")))) - (defn freeze-to-bytes "DEPRECATED: Use `freeze` instead." ^bytes [x & {:keys [compress?] :or {compress? true}}] diff --git a/src/taoensso/nippy/benchmarks.clj b/src/taoensso/nippy/benchmarks.clj index 03daf53..4ff2f47 100644 --- a/src/taoensso/nippy/benchmarks.clj +++ b/src/taoensso/nippy/benchmarks.clj @@ -43,8 +43,12 @@ (println {:default (bench1 #(freeze % {}) #(thaw % {}))}) - (println {:fast (bench1 #(freeze % {:compressor nil}) - #(thaw % {:compressor nil}))}) + (println {:fast (bench1 #(freeze % {:compressor nil + :skip-header? true}) + #(thaw % {:headerless-meta + {:version 2 + :compressed? false + :encrypted? false}}))}) (println {:encrypted (bench1 #(freeze % {:password [:cached "p"]}) #(thaw % {:password [:cached "p"]}))}) @@ -65,6 +69,11 @@ ;; (bench {:reader? true :lzma2? true :fressian? true :laps 1}) ;; (bench {:laps 2}) + ;;; 2014 Apr 5 w/ headerless :fast, LZ4 replacing Snappy as default compressor + ;; {:default {:round 7669, :freeze 4157, :thaw 3512, :size 16143}} + ;; {:fast {:round 6918, :freeze 3636, :thaw 3282, :size 16992}} + ;; {:encrypted {:round 11814, :freeze 6180, :thaw 5634, :size 16164}} + ;;; 2014 Jan 22: with common-type size optimizations, enlarged stress-data ;; {:reader {:round 109544, :freeze 39523, :thaw 70021, :size 27681}} ;; {:default {:round 9234, :freeze 5128, :thaw 4106, :size 15989}} diff --git a/src/taoensso/nippy/compression.clj b/src/taoensso/nippy/compression.clj index 6564385..85b7864 100644 --- a/src/taoensso/nippy/compression.clj +++ b/src/taoensso/nippy/compression.clj @@ -104,7 +104,8 @@ (->LZ4Compressor (.fastCompressor lz4-factory) (.fastDecompressor lz4-factory))) -(def lz4hc-compressor "Like `lz4-compressor` but trades some speed for ratio." +(def lz4hc-compressor + "Like `lz4-compressor` but trades some write speed for ratio." (->LZ4Compressor (.highCompressor lz4-factory) (.fastDecompressor lz4-factory))) diff --git a/test/taoensso/nippy/tests/main.clj b/test/taoensso/nippy/tests/main.clj index 6da3f3b..2a80fd4 100644 --- a/test/taoensso/nippy/tests/main.clj +++ b/test/taoensso/nippy/tests/main.clj @@ -16,7 +16,11 @@ ;;;; Core (expect test-data ((comp thaw freeze) test-data)) -(expect test-data ((comp thaw #(freeze % {:legacy-mode true})) test-data)) +(expect test-data ((comp #(thaw % {:headerless-meta {:version 1 + :compressed? true + :encrypted? false}}) + #(freeze % {:legacy-mode true})) + test-data)) (expect test-data ((comp #(thaw % {:password [:salted "p"]}) #(freeze % {:password [:salted "p"]})) test-data)) From 20b1c2b1d2aba45d936e38e021eedefe984cb42b Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 18:30:28 +0700 Subject: [PATCH 05/19] Encode compression type in Nippy header, major refactor/housekeeping Housekeeping includes: * Importing useful encryption+compression stuff into primary ns for lib consumers. * Promoting a number of things from Alpha status. * Exceptions are now all `ex-info`s. * Simplification of `thaw` API: Nippy v1 support is now automatic & configuration-free (performance impact in most cases is negligible). --- README.md | 6 +- src/taoensso/nippy.clj | 338 +++++++++++++++-------------- src/taoensso/nippy/benchmarks.clj | 107 ++------- src/taoensso/nippy/compression.clj | 67 +++--- src/taoensso/nippy/encryption.clj | 51 ++--- src/taoensso/nippy/tools.clj | 23 +- test/taoensso/nippy/tests/main.clj | 18 +- 7 files changed, 275 insertions(+), 335 deletions(-) diff --git a/README.md b/README.md index 40c06d6..24227c8 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,9 @@ Couldn't be simpler! See also the lower-level `freeze-to-out!` and `thaw-from-in!` fns for operating on `DataOutput` and `DataInput` types directly. -### Encryption (currently in **ALPHA**) +### Encryption (v2+) -Nippy v2+ also gives you **dead simple data encryption**. Add a single option to your usual freeze/thaw calls like so: +Nippy also gives you **dead simple data encryption**. Add a single option to your usual freeze/thaw calls like so: ```clojure (nippy/freeze nippy/stress-data {:password [:salted "my-password"]}) ; Encrypt @@ -126,7 +126,7 @@ Nippy v2+ also gives you **dead simple data encryption**. Add a single option to There's two default forms of encryption on offer: `:salted` and `:cached`. Each of these makes carefully-chosen trade-offs and is suited to one of two common use cases. See the `aes128-encryptor` [docstring](http://ptaoussanis.github.io/nippy/taoensso.nippy.encryption.html) for a detailed explanation of why/when you'd want one or the other. -### Custom types (v2.1+, ALPHA - subject to change) +### Custom types (v2.1+) ```clojure (defrecord MyType [data]) diff --git a/src/taoensso/nippy.clj b/src/taoensso/nippy.clj index a83992d..8ee57c3 100644 --- a/src/taoensso/nippy.clj +++ b/src/taoensso/nippy.clj @@ -6,9 +6,8 @@ [taoensso.encore :as encore] [taoensso.nippy (utils :as utils) - (compression :as compression :refer (lz4-compressor - snappy-compressor)) - (encryption :as encryption :refer (aes128-encryptor))]) + (compression :as compression) + (encryption :as encryption)]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream DataInputStream DataOutputStream Serializable ObjectOutputStream ObjectInputStream DataOutput DataInput] @@ -21,25 +20,32 @@ IRecord ISeq])) ;;;; Nippy 2.x+ header spec (4 bytes) -;; Header is optional but recommended + enabled by default. Uses: +;; Header is optional but recommended + enabled by default. Purpose: ;; * Sanity check (data appears to be Nippy data). ;; * Nippy version check (=> supports changes to data schema over time). -;; * Encrypted &/or compressed data identification. +;; * Supports :auto thaw compressor, encryptor. ;; -(def ^:private ^:const head-version 2) +(def ^:private ^:const head-version 1) (def ^:private head-sig (.getBytes "NPY" "UTF-8")) (def ^:private ^:const head-meta "Final byte stores version-dependent metadata." - {;;; Nippy <= v2.6 with Snappy as default compressor - (byte 0) {:version 1 :compressed? false :encrypted? false} - (byte 1) {:version 1 :compressed? true :encrypted? false} - (byte 2) {:version 1 :compressed? false :encrypted? true} - (byte 3) {:version 1 :compressed? true :encrypted? true} - - ;;; EXPERIMENTAL: Nippy >= v2.7 with LZ4 as default compressor - (byte 4) {:version 2 :compressed? false :encrypted? false} - (byte 5) {:version 2 :compressed? true :encrypted? false} - (byte 6) {:version 2 :compressed? false :encrypted? true} - (byte 7) {:version 2 :compressed? true :encrypted? true}}) + {(byte 0) {:version 1 :compressor-id nil :encryptor-id nil} + (byte 4) {:version 1 :compressor-id nil :encryptor-id :else} + (byte 5) {:version 1 :compressor-id :else :encryptor-id nil} + (byte 6) {:version 1 :compressor-id :else :encryptor-id :else} + ;; + (byte 2) {:version 1 :compressor-id nil :encryptor-id :aes128-sha512} + ;; + (byte 1) {:version 1 :compressor-id :snappy :encryptor-id nil} + (byte 3) {:version 1 :compressor-id :snappy :encryptor-id :aes128-sha512} + (byte 7) {:version 1 :compressor-id :snappy :encryptor-id :else} + ;; + (byte 8) {:version 1 :compressor-id :lz4 :encryptor-id nil} + (byte 9) {:version 1 :compressor-id :lz4 :encryptor-id :aes128-sha512} + (byte 10) {:version 1 :compressor-id :lz4 :encryptor-id :else} + ;; + (byte 11) {:version 1 :compressor-id :lzma2 :encryptor-id nil} + (byte 12) {:version 1 :compressor-id :lzma2 :encryptor-id :aes128-sha512} + (byte 13) {:version 1 :compressor-id :lzma2 :encryptor-id :else}}) (defmacro when-debug-mode [& body] (when #_true false `(do ~@body))) @@ -86,7 +92,7 @@ (def ^:const id-ratio (int 70)) (def ^:const id-record (int 80)) - ;; (def ^:const id-type (int 81)) ; TODO + ;; (def ^:const id-type (int 81)) ; TODO? (def ^:const id-date (int 90)) (def ^:const id-uuid (int 91)) @@ -111,6 +117,21 @@ (def ^:const id-old-keyword (int 12)) ; as of 2.0.0-alpha5, for str consistecy ) +;;;; Ns imports (mostly for convenience of lib consumers) + +(encore/defalias compress compression/compress) +(encore/defalias decompress compression/decompress) +(encore/defalias snappy-compressor compression/snappy-compressor) +(encore/defalias lzma2-compressor compression/lzma2-compressor) +(encore/defalias lz4-compressor compression/lz4-compressor) +(encore/defalias lz4hc-compressor compression/lz4hc-compressor) + +(encore/defalias encrypt encryption/encrypt) +(encore/defalias decrypt encryption/decrypt) +(encore/defalias aes128-encryptor encryption/aes128-encryptor) + +(encore/defalias freezable? utils/freezable?) + ;;;; Freezing (defprotocol Freezable @@ -137,7 +158,7 @@ (let [x (with-meta x {:tag 'String})] `(write-bytes ~out (.getBytes ~x "UTF-8") ~small?))) -(defmacro write-compact-long "EXPERIMENTAL! Uses 2->9 bytes." [out x] +(defmacro write-compact-long "Uses 2->9 bytes." [out x] `(write-bytes ~out (.toByteArray (java.math.BigInteger/valueOf (long ~x))) :small)) @@ -304,19 +325,25 @@ :else ; Fallback #3: *final-freeze-fallback* (if-let [ffb *final-freeze-fallback*] (ffb x out) - (throw (Exception. (format "Unfreezable type: %s %s" - (type x) (str x)))))))) + (throw (ex-info (format "Unfreezable type: %s %s" (type x) (str x)) + {:type (type x) + :as-str (pr-str x)})))))) (def ^:private head-meta-id (reduce-kv #(assoc %1 %3 %2) {} head-meta)) +(def ^:private get-head-ba + (memoize + (fn [head-meta] + (when-let [meta-id (get head-meta-id (assoc head-meta :version head-version))] + (encore/ba-concat head-sig (byte-array [meta-id])))))) -(defn- wrap-header [data-ba metadata] - (if-let [meta-id (head-meta-id (assoc metadata :version head-version))] - (let [head-ba (encore/ba-concat head-sig (byte-array [meta-id]))] - (encore/ba-concat head-ba data-ba)) - (throw (Exception. (str "Unrecognized header metadata: " metadata))))) +(defn- wrap-header [data-ba head-meta] + (if-let [head-ba (get-head-ba head-meta)] + (encore/ba-concat head-ba data-ba) + (throw (ex-info (format "Unrecognized header meta: %s" head-meta) + {:head-meta head-meta})))) -(comment (wrap-header (.getBytes "foo") {:compressed? true - :encrypted? false})) +(comment (wrap-header (.getBytes "foo") {:compressor-id :lz4 + :encryptor-id nil})) (defn freeze-to-out! "Low-level API. Serializes arg (any Clojure data type) to a DataOutput." @@ -324,27 +351,30 @@ (freeze-to-out data-output x)) (defn freeze - "Serializes arg (any Clojure data type) to a byte array. For custom types - extend the Clojure reader or see `extend-freeze`." - ^bytes [x & [{:keys [password compressor encryptor skip-header?] + "Serializes arg (any Clojure data type) to a byte array. To freeze custom + types, extend the Clojure reader or see `extend-freeze`." + ^bytes [x & [{:keys [compressor encryptor password skip-header?] :or {compressor lz4-compressor encryptor aes128-encryptor} :as opts}]] - (let [;;; Legacy mode is deprecated - compressor (if-not (:legacy-mode opts) compressor snappy-compressor) - encryptor (if-not (:legacy-mode opts) encryptor nil) - ;; - skip-header? (or skip-header? (:legacy-mode opts) ; Deprecated - ) - bas (ByteArrayOutputStream.) - sout (DataOutputStream. bas)] - (freeze-to-out! sout x) - (let [ba (.toByteArray bas) - ba (if compressor (compression/compress compressor ba) ba) - ba (if password (encryption/encrypt encryptor password ba) ba)] + (let [legacy-mode? (:legacy-mode opts) ; DEPRECATED Nippy v1-compatible freeze + compressor (if-not legacy-mode? compressor snappy-compressor) + encryptor (when password (if-not legacy-mode? encryptor nil)) + skip-header? (or skip-header? legacy-mode?) + baos (ByteArrayOutputStream.) + dos (DataOutputStream. baos)] + (freeze-to-out! dos x) + (let [ba (.toByteArray baos) + ba (if-not compressor ba (compress compressor ba)) + ba (if-not encryptor ba (encrypt encryptor password ba))] (if skip-header? ba - (wrap-header ba {:compressed? (boolean compressor) - :encrypted? (boolean password)}))))) + (wrap-header ba + {:compressor-id (when-let [c compressor] + (or (compression/standard-header-ids + (compression/header-id c)) :else)) + :encryptor-id (when-let [e encryptor] + (or (encryption/standard-header-ids + (encryption/header-id e)) :else))}))))) ;;;; Thawing @@ -362,8 +392,7 @@ (defmacro read-utf8 [in & [small?]] `(String. (read-bytes ~in ~small?) "UTF-8")) -(defmacro read-compact-long "EXPERIMENTAL!" [in] - `(long (BigInteger. (read-bytes ~in :small)))) +(defmacro read-compact-long [in] `(long (BigInteger. (read-bytes ~in :small)))) (defmacro ^:private read-coll [in coll] `(let [in# ~in] (encore/repeatedly-into* ~coll (.readInt in#) (thaw-from-in in#)))) @@ -463,19 +492,25 @@ id-old-keyword (keyword (.readUTF in)) (if-not (neg? type-id) - (throw (Exception. (str "Unknown type ID: " type-id))) + (throw (ex-info (format "Unknown type ID: %s" type-id) + {:type-id type-id})) ;; Custom types (if-let [reader (get @custom-readers type-id)] (try (reader in) (catch Exception e - (throw (Exception. (str "Reader exception for custom type ID: " - (- type-id)) e)))) - (throw (Exception. (str "No reader provided for custom type ID: " - (- type-id))))))) + (throw (ex-info + (format "Reader exception for custom type ID: %s" + (- type-id)) + {:type-id (- type-id)} e)))) + (throw (ex-info + (format "No reader provided for custom type ID: %s" + (- type-id)) + {:type-id (- type-id)}))))) (catch Exception e - (throw (Exception. (format "Thaw failed against type-id: %s" type-id) e)))))) + (throw (ex-info (format "Thaw failed against type-id: %s" type-id) + {:type-id type-id} e)))))) (defn thaw-from-in! "Low-level API. Deserializes a frozen object from given DataInput to its @@ -486,121 +521,118 @@ (defn- try-parse-header [ba] (when-let [[head-ba data-ba] (encore/ba-split ba 4)] (let [[head-sig* [meta-id]] (encore/ba-split head-ba 3)] - (when (encore/ba= head-sig* head-sig) ; Appears to be well-formed - [data-ba (head-meta meta-id {:unrecognized-meta? true})])))) + (when (encore/ba= head-sig* head-sig) ; Header appears to be well-formed + [data-ba (get head-meta meta-id {:unrecognized-meta? true})])))) + +(defn- get-auto-compressor [compressor-id] + (case compressor-id + nil nil + :snappy snappy-compressor + :lzma2 lzma2-compressor + :lz4 lz4-compressor + :no-header (throw (ex-info ":auto not supported on headerless data." {})) + :else (throw (ex-info ":auto not supported for non-standard compressors." {})) + (throw (ex-info (format "Unrecognized :auto compressor id: %s" compressor-id) + {:compressor-id compressor-id})))) + +(defn- get-auto-encryptor [encryptor-id] + (case encryptor-id + nil nil + :aes128-sha512 aes128-encryptor + :no-header (throw (ex-info ":auto not supported on headerless data." {})) + :else (throw (ex-info ":auto not supported for non-standard encryptors.")) + (throw (ex-info (format "Unrecognized :auto encryptor id: %s" encryptor-id) + {:encryptor-id encryptor-id})))) (defn thaw "Deserializes a frozen object from given byte array to its original Clojure - data type. By default[1] supports data frozen with current and all previous - versions of Nippy. For custom types extend the Clojure reader or see - `extend-thaw`. + data type. Supports data frozen with current and all previous versions of + Nippy. To thaw custom types, extend the Clojure reader or see `extend-thaw`. - [1] :headerless-meta provides a fallback facility for data frozen without a - standard Nippy header (notably all Nippy v1 data). A default is provided for - Nippy v1 thaw compatibility, but it's recommended that you _disable_ this - fallback (`{:headerless-meta nil}`) if you're certain you won't be thawing - headerless data." - [^bytes ba & [{:keys [password compressor encryptor headerless-meta] + Options include: + :compressor - An ICompressor, :auto (requires Nippy header), or nil. + :encryptor - An IEncryptor, :auto (requires Nippy header), or nil." + [^bytes ba & [{:keys [compressor encryptor password] :or {compressor :auto - encryptor :auto - headerless-meta ; Recommend set to nil when possible - {:version 2 - :compressed? true - :encrypted? false}} + encryptor :auto} :as opts}]] - (let [headerless-meta (merge headerless-meta (:legacy-opts opts)) ; Deprecated - _ (assert (or (nil? headerless-meta) - (head-meta-id headerless-meta)) - "Bad :headerless-meta (should be nil or a valid `head-meta` value)") + (assert (not (contains? opts :headerless-meta)) + ":headerless-meta `thaw` option removed as of Nippy v2.7.") + + (let [ex (fn [msg & [e]] (throw (ex-info (format "Thaw failed: %s" msg) + {:opts (merge opts + {:compressor compressor + :encryptor encryptor})} + e))) + thaw-data + (fn [data-ba compressor-id encryptor-id] + (let [compressor (if-not (identical? compressor :auto) compressor + (get-auto-compressor compressor-id)) + encryptor (if-not (identical? encryptor :auto) encryptor + (get-auto-encryptor encryptor-id))] + + (when (and encryptor (not password)) + (ex "Password required for decryption.")) - ex (fn [msg & [e]] (throw (Exception. (str "Thaw failed: " msg) e))) - try-thaw-data - (fn [data-ba {:keys [compressed? encrypted? version] - :as _head-or-headerless-meta}] - (let [encryptor (when (and password encrypted?) - (if-not (identical? encryptor :auto) encryptor - aes128-encryptor)) - compressor (when compressed? - (if-not (identical? compressor :auto) compressor - (case (int version) - 1 snappy-compressor - 2 lz4-compressor)))] (try (let [ba data-ba - ba (if encryptor (encryption/decrypt encryptor password ba) ba) - ba (if compressor (compression/decompress compressor ba) ba) - sin (DataInputStream. (ByteArrayInputStream. ba))] - (thaw-from-in! sin)) + ba (if-not encryptor ba (decrypt encryptor password ba)) + ba (if-not compressor ba (decompress compressor ba)) + dis (DataInputStream. (ByteArrayInputStream. ba))] + (thaw-from-in! dis)) (catch Exception e - (cond - encryptor (if head-meta (ex "Wrong password/encryptor?" e) - (ex "Unencrypted data?" e)) - compressor (if head-meta (ex "Encrypted data or wrong compressor?" e) - (ex "Uncompressed data?" e)) - :else (if head-meta (ex "Corrupt data?" e) - (ex "Data may be unfrozen, corrupt, compressed &/or encrypted.")))))))] + (ex "Decryption/decompression failure, or data unfrozen/damaged."))))) - (if-let [[data-ba {:keys [unrecognized-meta? compressed? encrypted?] + thaw-nippy-v1-data ; A little hackish, but necessary + (fn [data-ba] + (try (thaw-data data-ba :snappy nil) + (catch Exception _ + (thaw-data data-ba nil nil))))] + + (if-let [[data-ba {:keys [compressor-id encryptor-id unrecognized-meta?] :as head-meta}] (try-parse-header ba)] - (cond ; A well-formed header _appears_ to be present - (and (not headerless-meta) ; Cautious. It's unlikely but possible the - ; header sig match was a fluke and not an - ; indication of a real, well-formed header. - ; May really be headerless. - unrecognized-meta?) - (ex "Unrecognized (but apparently well-formed) header. Data frozen with newer Nippy version?") - - ;;; It's still possible below that the header match was a fluke, but it's - ;;; _very_ unlikely. Therefore _not_ going to incl. - ;;; `(not headerless-meta)` conditions below. - - (and compressed? (not compressor)) - (ex "Compressed data? Try again with compressor.") - (and encrypted? (not password)) - (if (::tools-thaw? opts) ::need-password - (ex "Encrypted data? Try again with password.")) - :else (try (try-thaw-data data-ba head-meta) - (catch Exception e - (if headerless-meta - (try (try-thaw-data ba headerless-meta) - (catch Exception _ - (throw e))) - (throw e))))) + ;; A well-formed header _appears_ to be present (it's possible though + ;; unlikely that this is a fluke and data is actually headerless): + (try (thaw-data data-ba compressor-id encryptor-id) + (catch Exception e + (try (thaw-nippy-v1-data) + (catch Exception _ + (if unrecognized-meta? + (ex "Unrecognized (but apparently well-formed) header. Data frozen with newer Nippy version?" + e) + (throw e)))))) ;; Well-formed header definitely not present - (if headerless-meta - (try-thaw-data ba headerless-meta) - (ex "Data may be unfrozen, corrupt, compressed &/or encrypted."))))) + (try (thaw-nippy-v1-data ba) + (catch Exception _ + (thaw-data ba :no-header :no-header)))))) (comment (thaw (freeze "hello")) (thaw (freeze "hello" {:compressor nil})) - (thaw (freeze "hello" {:password [:salted "p"]})) ; ex + (thaw (freeze "hello" {:password [:salted "p"]})) ; ex: no pwd (thaw (freeze "hello") {:password [:salted "p"]})) ;;;; Custom types (defmacro extend-freeze - "Alpha - subject to change. - Extends Nippy to support freezing of a custom type (ideally concrete) with + "Extends Nippy to support freezing of a custom type (ideally concrete) with id ∈[1, 128]: (defrecord MyType [data]) (extend-freeze MyType 1 [x data-output] (.writeUTF [data-output] (:data x)))" [type custom-type-id [x out] & body] (assert (and (>= custom-type-id 1) (<= custom-type-id 128))) - `(extend-type ~type - Freezable + `(extend-type ~type Freezable (~'freeze-to-out* [~x ~(with-meta out {:tag 'java.io.DataOutput})] (write-id ~out ~(int (- custom-type-id))) ~@body))) (defonce custom-readers (atom {})) ; { (fn [data-input]) ...} (defmacro extend-thaw - "Alpha - subject to change. - Extends Nippy to support thawing of a custom type with id ∈[1, 128]: + "Extends Nippy to support thawing of a custom type with id ∈[1, 128]: (extend-thaw 1 [data-input] (->MyType (.readUTF data-input)))" [custom-type-id [in] & body] @@ -623,16 +655,14 @@ compress? (> ba-len 1024)] (.writeBoolean out compress?) (if-not compress? (write-bytes out ba) - (let [ba* (compression/compress compression/lzma2-compressor ba)] + (let [ba* (compress lzma2-compressor ba)] (write-bytes out ba*))))) (extend-thaw 128 [in] (let [compressed? (.readBoolean in) - ba (read-bytes in)] - (thaw ba {:compressor compression/lzma2-compressor - :headerless-meta {:version 2 - :compressed? compressed? - :encrypted? false}}))) + ba (read-bytes in)] + (thaw ba {:compressor (when compressed? lzma2-compressor) + :encryptor nil}))) (comment (->> (apply str (repeatedly 1000 rand)) @@ -719,8 +749,6 @@ ;;;; Tools -(encore/defalias freezable? utils/freezable?) - (defn inspect-ba "Alpha - subject to change." [ba & [thaw-opts]] (if-not (encore/bytes? ba) :not-ba @@ -734,15 +762,15 @@ [data-ba nippy-header] (or (try-parse-header unwrapped-ba) [unwrapped-ba :no-header])] - {:known-wrapper known-wrapper - :nippy2-header nippy-header ; Nippy v1.x didn't have a header - :thawable? (try (thaw unwrapped-ba thaw-opts) true - (catch Exception _ false)) - :unwrapped-ba unwrapped-ba - :data-ba data-ba - :unwrapped-size (alength ^bytes unwrapped-ba) - :ba-size (alength ^bytes ba) - :data-size (alength ^bytes data-ba)}))) + {:known-wrapper known-wrapper + :nippy-v2-header nippy-header ; Nippy v1.x didn't have a header + :thawable? (try (thaw unwrapped-ba thaw-opts) true + (catch Exception _ false)) + :unwrapped-ba unwrapped-ba + :data-ba data-ba + :unwrapped-size (alength ^bytes unwrapped-ba) + :ba-size (alength ^bytes ba) + :data-size (alength ^bytes data-ba)}))) (comment (inspect-ba (freeze "hello")) (seq (:data-ba (inspect-ba (freeze "hello"))))) @@ -754,17 +782,3 @@ (def thaw-from-stream! "DEPRECATED: Use `thaw-from-in!` instead." thaw-from-in!) - -(defn freeze-to-bytes "DEPRECATED: Use `freeze` instead." - ^bytes [x & {:keys [compress?] - :or {compress? true}}] - (freeze x {:skip-header? true - :compressor (when compress? snappy-compressor) - :password nil})) - -(defn thaw-from-bytes "DEPRECATED: Use `thaw` instead." - [ba & {:keys [compressed?] - :or {compressed? true}}] - (thaw ba {:headerless-opts {:compressed? compressed?} - :compressor snappy-compressor - :password nil})) diff --git a/src/taoensso/nippy/benchmarks.clj b/src/taoensso/nippy/benchmarks.clj index 4ff2f47..e1abd51 100644 --- a/src/taoensso/nippy/benchmarks.clj +++ b/src/taoensso/nippy/benchmarks.clj @@ -3,8 +3,7 @@ (:require [clojure.tools.reader.edn :as edn] [clojure.data.fressian :as fressian] [taoensso.encore :as encore] - [taoensso.nippy :as nippy :refer (freeze thaw)] - [taoensso.nippy.compression :as compression])) + [taoensso.nippy :as nippy :refer (freeze thaw)])) (def data nippy/stress-data-benchable) @@ -45,16 +44,14 @@ #(thaw % {}))}) (println {:fast (bench1 #(freeze % {:compressor nil :skip-header? true}) - #(thaw % {:headerless-meta - {:version 2 - :compressed? false - :encrypted? false}}))}) + #(thaw % {:compressor nil + :encryptor nil}))}) (println {:encrypted (bench1 #(freeze % {:password [:cached "p"]}) #(thaw % {:password [:cached "p"]}))}) (when lzma2? ; Slow as molasses - (println {:lzma2 (bench1 #(freeze % {:compressor compression/lzma2-compressor}) - #(thaw % {:compressor compression/lzma2-compressor}))})) + (println {:lzma2 (bench1 #(freeze % {:compressor nippy/lzma2-compressor}) + #(thaw % {:compressor nippy/lzma2-compressor}))})) (when fressian? (println {:fressian (bench1 fressian-freeze fressian-thaw)}))) @@ -70,87 +67,21 @@ ;; (bench {:laps 2}) ;;; 2014 Apr 5 w/ headerless :fast, LZ4 replacing Snappy as default compressor - ;; {:default {:round 7669, :freeze 4157, :thaw 3512, :size 16143}} - ;; {:fast {:round 6918, :freeze 3636, :thaw 3282, :size 16992}} - ;; {:encrypted {:round 11814, :freeze 6180, :thaw 5634, :size 16164}} + {:default {:round 7669, :freeze 4157, :thaw 3512, :size 16143}} + {:fast {:round 6918, :freeze 3636, :thaw 3282, :size 16992}} + {:encrypted {:round 11814, :freeze 6180, :thaw 5634, :size 16164}} ;;; 2014 Jan 22: with common-type size optimizations, enlarged stress-data - ;; {:reader {:round 109544, :freeze 39523, :thaw 70021, :size 27681}} - ;; {:default {:round 9234, :freeze 5128, :thaw 4106, :size 15989}} - ;; {:fast {:round 7402, :freeze 4021, :thaw 3381, :size 16957}} - ;; {:encrypted {:round 12594, :freeze 6884, :thaw 5710, :size 16020}} - ;; {:lzma2 {:round 66759, :freeze 44246, :thaw 22513, :size 11208}} - ;; {:fressian {:round 13052, :freeze 8694, :thaw 4358, :size 16942}} + {:reader {:round 109544, :freeze 39523, :thaw 70021, :size 27681}} + {:default {:round 9234, :freeze 5128, :thaw 4106, :size 15989}} + {:fast {:round 7402, :freeze 4021, :thaw 3381, :size 16957}} + {:encrypted {:round 12594, :freeze 6884, :thaw 5710, :size 16020}} + {:lzma2 {:round 66759, :freeze 44246, :thaw 22513, :size 11208}} + {:fressian {:round 13052, :freeze 8694, :thaw 4358, :size 16942}} ;;; 19 Oct 2013: Nippy v2.3.0, with lzma2 & (nb!) round=freeze+thaw - ;; {:reader {:round 67798, :freeze 23202, :thaw 44596, :size 22971}} - ;; {:default {:round 3632, :freeze 2349, :thaw 1283, :size 12369}} - ;; {:encrypted {:round 6970, :freeze 4073, :thaw 2897, :size 12388}} - ;; {:fast {:round 3294, :freeze 2109, :thaw 1185, :size 13277}} - ;; {:lzma2 {:round 44590, :freeze 29567, :thaw 15023, :size 9076}} - - ;;; 11 Oct 2013: Nippy v2.2.0, with both ztellman mods - ;; {:defaults {:round 4319, :freeze 2950, :thaw 1446, :data-size 12369}} - ;; {:encrypted {:round 7675, :freeze 4479, :thaw 3160, :data-size 12388}} - ;; {:fast {:round 3928, :freeze 2530, :thaw 1269, :data-size 13277}} - ;; {:defaults-delta {:round 0.84 :freeze 0.79 :thaw 1.14}} ; vs 2.2.0 - - ;;; 11 Oct 2013: Nippy v2.2.0, with first ztellman mod - ;; {:defaults {:round 4059, :freeze 2578, :thaw 1351, :data-size 12342}} - ;; {:encrypted {:round 7248, :freeze 4058, :thaw 3041, :data-size 12372}} - ;; {:fast {:round 3430, :freeze 2085, :thaw 1229, :data-size 13277}} - ;; {:defaults-delta {:round 0.79 :freeze 0.69 :thaw 1.07}} ; vs 2.2.0 - - ;;; 11 Oct 2013: Nippy v2.2.0 - ;; {:defaults {:round 5135, :freeze 3711, :thaw 1266, :data-size 12393}} - ;; {:encrypted {:round 8655, :freeze 5323, :thaw 3036, :data-size 12420}} - ;; {:fast {:round 4670, :freeze 3282, :thaw 1294, :data-size 13277}} - - ;;; 7 Auguest 2013: Nippy v2.2.0-RC1 - ;; {:reader {:round 71582, :freeze 13656, :thaw 56730, :data-size 22964}} - ;; {:defaults {:round 5619, :freeze 3710, :thaw 1783, :data-size 12368}} - ;; {:encrypted {:round 9113, :freeze 5324, :thaw 3500, :data-size 12388}} - ;; {:fast {:round 5130, :freeze 3286, :thaw 1667, :data-size 13325}} - - ;;; 17 June 2013: Clojure 1.5.1, JVM 7 Nippy 2.0.0-alpha6 w/fast io-streams - ;; {:reader {:round 49819, :freeze 23601, :thaw 26247, :data-size 22966}} - ;; {:defaults {:round 5670, :freeze 3536, :thaw 1919, :data-size 12396}} - ;; {:encrypted {:round 9038, :freeze 5111, :thaw 3582, :data-size 12420}} - ;; {:fast {:round 5182, :freeze 3177, :thaw 1820, :data-size 13342}} - - ;;; 16 June 2013: Clojure 1.5.1, Nippy 2.0.0-alpha6 - ;; {:reader {:freeze 23601, :thaw 26247, :round 49819, :data-size 22966}} - ;; {:defaults {:freeze 3554, :thaw 2002, :round 5831, :data-size 12394}} - ;; {:encrypted {:freeze 5117, :thaw 3600, :round 9006, :data-size 12420}} - ;; {:fast {:freeze 3247, :thaw 1914, :round 5329, :data-size 13342}} - - ;;; 13 June 2013: Clojure 1.5.1, Nippy 2.0.0-alpha1 - ;; {:reader {:freeze 23124, :thaw 26469, :round 47674, :data-size 22923}} - ;; {:defaults {:freeze 4007, :thaw 2520, :round 6038, :data-size 12387}} - ;; {:encrypted {:freeze 5560, :thaw 3867, :round 9157, :data-size 12405}} - ;; {:fast {:freeze 3429, :thaw 2078, :round 5577, :data-size 13237}} - - ;;; 11 June 2013: Clojure 1.5.1, Nippy 1.3.0-alpha1 - ;; {:reader {:freeze 17042, :thaw 31579, :round 48379, :data-size 22954}} - ;; {:fast {:freeze 3078, :thaw 4684, :round 8117, :data-size 13274}} - ;; {:defaults {:freeze 3810, :thaw 5295, :round 9052, :data-size 12394}} - ;; {:encrypted {:freeze 5800, :thaw 6862, :round 12317, :data-size 12416}} - - ;;; Clojure 1.5.1, Nippy 1.2.1 (+ sorted-set, sorted-map) - ;; (def data (dissoc data :sorted-set :sorted-map)) - ;; {:reader {:freeze 15037, :thaw 27885, :round 43945}, - ;; :nippy {:freeze 3194, :thaw 4734, :round 8380}} - ;; {:reader-size 22975, :defaults-size 12400, :encrypted-size 12400} - - ;;; Clojure 1.4.0, Nippy 1.0.0 (+ tagged-uuid, tagged-date) - ;; {:reader {:freeze 22595, :thaw 31148, :round 54059} - ;; :nippy {:freeze 3324, :thaw 3725, :round 6918}} - - ;;; Clojure 1.3.0, Nippy 0.9.2 - ;; {:reader {:freeze 28505, :thaw 36451, :round 59545}, - ;; :nippy {:freeze 3751, :thaw 4184, :round 7769}} - - (println (bench* (roundtrip data))) ; Snappy implementations - ;; {:no-snappy [6163 6064 6042 6176] :JNI [6489 6446 6542 6412] - ;; :native-array-copy [6569 6419 6414 6590]} - ) + {:reader {:round 67798, :freeze 23202, :thaw 44596, :size 22971}} + {:default {:round 3632, :freeze 2349, :thaw 1283, :size 12369}} + {:encrypted {:round 6970, :freeze 4073, :thaw 2897, :size 12388}} + {:fast {:round 3294, :freeze 2109, :thaw 1185, :size 13277}} + {:lzma2 {:round 44590, :freeze 29567, :thaw 15023, :size 9076}}) diff --git a/src/taoensso/nippy/compression.clj b/src/taoensso/nippy/compression.clj index 85b7864..4f5b2eb 100644 --- a/src/taoensso/nippy/compression.clj +++ b/src/taoensso/nippy/compression.clj @@ -1,4 +1,4 @@ -(ns taoensso.nippy.compression "Alpha - subject to change." +(ns taoensso.nippy.compression {:author "Peter Taoussanis"} (:require [taoensso.encore :as encore]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream DataInputStream @@ -7,13 +7,17 @@ ;;;; Interface (defprotocol ICompressor + (header-id [compressor]) (compress ^bytes [compressor ba]) (decompress ^bytes [compressor ba])) ;;;; Default implementations +(def standard-header-ids "These'll support :auto thaw." #{:snappy :lzma2 :lz4}) + (deftype SnappyCompressor [] ICompressor + (header-id [_] :snappy) (compress [_ ba] (org.iq80.snappy.Snappy/compress ba)) (decompress [_ ba] (org.iq80.snappy.Snappy/uncompress ba 0 (alength ^bytes ba)))) @@ -29,30 +33,34 @@ (deftype LZMA2Compressor [compression-level] ;; Compression level ∈ℕ[0,9] (low->high) with 6 LZMA2 default (we use 0) ICompressor - (compress [_ ba] - (let [ba-len (alength ^bytes ba) - ba-os (ByteArrayOutputStream.) + (header-id [_] :lzma2) + (compress [_ ba] + (let [baos (ByteArrayOutputStream.) + dos (DataOutputStream. baos) + ;; + len-decomp (alength ^bytes ba) ;; Prefix with uncompressed length: - _ (.writeInt (DataOutputStream. ba-os) ba-len) - xzs (org.tukaani.xz.XZOutputStream. ba-os - (org.tukaani.xz.LZMA2Options. compression-level))] + _ (.writeInt dos len-decomp) + xzs (org.tukaani.xz.XZOutputStream. baos + (org.tukaani.xz.LZMA2Options. compression-level))] (.write xzs ^bytes ba) (.close xzs) - (.toByteArray ba-os))) + (.toByteArray baos))) (decompress [_ ba] - (let [ba-is (ByteArrayInputStream. ba) - ba-len (.readInt (DataInputStream. ba-is)) - ba (byte-array ba-len) - xzs (org.tukaani.xz.XZInputStream. ba-is)] - (.read xzs ba 0 ba-len) + (let [bais (ByteArrayInputStream. ba) + dis (DataInputStream. bais) + ;; + len-decomp (.readInt dis) + ba (byte-array len-decomp) + xzs (org.tukaani.xz.XZInputStream. bais)] + (.read xzs ba 0 len-decomp) (when (not= -1 (.read xzs)) ; Good practice as extra safety measure - (throw (Exception. "LZMA2 Decompress failed: corrupt data?"))) + (throw (ex-info "LZMA2 Decompress failed: corrupt data?" {:ba ba}))) ba))) (def lzma2-compressor - "Alpha - subject to change. - Default org.tukaani.xz.LZMA2 compressor: + "Default org.tukaani.xz.LZMA2 compressor: Ratio: high. Write speed: _very_ slow (also currently single-threaded). Read speed: slow. @@ -64,7 +72,8 @@ (deftype LZ4Compressor [^net.jpountz.lz4.LZ4Compressor compressor ^net.jpountz.lz4.LZ4Decompressor decompressor] ICompressor - (compress [_ ba] + (header-id [_] :lz4) + (compress [_ ba] (let [len-decomp (alength ^bytes ba) max-len-comp (.maxCompressedLength compressor len-decomp) ba-comp* (byte-array max-len-comp) ; Over-sized @@ -82,10 +91,10 @@ ;; len-decomp (.readInt dis) len-comp (- (alength ^bytes ba) 4) - ba-comp (byte-array len-comp) - _ (.readFully dis ba-comp 0 len-comp) + ;; ba-comp (byte-array len-comp) + ;; _ (.readFully dis ba-comp 0 len-comp) ba-decomp (byte-array len-decomp) - _ (.decompress decompressor ba-comp 0 ba-decomp 0 len-decomp)] + _ (.decompress decompressor ba 4 ba-decomp 0 len-decomp)] ba-decomp))) (def ^:private ^net.jpountz.lz4.LZ4Factory lz4-factory @@ -118,13 +127,13 @@ (count ba-bench)))}) (println - {:snappy (bench1 snappy-compressor) - ;; :lzma (bench1 lzma2-compressor) ; Slow! - :lz4 (bench1 lz4-compressor) - :lz4hc (bench1 lz4hc-compressor)}) + {:snappy (bench1 snappy-compressor) + ;:lzma2 (bench1 lzma2-compressor) ; Slow! + :lz4 (bench1 lz4-compressor) + :lz4hc (bench1 lz4hc-compressor)}) - ;;; 2014 April 5, initial benchmarks - {:snappy {:time 2214 :ratio 0.848} - :lzma {:time 46684 :ratio 0.494} - :lz4 {:time 1363 :ratio 0.819} - :lz4hc {:time 6045 :ratio 0.763}}) + ;;; 2014 April 7 + {:snappy {:time 2251, :ratio 0.852}, + :lzma2 {:time 46684 :ratio 0.494} + :lz4 {:time 1184, :ratio 0.819}, + :lz4hc {:time 5422, :ratio 0.761}}) diff --git a/src/taoensso/nippy/encryption.clj b/src/taoensso/nippy/encryption.clj index 5516783..cc715c4 100644 --- a/src/taoensso/nippy/encryption.clj +++ b/src/taoensso/nippy/encryption.clj @@ -1,13 +1,15 @@ (ns taoensso.nippy.encryption - "Alpha - subject to change. - Simple no-nonsense crypto with reasonable defaults. Because your Clojure data + "Simple no-nonsense crypto with reasonable defaults. Because your Clojure data deserves some privacy." {:author "Peter Taoussanis"} (:require [taoensso.encore :as encore])) ;;;; Interface +(def standard-header-ids "These'll support :auto thaw." #{:aes128-sha512}) + (defprotocol IEncryptor + (header-id [encryptor]) (encrypt ^bytes [encryptor pwd ba]) (decrypt ^bytes [encryptor pwd ba])) @@ -25,7 +27,7 @@ (defn- rand-bytes [size] (let [seed (byte-array size)] (.nextBytes prng seed) seed)) -;;;; Default keygen +;;;; Default key-gen (defn- sha512-key "SHA512-based key generator. Good JVM availability without extra dependencies @@ -38,6 +40,7 @@ (recur (.digest sha512-md ba) (dec n)) (-> ba (java.util.Arrays/copyOf aes128-block-size) (javax.crypto.spec.SecretKeySpec. "AES"))))) + (comment (time (sha512-key nil "hi" 1)) ; ~40ms per hash (fast) (time (sha512-key nil "hi" 5)) ; ~180ms (default) @@ -47,54 +50,52 @@ ;;;; Default implementations -(defn- destructure-typed-pwd - [typed-password] - (letfn [(throw-ex [] - (throw (AssertionError. - (str "Expected password form: " - "[<#{:salted :cached}> ].\n " - "See `default-aes128-encryptor` docstring for details!"))))] - (if-not (vector? typed-password) - (throw-ex) +(defn- destructure-typed-pwd [typed-password] + (let [throw-ex + (fn [] (throw (ex-info + (str "Expected password form: " + "[<#{:salted :cached}> ].\n " + "See `default-aes128-encryptor` docstring for details!") + {:typed-password typed-password})))] + (if-not (vector? typed-password) (throw-ex) (let [[type password] typed-password] - (if-not (#{:salted :cached} type) - (throw-ex) + (if-not (#{:salted :cached} type) (throw-ex) [type password]))))) (comment (destructure-typed-pwd [:salted "foo"])) -(defrecord AES128Encryptor [key-cache] +(defrecord AES128Encryptor [key-gen key-cache] IEncryptor - (encrypt [this typed-pwd data-ba] + (header-id [_] (if (= key-gen sha512-key) :aes128-sha512 :aes128-other)) + (encrypt [_ typed-pwd data-ba] (let [[type pwd] (destructure-typed-pwd typed-pwd) - salt? (= type :salted) + salt? (identical? type :salted) iv-ba (rand-bytes aes128-block-size) salt-ba (when salt? (rand-bytes salt-size)) prefix-ba (if-not salt? iv-ba (encore/ba-concat iv-ba salt-ba)) - key (encore/memoized (when-not salt? (:key-cache this)) - sha512-key salt-ba pwd) + key (encore/memoized (when-not salt? key-cache) + key-gen salt-ba pwd) iv (javax.crypto.spec.IvParameterSpec. iv-ba)] (.init aes128-cipher javax.crypto.Cipher/ENCRYPT_MODE ^javax.crypto.spec.SecretKeySpec key iv) (encore/ba-concat prefix-ba (.doFinal aes128-cipher data-ba)))) - (decrypt [this typed-pwd ba] + (decrypt [_ typed-pwd ba] (let [[type pwd] (destructure-typed-pwd typed-pwd) salt? (= type :salted) prefix-size (+ aes128-block-size (if salt? salt-size 0)) [prefix-ba data-ba] (encore/ba-split ba prefix-size) [iv-ba salt-ba] (if-not salt? [prefix-ba nil] (encore/ba-split prefix-ba aes128-block-size)) - key (encore/memoized (when-not salt? (:key-cache this)) - sha512-key salt-ba pwd) + key (encore/memoized (when-not salt? key-cache) + key-gen salt-ba pwd) iv (javax.crypto.spec.IvParameterSpec. iv-ba)] (.init aes128-cipher javax.crypto.Cipher/DECRYPT_MODE ^javax.crypto.spec.SecretKeySpec key iv) (.doFinal aes128-cipher data-ba)))) (def aes128-encryptor - "Alpha - subject to change. - Default 128bit AES encryptor with multi-round SHA-512 keygen. + "Default 128bit AES encryptor with multi-round SHA-512 key-gen. Password form [:salted \"my-password\"] --------------------------------------- @@ -128,7 +129,7 @@ Faster than `aes128-salted`, and harder to attack any particular key - but increased danger if a key is somehow compromised." - (->AES128Encryptor (atom {}))) + (->AES128Encryptor sha512-key (atom {}))) ;;;; Default implementation diff --git a/src/taoensso/nippy/tools.clj b/src/taoensso/nippy/tools.clj index a6f7516..ea1c84c 100644 --- a/src/taoensso/nippy/tools.clj +++ b/src/taoensso/nippy/tools.clj @@ -1,7 +1,6 @@ (ns taoensso.nippy.tools - "Alpha - subject to change. - Utilities for third-party tools that want to add fully-user-configurable Nippy - support. Used by Carmine and Faraday." + "Utilities for third-party tools that want to add fully-user-configurable + Nippy support. Used by Carmine and Faraday." {:author "Peter Taoussanis"} (:require [taoensso.nippy :as nippy])) @@ -23,26 +22,12 @@ (comment (freeze (wrap-for-freezing "wrapped")) (freeze "unwrapped")) -(defrecord EncryptedFrozen [ba]) -(defn encrypted-frozen? [x] (instance? EncryptedFrozen x)) - (def ^:dynamic *thaw-opts* nil) (defmacro with-thaw-opts "Evaluates body using given options for any automatic deserialization in context." [opts & body] `(binding [*thaw-opts* ~opts] ~@body)) -(defn thaw - "Like `nippy/thaw` but takes options from *thaw-opts* binding, and wraps - encrypted bytes for easy identification when no password has been provided - for decryption." +(defn thaw "Like `nippy/thaw` but takes options from *thaw-opts* binding." [ba & [{:keys [default-opts]}]] - (let [result (nippy/thaw ba (merge default-opts *thaw-opts* - {:taoensso.nippy/tools-thaw? true}))] - (if (= result :taoensso.nippy/need-password) - (EncryptedFrozen. ba) - result))) - -(comment (thaw (nippy/freeze "c" {:password [:cached "p"]})) - (with-thaw-opts {:password [:cached "p"]} - (thaw (nippy/freeze "c" {:password [:cached "p"]})))) + (nippy/thaw ba (merge default-opts *thaw-opts*))) diff --git a/test/taoensso/nippy/tests/main.clj b/test/taoensso/nippy/tests/main.clj index 2a80fd4..561cd5e 100644 --- a/test/taoensso/nippy/tests/main.clj +++ b/test/taoensso/nippy/tests/main.clj @@ -4,7 +4,6 @@ [clojure.test.check.properties :as check-props] [expectations :as test :refer :all] [taoensso.nippy :as nippy :refer (freeze thaw)] - [taoensso.nippy.compression :as compression] [taoensso.nippy.benchmarks :as benchmarks])) (comment (test/run-tests '[taoensso.nippy.tests.main])) @@ -16,29 +15,30 @@ ;;;; Core (expect test-data ((comp thaw freeze) test-data)) -(expect test-data ((comp #(thaw % {:headerless-meta {:version 1 - :compressed? true - :encrypted? false}}) +(expect test-data ((comp #(thaw % {}) #(freeze % {:legacy-mode true})) test-data)) (expect test-data ((comp #(thaw % {:password [:salted "p"]}) #(freeze % {:password [:salted "p"]})) test-data)) -(expect test-data ((comp #(thaw % {:compressor compression/lzma2-compressor}) - #(freeze % {:compressor compression/lzma2-compressor})) +(expect test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor}) + #(freeze % {:compressor nippy/lzma2-compressor})) test-data)) -(expect test-data ((comp #(thaw % {:compressor compression/lzma2-compressor +(expect test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor :password [:salted "p"]}) - #(freeze % {:compressor compression/lzma2-compressor + #(freeze % {:compressor nippy/lzma2-compressor :password [:salted "p"]})) test-data)) +(expect test-data ((comp #(thaw % {:compressor nippy/lz4-compressor}) + #(freeze % {:compressor nippy/lz4hc-compressor})) + test-data)) (expect ; Try roundtrip anything that simple-check can dream up (:result (check/quick-check 80 ; Time is n-non-linear (check-props/for-all [val check-gen/any] (= val (nippy/thaw (nippy/freeze val))))))) -(expect AssertionError (thaw (freeze test-data {:password "malformed"}))) +(expect Exception (thaw (freeze test-data {:password "malformed"}))) (expect Exception (thaw (freeze test-data {:password [:salted "p"]}))) (expect Exception (thaw (freeze test-data {:password [:salted "p"]}) {:compressor nil})) From 06cf8aefd6ce263ff42bee9cf3646ab6c20f9bf5 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 18:14:01 +0700 Subject: [PATCH 06/19] Update benchmarks --- benchmarks.png | Bin 23494 -> 23547 bytes src/taoensso/nippy/benchmarks.clj | 11 ++++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/benchmarks.png b/benchmarks.png index 3fc7db673ef9e171f01ad3bbfd5e499a62ff18ec..f46c3390e963271d6a2602cf08029e381da7c45b 100644 GIT binary patch literal 23547 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 diff --git a/src/taoensso/nippy/benchmarks.clj b/src/taoensso/nippy/benchmarks.clj index e1abd51..9da799c 100644 --- a/src/taoensso/nippy/benchmarks.clj +++ b/src/taoensso/nippy/benchmarks.clj @@ -66,10 +66,15 @@ ;; (bench {:reader? true :lzma2? true :fressian? true :laps 1}) ;; (bench {:laps 2}) + ;;; 2014 Apr 7 w/ some additional implementation tuning + {:default {:round 6533, :freeze 3618, :thaw 2915, :size 16139}} + {:fast {:round 6250, :freeze 3376, :thaw 2874, :size 16992}} + {:encrypted {:round 10583, :freeze 5581, :thaw 5002, :size 16164}} + ;;; 2014 Apr 5 w/ headerless :fast, LZ4 replacing Snappy as default compressor - {:default {:round 7669, :freeze 4157, :thaw 3512, :size 16143}} - {:fast {:round 6918, :freeze 3636, :thaw 3282, :size 16992}} - {:encrypted {:round 11814, :freeze 6180, :thaw 5634, :size 16164}} + {:default {:round 7039, :freeze 3865, :thaw 3174, :size 16123}} + {:fast {:round 6394, :freeze 3379, :thaw 3015, :size 16992}} + {:encrypted {:round 11035, :freeze 5860, :thaw 5175, :size 16148}} ;;; 2014 Jan 22: with common-type size optimizations, enlarged stress-data {:reader {:round 109544, :freeze 39523, :thaw 70021, :size 27681}} From 70dd8f637c13c694c4bd517dabad4f42073d33a4 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 8 Apr 2014 23:33:23 +0700 Subject: [PATCH 07/19] Lock-free memoized type test --- src/taoensso/nippy/utils.clj | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/taoensso/nippy/utils.clj b/src/taoensso/nippy/utils.clj index 4ed2138..6298f5c 100644 --- a/src/taoensso/nippy/utils.clj +++ b/src/taoensso/nippy/utils.clj @@ -1,7 +1,8 @@ (ns taoensso.nippy.utils {:author "Peter Taoussanis"} (:require [clojure.string :as str] - [clojure.tools.reader.edn :as edn]) + [clojure.tools.reader.edn :as edn] + [taoensso.encore :as encore]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream Serializable ObjectOutputStream ObjectInputStream])) @@ -17,12 +18,7 @@ cacheable? (not (re-find #"__\d+" (str t))) ; gensym form test (fn [] (try (f-test x) (catch Exception _ false)))] (if-not cacheable? (test) - (if-let [dv (@cache t)] @dv - (locking cache ; For thread racing - (if-let [dv (@cache t)] @dv ; Retry after lock acquisition - (let [dv (delay (test))] - (swap! cache assoc t dv) - @dv))))))))) + @(encore/swap-val! cache t #(if % % (delay (test))))))))) (def serializable? (memoize-type-test @@ -46,10 +42,10 @@ (readable? "Hello world") (readable? (fn [])) - (time (dotimes [_ 10000] (serializable? "Hello world"))) - (time (dotimes [_ 10000] (serializable? (fn [])))) - (time (dotimes [_ 10000] (readable? "Hello world"))) - (time (dotimes [_ 10000] (readable? (fn []))))) + (time (dotimes [_ 10000] (serializable? "Hello world"))) ; Cacheable + (time (dotimes [_ 10000] (serializable? (fn [])))) ; Uncacheable + (time (dotimes [_ 10000] (readable? "Hello world"))) ; Cacheable + (time (dotimes [_ 10000] (readable? (fn []))))) ; Uncacheable ;;;; From 39c5b1287523f0262e703651620e8cfd2e9cc1d6 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Fri, 4 Jul 2014 16:42:28 +0700 Subject: [PATCH 08/19] Bump deps --- project.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project.clj b/project.clj index fb20648..e0bb080 100644 --- a/project.clj +++ b/project.clj @@ -11,8 +11,8 @@ *assert* true} :dependencies [[org.clojure/clojure "1.4.0"] - [org.clojure/tools.reader "0.8.4"] - [com.taoensso/encore "1.3.1"] + [org.clojure/tools.reader "0.8.5"] + [com.taoensso/encore "1.7.0"] [org.iq80.snappy/snappy "0.3"] [org.tukaani/xz "1.5"] [net.jpountz.lz4/lz4 "1.2.0"]] @@ -24,9 +24,9 @@ :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :test {:jvm-opts ["-Xms1024m" "-Xmx2048m"] :dependencies [[expectations "1.4.56"] - [org.clojure/test.check "0.5.7"] + [org.clojure/test.check "0.5.8"] [org.clojure/data.fressian "0.2.0"] - [org.xerial.snappy/snappy-java "1.1.1-M1"]] + [org.xerial.snappy/snappy-java "1.1.1"]] :plugins [[lein-expectations "0.0.8"] [lein-autoexpect "1.2.2"]]} :dev* [:dev {:jvm-opts ^:replace ["-server"] From 9a9330ed51095ffd7929c1b8a8fea4d8f24ce0d7 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Jul 2014 14:43:24 +0700 Subject: [PATCH 09/19] Update project.clj template --- project.clj | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/project.clj b/project.clj index e0bb080..17fcaa7 100644 --- a/project.clj +++ b/project.clj @@ -9,6 +9,7 @@ :min-lein-version "2.3.3" :global-vars {*warn-on-reflection* true *assert* true} + :dependencies [[org.clojure/clojure "1.4.0"] [org.clojure/tools.reader "0.8.5"] @@ -17,37 +18,33 @@ [org.tukaani/xz "1.5"] [net.jpountz.lz4/lz4 "1.2.0"]] - :test-paths ["test" "src"] :profiles {;; :default [:base :system :user :provided :dev] + :server-jvm {:jvm-opts ^:replace ["-server" "-Xms1024m" "-Xmx2048m"]} :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :test {:jvm-opts ["-Xms1024m" "-Xmx2048m"] - :dependencies [[expectations "1.4.56"] + :dependencies [[expectations "2.0.7"] [org.clojure/test.check "0.5.8"] + ;; [com.cemerick/double-check "0.5.7"] [org.clojure/data.fressian "0.2.0"] - [org.xerial.snappy/snappy-java "1.1.1"]] - :plugins [[lein-expectations "0.0.8"] - [lein-autoexpect "1.2.2"]]} - :dev* [:dev {:jvm-opts ^:replace ["-server"] - ;; :hooks [cljx.hooks leiningen.cljsbuild] ; cljx - }] - :dev - [:1.6 :test - {:jvm-opts ^:replace ["-server" "-Xms1024m" "-Xmx2048m"] - :dependencies [] - :plugins [[lein-ancient "0.5.4"] - [codox "0.6.7"]]}]} + [org.xerial.snappy/snappy-java "1.1.1"]]} + :dev [:1.6 :test + {:plugins + [[lein-pprint "1.1.1"] + [lein-ancient "0.5.5"] + [lein-expectations "0.0.8"] + [lein-autoexpect "1.2.2"] + [codox "0.8.10"]]}]} + + :test-paths ["test" "src"] - ;; :codox {:sources ["target/classes"]} ; cljx :aliases {"test-all" ["with-profile" "default:+1.5:+1.6" "expectations"] ;; "test-all" ["with-profile" "default:+1.6" "expectations"] "test-auto" ["with-profile" "+test" "autoexpect"] - ;; "build-once" ["do" "cljx" "once," "cljsbuild" "once"] ; cljx - ;; "deploy-lib" ["do" "build-once," "deploy" "clojars," "install"] ; cljx "deploy-lib" ["do" "deploy" "clojars," "install"] - "start-dev" ["with-profile" "+dev*" "repl" ":headless"]} + "start-dev" ["with-profile" "+server-jvm" "repl" ":headless"]} :repositories {"sonatype" From e17a7f8248db7667ff4123ac6a16120f88dc12fb Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Jul 2014 21:50:01 +0700 Subject: [PATCH 10/19] Temporarily revert `expectations` dep bump due to #40 https://github.com/jaycfields/expectations/issues/40 --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 17fcaa7..71a82eb 100644 --- a/project.clj +++ b/project.clj @@ -24,7 +24,7 @@ :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :test {:jvm-opts ["-Xms1024m" "-Xmx2048m"] - :dependencies [[expectations "2.0.7"] + :dependencies [[expectations "1.4.56"] [org.clojure/test.check "0.5.8"] ;; [com.cemerick/double-check "0.5.7"] [org.clojure/data.fressian "0.2.0"] From 9e60939848b22d0723323b2a13ced6b93a3feca8 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Fri, 4 Jul 2014 20:05:18 +0700 Subject: [PATCH 11/19] EXPERIMENTAL: Support keyword-id extensions (#50) --- README.md | 4 +- src/taoensso/nippy.clj | 113 +++++++++++++++++++++-------- test/taoensso/nippy/tests/main.clj | 14 +++- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 24227c8..b00b106 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,11 @@ There's two default forms of encryption on offer: `:salted` and `:cached`. Each ```clojure (defrecord MyType [data]) -(nippy/extend-freeze MyType 1 ; A unique type id ∈[1, 128] +(nippy/extend-freeze MyType :my-type/foo ; A unique (namespaced) type identifier [x data-output] (.writeUTF data-output (:data x))) -(nippy/extend-thaw 1 ; Same type id +(nippy/extend-thaw :my-type/foo ; Same type id [data-input] (->MyType (.readUTF data-input))) diff --git a/src/taoensso/nippy.clj b/src/taoensso/nippy.clj index 8ee57c3..a1584b5 100644 --- a/src/taoensso/nippy.clj +++ b/src/taoensso/nippy.clj @@ -19,11 +19,15 @@ PersistentQueue PersistentTreeMap PersistentTreeSet PersistentList ; LazySeq IRecord ISeq])) -;;;; Nippy 2.x+ header spec (4 bytes) -;; Header is optional but recommended + enabled by default. Purpose: -;; * Sanity check (data appears to be Nippy data). -;; * Nippy version check (=> supports changes to data schema over time). -;; * Supports :auto thaw compressor, encryptor. +;;;; Nippy data format +;; * 4-byte header (Nippy v2.x+) (may be disabled but incl. by default) [1]. +;; { * 1-byte type id. +;; * Arb-length payload. } ... +;; +;; [1] Inclusion of header is strongly recommended. Purpose: +;; * Sanity check (confirm that data appears to be Nippy data). +;; * Nippy version check (=> supports changes to data schema over time). +;; * Supports :auto thaw compressor, encryptor. ;; (def ^:private ^:const head-version 1) (def ^:private head-sig (.getBytes "NPY" "UTF-8")) @@ -51,8 +55,10 @@ ;;;; Data type IDs -;; **Negative ids reserved for user-defined types** (do ; Just for easier IDE collapsing + + ;; ** Negative ids reserved for user-defined types ** + ;; (def ^:const id-reserved (int 0)) ;; 1 (def ^:const id-bytes (int 2)) @@ -93,6 +99,7 @@ (def ^:const id-record (int 80)) ;; (def ^:const id-type (int 81)) ; TODO? + (def ^:const id-prefixed-custom (int 82)) (def ^:const id-date (int 90)) (def ^:const id-uuid (int 91)) @@ -402,6 +409,20 @@ [(thaw-from-in in#) (thaw-from-in in#)]))) (declare ^:private custom-readers) +(defn- read-custom! [type-id in] + (if-let [custom-reader (get @custom-readers type-id)] + (try + (custom-reader in) + (catch Exception e + (throw + (ex-info + (format "Reader exception for custom type with internal id: %s" + type-id) {:internal-type-id type-id} e)))) + (throw + (ex-info + (format "No reader provided for custom type with internal id: %s" + type-id) + {:internal-type-id type-id})))) (defn- thaw-from-in [^DataInput in] @@ -491,22 +512,12 @@ (* 2 (.readInt in)) (thaw-from-in in))) id-old-keyword (keyword (.readUTF in)) - (if-not (neg? type-id) - (throw (ex-info (format "Unknown type ID: %s" type-id) - {:type-id type-id})) + id-prefixed-custom ; Prefixed custom type + (let [hash-id (.readShort in)] + (read-custom! hash-id in)) - ;; Custom types - (if-let [reader (get @custom-readers type-id)] - (try (reader in) - (catch Exception e - (throw (ex-info - (format "Reader exception for custom type ID: %s" - (- type-id)) - {:type-id (- type-id)} e)))) - (throw (ex-info - (format "No reader provided for custom type ID: %s" - (- type-id)) - {:type-id (- type-id)}))))) + (read-custom! type-id in) ; Unprefixed custom type (catchall) + ) (catch Exception e (throw (ex-info (format "Thaw failed against type-id: %s" type-id) @@ -617,29 +628,67 @@ ;;;; Custom types +(defn- assert-custom-type-id [custom-type-id] + (assert (or (keyword? custom-type-id) + (and (integer? custom-type-id) (<= 1 custom-type-id 128))))) + +(defn coerce-custom-type-id + "* +ive byte id -> -ive byte id (for unprefixed custom types). + * Keyword id -> Short hash id (for prefixed custom types)." + [custom-type-id] + (assert-custom-type-id custom-type-id) + (if-not (keyword? custom-type-id) + (int (- custom-type-id)) + (let [hash-id (hash custom-type-id) + short-hash-id (if (pos? hash-id) + (mod hash-id Short/MAX_VALUE) + (mod hash-id Short/MIN_VALUE))] + ;; Make sure hash ids can't collide with byte ids (unlikely anyway): + (assert (not (<= -128 short-hash-id -1)) + "Custom type id hash collision; please choose a different id") + (int short-hash-id)))) + +(comment (coerce-custom-type-id 77) + (coerce-custom-type-id :foo/bar)) + (defmacro extend-freeze "Extends Nippy to support freezing of a custom type (ideally concrete) with - id ∈[1, 128]: + given id of form: + * Keyword - 2 byte overhead, resistent to id collisions. + * Byte ∈[1, 128] - no overhead, subject to id collisions. + (defrecord MyType [data]) - (extend-freeze MyType 1 [x data-output] + (extend-freeze MyType :foo/my-type [x data-output] ; Keyword id + (.writeUTF [data-output] (:data x))) + ;; or + (extend-freeze MyType 1 [x data-output] ; Byte id (.writeUTF [data-output] (:data x)))" [type custom-type-id [x out] & body] - (assert (and (>= custom-type-id 1) (<= custom-type-id 128))) + (assert-custom-type-id custom-type-id) `(extend-type ~type Freezable (~'freeze-to-out* [~x ~(with-meta out {:tag 'java.io.DataOutput})] - (write-id ~out ~(int (- custom-type-id))) + (if-not ~(keyword? custom-type-id) + ;; Unprefixed [cust byte id][payload]: + (write-id ~out ~(coerce-custom-type-id custom-type-id)) + ;; Prefixed [const byte id][cust hash id][payload]: + (do (write-id ~out id-prefixed-custom) + (.writeShort ~out ~(coerce-custom-type-id custom-type-id)))) ~@body))) -(defonce custom-readers (atom {})) ; { (fn [data-input]) ...} +(defonce custom-readers (atom {})) ; { (fn [data-input]) ...} (defmacro extend-thaw - "Extends Nippy to support thawing of a custom type with id ∈[1, 128]: - (extend-thaw 1 [data-input] + "Extends Nippy to support thawing of a custom type with given id: + (extend-thaw :foo/my-type [data-input] ; Keyword id + (->MyType (.readUTF data-input))) + ;; or + (extend-thaw 1 [data-input] ; Byte id (->MyType (.readUTF data-input)))" [custom-type-id [in] & body] - (assert (and (>= custom-type-id 1) (<= custom-type-id 128))) - `(swap! custom-readers assoc ~(int (- custom-type-id)) - (fn [~(with-meta in {:tag 'java.io.DataInput})] - ~@body))) + (assert-custom-type-id custom-type-id) + `(swap! custom-readers assoc + ~(coerce-custom-type-id custom-type-id) + (fn [~(with-meta in {:tag 'java.io.DataInput})] + ~@body))) (comment (defrecord MyType [data]) (extend-freeze MyType 1 [x out] (.writeUTF out (:data x))) diff --git a/test/taoensso/nippy/tests/main.clj b/test/taoensso/nippy/tests/main.clj index 561cd5e..f15c14a 100644 --- a/test/taoensso/nippy/tests/main.clj +++ b/test/taoensso/nippy/tests/main.clj @@ -57,16 +57,22 @@ ;;; Extend to custom Type (defrecord MyType [data]) -(nippy/extend-freeze MyType 1 [x s] (.writeUTF s (:data x))) -(expect Exception (thaw (freeze (->MyType "val")))) +(expect Exception (do (nippy/extend-freeze MyType 1 [x s] (.writeUTF s (:data x))) + (thaw (freeze (->MyType "val"))))) (expect (do (nippy/extend-thaw 1 [s] (->MyType (.readUTF s))) (let [type (->MyType "val")] (= type (thaw (freeze type)))))) ;;; Extend to custom Record (defrecord MyRec [data]) -(expect (do (nippy/extend-freeze MyRec 2 [x s] (.writeUTF s (str "fast-" (:data x)))) +(expect (do (nippy/extend-freeze MyRec 2 [x s] (.writeUTF s (str "foo-" (:data x)))) (nippy/extend-thaw 2 [s] (->MyRec (.readUTF s))) - (= (->MyRec "fast-val") (thaw (freeze (->MyRec "val")))))) + (= (->MyRec "foo-val") (thaw (freeze (->MyRec "val")))))) + +;;; Keyword (prefixed) extensions +(expect + (do (nippy/extend-freeze MyType :nippy-tests/MyType [x s] (.writeUTF s (:data x))) + (nippy/extend-thaw :nippy-tests/MyType [s] (->MyType (.readUTF s))) + (let [type (->MyType "val")] (= type (thaw (freeze type)))))) ;;;; Stable binary representation of vals ; EXPERIMENTAL From 05b424fe33c7ff43aa4d9cdc77fae0075f124285 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sat, 5 Apr 2014 18:14:24 +0700 Subject: [PATCH 12/19] v2.7.0-alpha1 --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 5 +++-- project.clj | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25345db..b255404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## v2.7.0-alpha1 / 2014 Jul 6 + +> **Major release** with significant performance improvements, a new default compression type ([LZ4](http://blog.jpountz.net/post/28092106032/wow-lz4-is-fast)), and better support for a variety of compression/encryption tools. +> +> The data format is fully **backwards-compatible**, the API is backwards compatible **unless** you are using the `:headerless-meta` thaw option. + +### Changes + + * A number of internal performance improvements. + * Added [LZ4](http://blog.jpountz.net/post/28092106032/wow-lz4-is-fast) compressor, **replacing Snappy as the default** (often ~10+% faster with similar compression ratios). **Thanks to [mpenet](https://github.com/mpenet) for his work on this**! + * **BREAKING**: the `thaw` `:headerless-meta` option has been dropped. Its purpose was to provide Nippy v1 compatibility, which is now done automatically. To prevent any surprises, `thaw` calls with this option will now **throw an assertion error**. + * **IMPORTANT**: the `thaw` API has been improved (simplified). The default `:encryptor` and `:compressor` values are now both `:auto`, which'll choose intelligently based on data now included with the Nippy header. Behaviour remains the same for data written without a header: you must specify the correct `:compressor` and `:encryptor` values manually. + * Promoted from Alpha status: `taoensso.nippy.compression` ns, `taoensso.nippy.encryption` ns, `taoensso.nippy.tools` ns, `extend-freeze`, `extend-thaw`. + * All Nippy exceptions are now `ex-info`s. + +### NEW + + * #50: `extend-freeze`, `extend-thaw` can now take arbitrary keyword type ids (see docstrings for more info). + + ## v2.6.3 / 2014 Apr 29 * Fix #48: broken freeze/thaw identity for empty lazy seqs (@vgeshel). diff --git a/README.md b/README.md index b00b106..34aa602 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ **[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contributing](#contact--contributing) | current ([semantic][]) version: ```clojure -[com.taoensso/nippy "2.6.3"] ; Stable (please upgrade from v2.6.0 ASAP) +[com.taoensso/nippy "2.6.3"] ; Stable +[com.taoensso/nippy "2.7.0-alpha1"] ; EXPERIMENTAL! May irrevocably eat your data!! ``` -v2.6 is a **major, backwards-compatible release** with: improved performance (incl. frozen data size), a new low-level DataInput/DataOuput API, improved support for headerless freezing, and 1-to-1 binary-value representation guarantees. See the [CHANGELOG][] for details. +v2.7 is a major, **mostly backwards-compatible** release focused on improved performance and a new default compression scheme (LZ4). See the [CHANGELOG][] for details. Thanks to [mpenet](https://github.com/mpenet) for his work on the LZ4 support! # Nippy, a Clojure serialization library diff --git a/project.clj b/project.clj index 71a82eb..8efe3be 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject com.taoensso/nippy "2.6.3" +(defproject com.taoensso/nippy "2.7.0-alpha1" :author "Peter Taoussanis " :description "Clojure serialization library" :url "https://github.com/ptaoussanis/nippy" From 13cdf06a60dc257a07507920eeb6bf7198b03ea2 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sun, 6 Jul 2014 13:47:38 +0700 Subject: [PATCH 13/19] Make `coerce-custom-type-id` private --- src/taoensso/nippy.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/taoensso/nippy.clj b/src/taoensso/nippy.clj index a1584b5..928cee9 100644 --- a/src/taoensso/nippy.clj +++ b/src/taoensso/nippy.clj @@ -632,7 +632,7 @@ (assert (or (keyword? custom-type-id) (and (integer? custom-type-id) (<= 1 custom-type-id 128))))) -(defn coerce-custom-type-id +(defn- coerce-custom-type-id "* +ive byte id -> -ive byte id (for unprefixed custom types). * Keyword id -> Short hash id (for prefixed custom types)." [custom-type-id] From 7925982939a3510548fd65df24de883d6be83554 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 7 Jul 2014 12:41:16 +0700 Subject: [PATCH 14/19] Print warning when replacing a custom type reader --- src/taoensso/nippy.clj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/taoensso/nippy.clj b/src/taoensso/nippy.clj index 928cee9..418f960 100644 --- a/src/taoensso/nippy.clj +++ b/src/taoensso/nippy.clj @@ -685,6 +685,9 @@ (->MyType (.readUTF data-input)))" [custom-type-id [in] & body] (assert-custom-type-id custom-type-id) + (when (contains? @custom-readers (coerce-custom-type-id custom-type-id)) + (println (format "Warning: resetting Nippy thaw for custom type with id: %s" + custom-type-id))) `(swap! custom-readers assoc ~(coerce-custom-type-id custom-type-id) (fn [~(with-meta in {:tag 'java.io.DataInput})] From d2ddeb846df3a4154cd84c7584cc91694f731a40 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 7 Jul 2014 12:47:38 +0700 Subject: [PATCH 15/19] v2.7.0-SNAPSHOT --- CHANGELOG.md | 7 +++++++ project.clj | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b255404..128d43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v2.7.0-SNAPSHOT / Unreleased + +### Changes + + * `extend-thaw` now prints a warning when replacing a pre-existing type id. + + ## v2.7.0-alpha1 / 2014 Jul 6 > **Major release** with significant performance improvements, a new default compression type ([LZ4](http://blog.jpountz.net/post/28092106032/wow-lz4-is-fast)), and better support for a variety of compression/encryption tools. diff --git a/project.clj b/project.clj index 8efe3be..f7b36f8 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject com.taoensso/nippy "2.7.0-alpha1" +(defproject com.taoensso/nippy "2.7.0-SNAPSHOT" :author "Peter Taoussanis " :description "Clojure serialization library" :url "https://github.com/ptaoussanis/nippy" From 2a13ccfdf792328f1db4f8aea3757ff6f0ce858f Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Sun, 3 Aug 2014 19:17:44 +0700 Subject: [PATCH 16/19] Revert "Temporarily revert `expectations` dep bump due to #40" This reverts commit e17a7f8248db7667ff4123ac6a16120f88dc12fb. Expectations v2.0.8 fixes this issue, Ref. https://github.com/jaycfields/expectations/issues/40#issuecomment-50468973 --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index f7b36f8..3d6b374 100644 --- a/project.clj +++ b/project.clj @@ -24,7 +24,7 @@ :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :test {:jvm-opts ["-Xms1024m" "-Xmx2048m"] - :dependencies [[expectations "1.4.56"] + :dependencies [[expectations "2.0.8"] [org.clojure/test.check "0.5.8"] ;; [com.cemerick/double-check "0.5.7"] [org.clojure/data.fressian "0.2.0"] From 5249f9d06049d8ef35ed8fe83eba96728fbf1016 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Fri, 15 Aug 2014 16:17:09 +0700 Subject: [PATCH 17/19] Switch to Break Versioning --- CHANGELOG.md | 2 ++ README.md | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128d43e..d38b4e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +> This project uses [Break Versioning](https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md) as of **Aug 16, 2014**. + ## v2.7.0-SNAPSHOT / Unreleased ### Changes diff --git a/README.md b/README.md index 34aa602..76272b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contributing](#contact--contributing) | current ([semantic][]) version: +**[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contrib](#contact--contributing) | current [Break Version][]: ```clojure [com.taoensso/nippy "2.6.3"] ; Stable @@ -167,7 +167,8 @@ Copyright © 2012-2014 Peter Taoussanis. Distributed under the [Eclipse Publ [CHANGELOG]: [other Clojure libs]: [Twitter]: -[semantic]: +[SemVer]: +[Break Version]: [Leiningen]: [CDS]: [ClojureWerkz]: From ae42a8f9dee79bc3e23b9b4d86acaefb549d16a8 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Wed, 27 Aug 2014 19:15:39 +0700 Subject: [PATCH 18/19] Bump deps --- project.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project.clj b/project.clj index 3d6b374..ab79f5f 100644 --- a/project.clj +++ b/project.clj @@ -12,8 +12,8 @@ :dependencies [[org.clojure/clojure "1.4.0"] - [org.clojure/tools.reader "0.8.5"] - [com.taoensso/encore "1.7.0"] + [org.clojure/tools.reader "0.8.7"] + [com.taoensso/encore "1.7.1"] [org.iq80.snappy/snappy "0.3"] [org.tukaani/xz "1.5"] [net.jpountz.lz4/lz4 "1.2.0"]] @@ -24,11 +24,11 @@ :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :test {:jvm-opts ["-Xms1024m" "-Xmx2048m"] - :dependencies [[expectations "2.0.8"] - [org.clojure/test.check "0.5.8"] + :dependencies [[expectations "2.0.9"] + [org.clojure/test.check "0.5.9"] ;; [com.cemerick/double-check "0.5.7"] [org.clojure/data.fressian "0.2.0"] - [org.xerial.snappy/snappy-java "1.1.1"]]} + [org.xerial.snappy/snappy-java "1.1.1.3"]]} :dev [:1.6 :test {:plugins [[lein-pprint "1.1.1"] From 814899b0ba333b7617305d65faa94229d69664e5 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Wed, 27 Aug 2014 19:18:33 +0700 Subject: [PATCH 19/19] v2.7.0-RC1 --- CHANGELOG.md | 10 ++-------- README.md | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d38b4e4..a6e7872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,6 @@ > This project uses [Break Versioning](https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md) as of **Aug 16, 2014**. -## v2.7.0-SNAPSHOT / Unreleased - -### Changes - - * `extend-thaw` now prints a warning when replacing a pre-existing type id. - - -## v2.7.0-alpha1 / 2014 Jul 6 +## v2.7.0-RC1 / 2014 Aug 27 > **Major release** with significant performance improvements, a new default compression type ([LZ4](http://blog.jpountz.net/post/28092106032/wow-lz4-is-fast)), and better support for a variety of compression/encryption tools. > @@ -21,6 +14,7 @@ * **IMPORTANT**: the `thaw` API has been improved (simplified). The default `:encryptor` and `:compressor` values are now both `:auto`, which'll choose intelligently based on data now included with the Nippy header. Behaviour remains the same for data written without a header: you must specify the correct `:compressor` and `:encryptor` values manually. * Promoted from Alpha status: `taoensso.nippy.compression` ns, `taoensso.nippy.encryption` ns, `taoensso.nippy.tools` ns, `extend-freeze`, `extend-thaw`. * All Nippy exceptions are now `ex-info`s. + * `extend-thaw` now prints a warning when replacing a pre-existing type id. ### NEW diff --git a/README.md b/README.md index 76272b9..7f155df 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ **[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contrib](#contact--contributing) | current [Break Version][]: ```clojure -[com.taoensso/nippy "2.6.3"] ; Stable -[com.taoensso/nippy "2.7.0-alpha1"] ; EXPERIMENTAL! May irrevocably eat your data!! +[com.taoensso/nippy "2.6.3"] ; Stable +[com.taoensso/nippy "2.7.0-RC1"] ; Development ``` v2.7 is a major, **mostly backwards-compatible** release focused on improved performance and a new default compression scheme (LZ4). See the [CHANGELOG][] for details. Thanks to [mpenet](https://github.com/mpenet) for his work on the LZ4 support!