Compare commits
100 commits
cljdoc-v3.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae4463a85c | ||
|
|
1b99516177 | ||
|
|
e68c2d56fe | ||
|
|
bfba59483a | ||
|
|
f7bb2824ac | ||
|
|
8d62dc2826 | ||
|
|
da57206b0d | ||
|
|
e0f49ced5a | ||
|
|
8d107650cd | ||
|
|
1026ea0ae7 | ||
|
|
c92457025f | ||
|
|
3cb29f3c2e | ||
|
|
a9ea13618c | ||
|
|
d415a2bf72 | ||
|
|
b217db5579 | ||
|
|
9022aad018 | ||
|
|
c0d1da1bb4 | ||
|
|
bb178f66fc | ||
|
|
9b380821cd | ||
|
|
c5209e32ce | ||
|
|
f6240582e1 | ||
|
|
4cb2a14adf | ||
|
|
dc52356106 | ||
|
|
bd4d5205d5 | ||
|
|
229ab94c14 | ||
|
|
535d4e5ab0 | ||
|
|
51298e9252 | ||
|
|
738023764c | ||
|
|
1b05c9b8f9 | ||
|
|
82a050b925 | ||
|
|
37cf415c02 | ||
|
|
92c4a83d61 | ||
|
|
af928ed6a4 | ||
|
|
f749e07eed | ||
|
|
4d96757447 | ||
|
|
03c4cf1784 | ||
|
|
ea7d9ae9de | ||
|
|
cb0b871fe8 | ||
|
|
7be9b4f789 | ||
|
|
cb5b7cf063 | ||
|
|
40143e71ee | ||
|
|
7e84f58ee4 | ||
|
|
b4d161db53 | ||
|
|
578c585bbf | ||
|
|
676898495c | ||
|
|
7d2800d106 | ||
|
|
3c27f03bc4 | ||
|
|
dcc6b081f1 | ||
|
|
f287df9e9c | ||
|
|
9b27a00a59 | ||
|
|
9db09e16a9 | ||
|
|
c2770c6e99 | ||
|
|
ba6477c097 | ||
|
|
9c260b03c4 | ||
|
|
27b3ed958b | ||
|
|
e2a44abf6f | ||
|
|
2900a4d758 | ||
|
|
265b15c94c | ||
|
|
cba055306a | ||
|
|
0d002f8d06 | ||
|
|
d566134da8 | ||
|
|
f3ff7ae8a3 | ||
|
|
fb6f75e4d7 | ||
|
|
e0cd00345d | ||
|
|
99970d5129 | ||
|
|
bcf767332e | ||
|
|
fef079d81d | ||
|
|
6ad5aebd1a | ||
|
|
c8f30e171d | ||
|
|
b28b0ca175 | ||
|
|
d99e0f8541 | ||
|
|
54d179f629 | ||
|
|
01e42d23e6 | ||
|
|
df5a7df91f | ||
|
|
e864294321 | ||
|
|
7953751eba | ||
|
|
8d76d9c350 | ||
|
|
d5a836326a | ||
|
|
ed95701c79 | ||
|
|
ac24f872a8 | ||
|
|
40c1dce6bf | ||
|
|
38d6aab5c1 | ||
|
|
af3957f4db | ||
|
|
79fbf8f3c5 | ||
|
|
1dffa74b8e | ||
|
|
8b7186a930 | ||
|
|
89f98b440f | ||
|
|
60bc4e9976 | ||
|
|
0a9d67084b | ||
|
|
fa1cc66bf3 | ||
|
|
aba153e086 | ||
|
|
d8b1825ea0 | ||
|
|
8778fa49cc | ||
|
|
8e363dbcf9 | ||
|
|
648d5c0232 | ||
|
|
0a15278206 | ||
|
|
621f1189c7 | ||
|
|
129ce952bc | ||
|
|
3ac06b6c87 | ||
|
|
064015e874 |
30 changed files with 3360 additions and 2736 deletions
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
name: build
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 14
|
|
||||||
|
|
||||||
- name: Cache deps
|
|
||||||
uses: actions/cache@v1
|
|
||||||
id: cache-deps
|
|
||||||
with:
|
|
||||||
path: ~/.m2/repository
|
|
||||||
key: ${{ runner.os }}-java14-${{ hashFiles('project.clj') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-java14-
|
|
||||||
|
|
||||||
- name: Run tests for Java 14
|
|
||||||
run: |
|
|
||||||
lein test
|
|
||||||
32
.github/workflows/graal-tests.yml
vendored
Normal file
32
.github/workflows/graal-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
name: Graal tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
java: ['17']
|
||||||
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: graalvm/setup-graalvm@v1
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
components: 'native-image'
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: DeLaGuardo/setup-clojure@12.5
|
||||||
|
with:
|
||||||
|
lein: latest
|
||||||
|
bb: latest
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.m2/repository
|
||||||
|
key: deps-${{ hashFiles('deps.edn') }}
|
||||||
|
restore-keys: deps-
|
||||||
|
|
||||||
|
- run: bb graal-tests
|
||||||
30
.github/workflows/main-tests.yml
vendored
Normal file
30
.github/workflows/main-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
name: Main tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
java: ['17', '19', '21']
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'corretto'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
|
||||||
|
- uses: DeLaGuardo/setup-clojure@12.5
|
||||||
|
with:
|
||||||
|
lein: latest
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
id: cache-deps
|
||||||
|
with:
|
||||||
|
path: ~/.m2/repository
|
||||||
|
key: deps-${{ hashFiles('project.clj') }}
|
||||||
|
restore-keys: deps-
|
||||||
|
|
||||||
|
- run: lein test-all
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,3 +10,4 @@ pom.xml*
|
||||||
/target/
|
/target/
|
||||||
/checkouts/
|
/checkouts/
|
||||||
/logs/
|
/logs/
|
||||||
|
/wiki/.git
|
||||||
|
|
|
||||||
1020
CHANGELOG.md
1020
CHANGELOG.md
File diff suppressed because it is too large
Load diff
286
README.md
286
README.md
|
|
@ -1,230 +1,128 @@
|
||||||
<a href="https://www.taoensso.com" title="More stuff by @ptaoussanis at www.taoensso.com">
|
<a href="https://www.taoensso.com/clojure" title="More stuff by @ptaoussanis at www.taoensso.com"><img src="https://www.taoensso.com/open-source.png" alt="Taoensso open source" width="340"/></a>
|
||||||
<img src="https://www.taoensso.com/taoensso-open-source.png" alt="Taoensso open-source" width="400"/></a>
|
[**API**][cljdoc] | [**Wiki**][GitHub wiki] | [Latest releases](#latest-releases) | [Get support][GitHub issues]
|
||||||
|
|
||||||
**[CHANGELOG]** | [API] | current [Break Version]:
|
# Nippy
|
||||||
|
|
||||||
|
### Fast serialization library for Clojure
|
||||||
|
|
||||||
|
Clojure's rich data types are awesome. And its [reader](https://clojure.org/reference/reader) allows you to take your data just about anywhere. But the reader can be painfully slow when you've got a lot of data to crunch (like when you're serializing to a database).
|
||||||
|
|
||||||
|
Nippy is a mature, high-performance **drop-in alternative to the reader**.
|
||||||
|
|
||||||
|
It is used at scale by [Carmine](https://www.taoensso.com/carmine), [Faraday](https://www.taoensso.com/faraday), [PigPen](https://github.com/Netflix/PigPen), [Onyx](https://github.com/onyx-platform/onyx), [XTDB](https://github.com/xtdb/xtdb), [Datalevin](https://github.com/juji-io/datalevin), and others.
|
||||||
|
|
||||||
|
## Latest release/s
|
||||||
|
|
||||||
|
- `2025-04-15` `v3.5.0`: [release info](../../releases/tag/v3.5.0)
|
||||||
|
|
||||||
|
[![Main tests][Main tests SVG]][Main tests URL]
|
||||||
|
[![Graal tests][Graal tests SVG]][Graal tests URL]
|
||||||
|
|
||||||
|
See [here][GitHub releases] for earlier releases.
|
||||||
|
|
||||||
|
## Why Nippy?
|
||||||
|
|
||||||
|
- Small, simple **pure-Clojure** library
|
||||||
|
- **Terrific performance**: the [best](#performance) for Clojure that I'm aware of
|
||||||
|
- Comprehensive support for [all standard data types](../../wiki/1-Getting-started#deserializing)
|
||||||
|
- Easily extendable to [custom data types](../../wiki/1-Getting-started#custom-types)
|
||||||
|
- **Robust test suite** incl. coverage of every supported type
|
||||||
|
- **Mature** and widely used in production for 12+ years
|
||||||
|
- Optional auto fallback to [Java Serializable](https://taoensso.github.io/nippy/taoensso.nippy.html#var-*freeze-serializable-allowlist*) for [safe](https://cljdoc.org/d/com.taoensso/nippy/CURRENT/api/taoensso.nippy#*freeze-serializable-allowlist*) types
|
||||||
|
- Optional auto fallback to Clojure Reader (including tagged literals)
|
||||||
|
- Optional smart **compression** with [LZ4](https://code.google.com/p/lz4/) or [Zstandard](https://facebook.github.io/zstd/)
|
||||||
|
- Optional [encryption](../../wiki/1-Getting-started#encryption) with AES128
|
||||||
|
- [Tools](https://taoensso.github.io/nippy/taoensso.nippy.tools.html) for easy + robust **integration into 3rd-party libraries**, etc.
|
||||||
|
- Powerful [thaw transducer](https://taoensso.github.io/nippy/taoensso.nippy.html#var-*thaw-xform*) for flexible data inspection and transformation
|
||||||
|
|
||||||
|
## Quick example
|
||||||
|
|
||||||
|
Nippy's super easy to use:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
[com.taoensso/nippy "3.2.0"]
|
(require '[taoensso.nippy :as nippy])
|
||||||
|
|
||||||
|
;; Freeze any Clojure value
|
||||||
|
(nippy/freeze <my-value>) ; => Serialized byte[]
|
||||||
|
|
||||||
|
;; Thaw the byte[] to get back the original value:
|
||||||
|
(nippy/thaw (nippy/freeze <my-value>)) ; => <my-value>
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--  -->
|
See the [wiki](https://github.com/taoensso/nippy/wiki/1-Getting-started#deserializing) for more.
|
||||||
|
|
||||||
> See [here](https://taoensso.com/clojure/backers) if you're interested in helping support my open-source work, thanks! - Peter Taoussanis
|
## Operational considerations
|
||||||
|
|
||||||
## _SECURITY ADVISORY_
|
### Data longevity
|
||||||
|
|
||||||
Users of Nippy <= `v2.15.0-RC1` should **please upgrade ASAP** due to a **Remote Code Execution (RCE) vulnerability** when deserializing data from an **untrusted source**.
|
Nippy is widely used to store **long-lived** data and promises (as always) that **data serialized today should be readable by all future versions of Nippy**.
|
||||||
|
|
||||||
See [here](https://github.com/ptaoussanis/nippy/issues/130) for details, including upgrade instructions.
|
But please note that the **converse is not generally true**:
|
||||||
|
|
||||||
# Nippy: the fastest serialization library for Clojure
|
- Nippy `vX` **should** be able to read all data from Nippy `vY<=X` (backwards compatibility)
|
||||||
|
- Nippy `vX` **may/not** be able to read all data from Nippy `vY>X` (forwards compatibility)
|
||||||
|
|
||||||
Clojure's [rich data types] are *awesome*. And its [reader] allows you to take your data just about anywhere. But the reader can be painfully slow when you've got a lot of data to crunch (like when you're serializing to a database).
|
### Rolling updates and rollback
|
||||||
|
|
||||||
Nippy is an attempt to provide a reliable, high-performance **drop-in alternative to the reader**. Used by the [Carmine Redis client], the [Faraday DynamoDB client], [PigPen], [Onyx] and others.
|
From time to time, Nippy may introduce:
|
||||||
|
|
||||||
## Features
|
- Support for serializing **new types**
|
||||||
* Small, uncomplicated **all-Clojure** library
|
- Optimizations to the serialization of **pre-existing types**
|
||||||
* **Terrific performance** (the fastest for Clojure that I'm aware of)
|
|
||||||
* Comprehesive **support for all standard data types**
|
|
||||||
* **Easily extendable to custom data types** (v2.1+)
|
|
||||||
* Java's **Serializable** fallback when available (v2.5+)
|
|
||||||
* **Reader-fallback** for all other types (including Clojure 1.4+ tagged literals)
|
|
||||||
* **Full test coverage** for every supported type
|
|
||||||
* Fully pluggable **compression**, including built-in high-performance [LZ4] compressor
|
|
||||||
* Fully pluggable **encryption**, including built-in high-strength AES128 enabled with a single `:password [:salted "my-password"]` option (v2+)
|
|
||||||
* Utils for **easy integration into 3rd-party tools/libraries** (v2+)
|
|
||||||
|
|
||||||
## Getting started
|
To help ease **rolling updates** and to better support **rollback**, Nippy (since version v3.4) will always introduce such changes over **two version releases**:
|
||||||
|
|
||||||
Add the necessary dependency to your project:
|
- Release 1: to add **read support** for the new types
|
||||||
|
- Release 2: to add **write support** for the new types
|
||||||
|
|
||||||
```clojure
|
Starting from v3.4, Nippy's release notes will **always clearly indicate** if a particular update sequence is recommended.
|
||||||
Leiningen: [com.taoensso/nippy "3.2.0"] ; or
|
|
||||||
deps.edn: com.taoensso/nippy {:mvn/version "3.2.0"}
|
|
||||||
```
|
|
||||||
|
|
||||||
And setup your namespace imports:
|
### Stability of byte output
|
||||||
|
|
||||||
```clojure
|
It has **never been an objective** of Nippy to offer **predictable byte output**, and I'd generally **recommend against** depending on specific byte output.
|
||||||
(ns my-ns (:require [taoensso.nippy :as nippy]))
|
|
||||||
```
|
|
||||||
|
|
||||||
### De/serializing
|
However, I know that a small minority of users *do* have specialized needs in this area.
|
||||||
|
|
||||||
As an example of what it can do, let's take a look at Nippy's own reference stress data:
|
So starting with Nippy v3.4, Nippy's release notes will **always clearly indicate** if any changes to byte output are expected.
|
||||||
|
|
||||||
```clojure
|
|
||||||
nippy/stress-data
|
|
||||||
=>
|
|
||||||
{:nil nil
|
|
||||||
:true true
|
|
||||||
:false false
|
|
||||||
:boxed-false (Boolean. false)
|
|
||||||
|
|
||||||
:char \ಬ
|
|
||||||
:str-short "ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸ"
|
|
||||||
:str-long (apply str (range 1000))
|
|
||||||
:kw :keyword
|
|
||||||
:kw-ns ::keyword
|
|
||||||
:kw-long (keyword
|
|
||||||
(apply str "kw" (range 1000))
|
|
||||||
(apply str "kw" (range 1000)))
|
|
||||||
|
|
||||||
:sym 'foo
|
|
||||||
:sym-ns 'foo/bar
|
|
||||||
:sym-long (symbol
|
|
||||||
(apply str "sym" (range 1000))
|
|
||||||
(apply str "sym" (range 1000)))
|
|
||||||
|
|
||||||
:regex #"^(https?:)?//(www\?|\?)?"
|
|
||||||
|
|
||||||
:many-small-numbers (vec (range 200))
|
|
||||||
:many-small-keywords (->> (java.util.Locale/getISOLanguages)
|
|
||||||
(mapv keyword))
|
|
||||||
:many-small-strings (->> (java.util.Locale/getISOCountries)
|
|
||||||
(mapv #(.getDisplayCountry (java.util.Locale. "en" %))))
|
|
||||||
|
|
||||||
:queue (enc/queue [:a :b :c :d :e :f :g])
|
|
||||||
:queue-empty (enc/queue)
|
|
||||||
:sorted-set (sorted-set 1 2 3 4 5)
|
|
||||||
:sorted-map (sorted-map :b 2 :a 1 :d 4 :c 3)
|
|
||||||
|
|
||||||
:list (list 1 2 3 4 5 (list 6 7 8 (list 9 10 '(()))))
|
|
||||||
:vector [1 2 3 4 5 [6 7 8 [9 10 [[]]]]]
|
|
||||||
:map {:a 1 :b 2 :c 3 :d {:e 4 :f {:g 5 :h 6 :i 7 :j {{} {}}}}}
|
|
||||||
:set #{1 2 3 4 5 #{6 7 8 #{9 10 #{#{}}}}}
|
|
||||||
:meta (with-meta {:a :A} {:metakey :metaval})
|
|
||||||
:nested [#{{1 [:a :b] 2 [:c :d] 3 [:e :f]} [#{{}}] #{:a :b}}
|
|
||||||
#{{1 [:a :b] 2 [:c :d] 3 [:e :f]} [#{{}}] #{:a :b}}
|
|
||||||
[1 [1 2 [1 2 3 [1 2 3 4 [1 2 3 4 5]]]]]]
|
|
||||||
|
|
||||||
:lazy-seq (repeatedly 1000 rand)
|
|
||||||
:lazy-seq-empty (map identity '())
|
|
||||||
|
|
||||||
:byte (byte 16)
|
|
||||||
:short (short 42)
|
|
||||||
:integer (int 3)
|
|
||||||
:long (long 3)
|
|
||||||
:bigint (bigint 31415926535897932384626433832795)
|
|
||||||
|
|
||||||
:float (float 3.14)
|
|
||||||
:double (double 3.14)
|
|
||||||
:bigdec (bigdec 3.1415926535897932384626433832795)
|
|
||||||
|
|
||||||
:ratio 22/7
|
|
||||||
:uri (URI. "https://clojure.org/reference/data_structures")
|
|
||||||
:uuid (java.util.UUID/randomUUID)
|
|
||||||
:date (java.util.Date.)
|
|
||||||
|
|
||||||
;;; JVM 8+
|
|
||||||
:time-instant (java.time.Instant/now)
|
|
||||||
:time-duration (java.time.Duration/ofSeconds 100 100)
|
|
||||||
:time-period (java.time.Period/of 1 1 1)
|
|
||||||
|
|
||||||
:bytes (byte-array [(byte 1) (byte 2) (byte 3)])
|
|
||||||
:objects (object-array [1 "two" {:data "data"}])
|
|
||||||
|
|
||||||
:stress-record (StressRecord. "data")
|
|
||||||
:stress-type (StressType. "data")
|
|
||||||
|
|
||||||
;; Serializable
|
|
||||||
:throwable (Throwable. "Yolo")
|
|
||||||
:exception (try (/ 1 0) (catch Exception e e))
|
|
||||||
:ex-info (ex-info "ExInfo" {:data "data"})}
|
|
||||||
```
|
|
||||||
|
|
||||||
Serialize it:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(def frozen-stress-data (nippy/freeze nippy/stress-data))
|
|
||||||
=> #<byte[] [B@3253bcf3>
|
|
||||||
```
|
|
||||||
|
|
||||||
Deserialize it:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(nippy/thaw frozen-stress-data)
|
|
||||||
=> {:bytes (byte-array [(byte 1) (byte 2) (byte 3)])
|
|
||||||
:nil nil
|
|
||||||
:boolean true
|
|
||||||
<...> }
|
|
||||||
```
|
|
||||||
|
|
||||||
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 (v2+)
|
|
||||||
|
|
||||||
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
|
|
||||||
(nippy/thaw <encrypted-data> {:password [:salted "my-password"]}) ; Decrypt
|
|
||||||
```
|
|
||||||
|
|
||||||
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` [API] docs for a detailed explanation of why/when you'd want one or the other.
|
|
||||||
|
|
||||||
### Custom types (v2.1+)
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(defrecord MyType [data])
|
|
||||||
|
|
||||||
(nippy/extend-freeze MyType :my-type/foo ; A unique (namespaced) type identifier
|
|
||||||
[x data-output]
|
|
||||||
(.writeUTF data-output (:data x)))
|
|
||||||
|
|
||||||
(nippy/extend-thaw :my-type/foo ; Same type id
|
|
||||||
[data-input]
|
|
||||||
(MyType. (.readUTF data-input)))
|
|
||||||
|
|
||||||
(nippy/thaw (nippy/freeze (MyType. "Joe"))) => #taoensso.nippy.MyType{:data "Joe"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
Nippy is currently the **fastest serialization library for Clojure** that I'm aware of, and offers roundtrip times between **~10x and ~15x** faster than Clojure's `tools.reader.edn`, with a **~40% smaller output size**.
|
Nippy is fast! Latest [benchmark](../../blob/master/test/taoensso/nippy_benchmarks.clj) results:
|
||||||
|
|
||||||
![benchmarks-png]
|

|
||||||
|
|
||||||
[Detailed benchmark info] is available on Google Docs.
|
PRs welcome to include other alternatives in the bench suite!
|
||||||
|
|
||||||
## Contacting me / contributions
|
## Documentation
|
||||||
|
|
||||||
Please use the project's [GitHub issues page] for all questions, ideas, etc. **Pull requests welcome**. See the project's [GitHub contributors page] for a list of contributors.
|
- [Wiki][GitHub wiki] (getting started, usage, etc.)
|
||||||
|
- API reference via [cljdoc][cljdoc]
|
||||||
|
|
||||||
Otherwise, you can reach me at [Taoensso.com]. Happy hacking!
|
## Funding
|
||||||
|
|
||||||
\- [Peter Taoussanis]
|
You can [help support][sponsor] continued work on this project, thank you!! 🙏
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the [EPL v1.0] \(same as Clojure).
|
Copyright © 2012-2025 [Peter Taoussanis][].
|
||||||
Copyright © 2012-2022 [Peter Taoussanis].
|
Licensed under [EPL 1.0](LICENSE.txt) (same as Clojure).
|
||||||
|
|
||||||
|
<!-- Common -->
|
||||||
|
|
||||||
|
[GitHub releases]: ../../releases
|
||||||
|
[GitHub issues]: ../../issues
|
||||||
|
[GitHub wiki]: ../../wiki
|
||||||
|
|
||||||
<!--- Standard links -->
|
|
||||||
[Taoensso.com]: https://www.taoensso.com
|
|
||||||
[Peter Taoussanis]: https://www.taoensso.com
|
[Peter Taoussanis]: https://www.taoensso.com
|
||||||
[@ptaoussanis]: https://www.taoensso.com
|
[sponsor]: https://www.taoensso.com/sponsor
|
||||||
[More by @ptaoussanis]: https://www.taoensso.com
|
|
||||||
[Break Version]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md
|
|
||||||
|
|
||||||
<!--- Standard links (repo specific) -->
|
<!-- Project -->
|
||||||
[CHANGELOG]: https://github.com/ptaoussanis/nippy/releases
|
|
||||||
[API]: http://ptaoussanis.github.io/nippy/
|
|
||||||
[GitHub issues page]: https://github.com/ptaoussanis/nippy/issues
|
|
||||||
[GitHub contributors page]: https://github.com/ptaoussanis/nippy/graphs/contributors
|
|
||||||
[EPL v1.0]: https://raw.githubusercontent.com/ptaoussanis/nippy/master/LICENSE
|
|
||||||
[Hero]: https://raw.githubusercontent.com/ptaoussanis/nippy/master/hero.png "Title"
|
|
||||||
|
|
||||||
<!--- Unique links -->
|
[cljdoc]: https://cljdoc.org/d/com.taoensso/nippy/CURRENT/api/taoensso.nippy
|
||||||
[rich data types]: http://clojure.org/reference/datatypes
|
|
||||||
[reader]: http://clojure.org/reference/reader
|
[Clojars SVG]: https://img.shields.io/clojars/v/com.taoensso/nippy.svg
|
||||||
[Carmine Redis client]: https://github.com/ptaoussanis/carmine
|
[Clojars URL]: https://clojars.org/com.taoensso/nippy
|
||||||
[Faraday DynamoDB client]: https://github.com/ptaoussanis/faraday
|
|
||||||
[PigPen]: https://github.com/Netflix/PigPen
|
[Main tests SVG]: https://github.com/taoensso/nippy/actions/workflows/main-tests.yml/badge.svg
|
||||||
[Onyx]: https://github.com/onyx-platform/onyx
|
[Main tests URL]: https://github.com/taoensso/nippy/actions/workflows/main-tests.yml
|
||||||
[LZ4]: https://code.google.com/p/lz4/
|
[Graal tests SVG]: https://github.com/taoensso/nippy/actions/workflows/graal-tests.yml/badge.svg
|
||||||
[benchmarks-png]: https://github.com/ptaoussanis/nippy/raw/master/benchmarks.png
|
[Graal tests URL]: https://github.com/taoensso/nippy/actions/workflows/graal-tests.yml
|
||||||
[Detailed benchmark info]: https://docs.google.com/spreadsheet/ccc?key=0AuSXb68FH4uhdE5kTTlocGZKSXppWG9sRzA5Y2pMVkE
|
|
||||||
|
|
|
||||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Security policy
|
||||||
|
|
||||||
|
## Advisories
|
||||||
|
|
||||||
|
All security advisories will be posted [on GitHub](https://github.com/taoensso/nippy/security/advisories).
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Please report possible security vulnerabilities [via GitHub](https://github.com/taoensso/nippy/security/advisories), or by emailing me at `my first name at taoensso.com`. You may encrypt emails with [my public PGP/GPG key](https://www.taoensso.com/pgp).
|
||||||
|
|
||||||
|
Thank you!
|
||||||
|
|
||||||
|
\- [Peter Taoussanis](https://www.taoensso.com)
|
||||||
10
bb.edn
Normal file
10
bb.edn
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{:paths ["bb"]
|
||||||
|
:tasks
|
||||||
|
{:requires ([graal-tests])
|
||||||
|
graal-tests
|
||||||
|
{:doc "Run Graal native-image tests"
|
||||||
|
:task
|
||||||
|
(do
|
||||||
|
(graal-tests/uberjar)
|
||||||
|
(graal-tests/native-image)
|
||||||
|
(graal-tests/run-tests))}}}
|
||||||
38
bb/graal_tests.clj
Executable file
38
bb/graal_tests.clj
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bb
|
||||||
|
|
||||||
|
(ns graal-tests
|
||||||
|
(:require
|
||||||
|
[clojure.string :as str]
|
||||||
|
[babashka.fs :as fs]
|
||||||
|
[babashka.process :refer [shell]]))
|
||||||
|
|
||||||
|
(defn uberjar []
|
||||||
|
(let [command "lein with-profiles +graal-tests uberjar"
|
||||||
|
command
|
||||||
|
(if (fs/windows?)
|
||||||
|
(if (fs/which "lein")
|
||||||
|
command
|
||||||
|
;; Assume PowerShell powershell module
|
||||||
|
(str "powershell.exe -command " (pr-str command)))
|
||||||
|
command)]
|
||||||
|
|
||||||
|
(shell command)))
|
||||||
|
|
||||||
|
(defn executable [dir name]
|
||||||
|
(-> (fs/glob dir (if (fs/windows?) (str name ".{exe,bat,cmd}") name))
|
||||||
|
first
|
||||||
|
fs/canonicalize
|
||||||
|
str))
|
||||||
|
|
||||||
|
(defn native-image []
|
||||||
|
(let [graalvm-home (System/getenv "GRAALVM_HOME")
|
||||||
|
bin-dir (str (fs/file graalvm-home "bin"))]
|
||||||
|
(shell (executable bin-dir "gu") "install" "native-image")
|
||||||
|
(shell (executable bin-dir "native-image")
|
||||||
|
"--features=clj_easy.graal_build_time.InitClojureClasses"
|
||||||
|
"--no-fallback" "-jar" "target/graal-tests.jar" "graal_tests")))
|
||||||
|
|
||||||
|
(defn run-tests []
|
||||||
|
(let [{:keys [out]} (shell {:out :string} (executable "." "graal_tests"))]
|
||||||
|
(assert (str/includes? out "loaded") out)
|
||||||
|
(println "Native image works!")))
|
||||||
BIN
benchmarks.png
BIN
benchmarks.png
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 68 KiB |
91
project.clj
91
project.clj
|
|
@ -1,59 +1,64 @@
|
||||||
(defproject com.taoensso/nippy "3.2.0"
|
(defproject com.taoensso/nippy "3.5.0"
|
||||||
:author "Peter Taoussanis <https://www.taoensso.com>"
|
:author "Peter Taoussanis <https://www.taoensso.com>"
|
||||||
:description "High-performance serialization library for Clojure"
|
:description "Fast serialization library for Clojure"
|
||||||
:url "https://github.com/ptaoussanis/nippy"
|
:url "https://www.taoensso.com/nippy"
|
||||||
:license {:name "Eclipse Public License"
|
|
||||||
:url "http://www.eclipse.org/legal/epl-v10.html"
|
:license
|
||||||
:distribution :repo
|
{:name "Eclipse Public License - v 1.0"
|
||||||
:comments "Same as Clojure"}
|
:url "https://www.eclipse.org/legal/epl-v10.html"}
|
||||||
:min-lein-version "2.3.3"
|
|
||||||
:global-vars {*warn-on-reflection* true
|
|
||||||
*assert* true
|
|
||||||
;; *unchecked-math* :warn-on-boxed
|
|
||||||
}
|
|
||||||
|
|
||||||
:dependencies
|
:dependencies
|
||||||
[[org.clojure/tools.reader "1.3.6"]
|
[[org.clojure/tools.reader "1.5.2"]
|
||||||
[com.taoensso/encore "3.23.0"]
|
[com.taoensso/encore "3.142.0"]
|
||||||
[org.iq80.snappy/snappy "0.4"]
|
[org.tukaani/xz "1.10"]
|
||||||
[org.tukaani/xz "1.9"]
|
[io.airlift/aircompressor "2.0.2"]]
|
||||||
[org.lz4/lz4-java "1.8.0"]]
|
|
||||||
|
|
||||||
:resources
|
:test-paths ["test" #_"src"]
|
||||||
["resources"]
|
|
||||||
|
|
||||||
:plugins
|
|
||||||
[[lein-pprint "1.3.2"]
|
|
||||||
[lein-ancient "0.7.0"]
|
|
||||||
[lein-codox "0.10.8"]]
|
|
||||||
|
|
||||||
:profiles
|
:profiles
|
||||||
{;; :default [:base :system :user :provided :dev]
|
{;; :default [:base :system :user :provided :dev]
|
||||||
:server-jvm {:jvm-opts ^:replace ["-server" "-Xms1024m" "-Xmx2048m"]}
|
:provided {:dependencies [[org.clojure/clojure "1.11.4"]]}
|
||||||
:provided {:dependencies [[org.clojure/clojure "1.7.0"]]}
|
:c1.12 {:dependencies [[org.clojure/clojure "1.12.0"]]}
|
||||||
:1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}
|
:c1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]}
|
||||||
:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
|
:c1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}
|
||||||
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
|
|
||||||
:1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
|
:graal-tests
|
||||||
:1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]}
|
{:source-paths ["test"]
|
||||||
:depr {:jvm-opts ["-Dtaoensso.elide-deprecated=true"]}
|
:main taoensso.graal-tests
|
||||||
:test
|
:aot [taoensso.graal-tests]
|
||||||
|
:uberjar-name "graal-tests.jar"
|
||||||
|
:dependencies
|
||||||
|
[[org.clojure/clojure "1.11.3"]
|
||||||
|
[com.github.clj-easy/graal-build-time "1.0.5"]]}
|
||||||
|
|
||||||
|
:dev
|
||||||
{:jvm-opts
|
{:jvm-opts
|
||||||
["-Xms1024m" "-Xmx2048m"
|
["-server"
|
||||||
|
"-Xms1024m" "-Xmx2048m"
|
||||||
|
"-Dtaoensso.elide-deprecated=true"
|
||||||
"-Dtaoensso.nippy.thaw-serializable-allowlist-base=base.1, base.2"
|
"-Dtaoensso.nippy.thaw-serializable-allowlist-base=base.1, base.2"
|
||||||
"-Dtaoensso.nippy.thaw-serializable-allowlist-add=add.1 , add.2"]
|
"-Dtaoensso.nippy.thaw-serializable-allowlist-add=add.1 , add.2"
|
||||||
|
#_"-Dtaoensso.nippy.target-release=320"
|
||||||
|
#_"-Dtaoensso.nippy.target-release=350"]
|
||||||
|
|
||||||
|
:global-vars
|
||||||
|
{*warn-on-reflection* true
|
||||||
|
*assert* true
|
||||||
|
*unchecked-math* false #_:warn-on-boxed}
|
||||||
|
|
||||||
:dependencies
|
:dependencies
|
||||||
[[org.clojure/test.check "1.1.1"]
|
[[org.clojure/test.check "1.1.1"]
|
||||||
[org.clojure/data.fressian "1.0.0"]
|
[org.clojure/data.fressian "1.1.0"]]
|
||||||
[org.xerial.snappy/snappy-java "1.1.8.4"]]}
|
|
||||||
|
|
||||||
:dev [:1.11 :test :server-jvm :depr]}
|
:plugins
|
||||||
|
[[lein-pprint "1.3.2"]
|
||||||
|
[lein-ancient "0.7.0"]]}}
|
||||||
|
|
||||||
:aliases
|
:aliases
|
||||||
{"start-dev" ["with-profile" "+dev" "repl" ":headless"]
|
{"start-dev" ["with-profile" "+dev" "repl" ":headless"]
|
||||||
"deploy-lib" ["do" "deploy" "clojars," "install"]
|
;; "build-once" ["do" ["clean"] ["cljsbuild" "once"]]
|
||||||
"test-all" ["with-profile" "+1.10:+1.9:+1.8:+1.7" "test"]}
|
"deploy-lib" ["do" #_["build-once"] ["deploy" "clojars"] ["install"]]
|
||||||
|
|
||||||
:repositories
|
"test-clj" ["with-profile" "+c1.12:+c1.11:+c1.10" "test"]
|
||||||
{"sonatype-oss-public"
|
;; "test-cljs" ["with-profile" "+c1.12" "cljsbuild" "test"]
|
||||||
"https://oss.sonatype.org/content/groups/public/"})
|
"test-all" ["do" ["clean"] ["test-clj"] #_["test-cljs"]]})
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,167 +0,0 @@
|
||||||
(ns taoensso.nippy.benchmarks
|
|
||||||
(:require [clojure.data.fressian :as fressian]
|
|
||||||
[taoensso.encore :as enc]
|
|
||||||
[taoensso.nippy :as nippy :refer [freeze thaw]]))
|
|
||||||
|
|
||||||
(def data #_22 nippy/stress-data-benchable)
|
|
||||||
|
|
||||||
(defn fressian-freeze [value]
|
|
||||||
(let [^java.nio.ByteBuffer bb (fressian/write value)
|
|
||||||
len (.remaining bb)
|
|
||||||
ba (byte-array len)]
|
|
||||||
(.get bb ba 0 len)
|
|
||||||
ba))
|
|
||||||
|
|
||||||
(defn fressian-thaw [value]
|
|
||||||
(let [bb (java.nio.ByteBuffer/wrap value)]
|
|
||||||
(fressian/read bb)))
|
|
||||||
|
|
||||||
(comment (fressian-thaw (fressian-freeze data)))
|
|
||||||
|
|
||||||
(defmacro bench* [& body] `(enc/bench 10000 {:warmup-laps 25000} ~@body))
|
|
||||||
(defn bench1 [freezer thawer & [sizer]]
|
|
||||||
(let [data-frozen (freezer data)
|
|
||||||
time-freeze (bench* (freezer data))
|
|
||||||
time-thaw (bench* (thawer data-frozen))]
|
|
||||||
{:round (+ time-freeze time-thaw)
|
|
||||||
:freeze time-freeze
|
|
||||||
:thaw time-thaw
|
|
||||||
:size ((or sizer count) data-frozen)}))
|
|
||||||
|
|
||||||
(defn bench [{:keys [reader? lzma2? fressian? laps] :or {laps 1}}]
|
|
||||||
(println "\nBenching (this can take some time)")
|
|
||||||
(println "----------------------------------")
|
|
||||||
(dotimes [l laps]
|
|
||||||
(println (str "\nLap " (inc l) "/" laps "..."))
|
|
||||||
|
|
||||||
(when reader? ; Slow
|
|
||||||
(println {:reader (bench1 enc/pr-edn enc/read-edn
|
|
||||||
#(count (.getBytes ^String % "UTF-8")))}))
|
|
||||||
|
|
||||||
(when lzma2? ; Slow
|
|
||||||
(println {:lzma2 (bench1 #(freeze % {:compressor nippy/lzma2-compressor})
|
|
||||||
#(thaw % {:compressor nippy/lzma2-compressor}))}))
|
|
||||||
|
|
||||||
(when fressian?
|
|
||||||
(println {:fressian (bench1 fressian-freeze fressian-thaw)}))
|
|
||||||
|
|
||||||
(println {:encrypted (bench1 #(freeze % {:password [:cached "p"]})
|
|
||||||
#(thaw % {:password [:cached "p"]}))})
|
|
||||||
(println {:default (bench1 #(freeze % {})
|
|
||||||
#(thaw % {}))})
|
|
||||||
(println {:fast (bench1 nippy/fast-freeze nippy/fast-thaw)}))
|
|
||||||
|
|
||||||
(println "\nDone! (Time for cake?)")
|
|
||||||
true)
|
|
||||||
|
|
||||||
(comment (enc/read-edn (enc/pr-edn data))
|
|
||||||
(bench1 fressian-freeze fressian-thaw))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(set! *unchecked-math* false)
|
|
||||||
;; (bench {:reader? true :lzma2? true :fressian? true :laps 2})
|
|
||||||
;; (bench {:laps 2})
|
|
||||||
|
|
||||||
;;; 2016 Jul 17, v2.12.0-RC2, minor final optimizations
|
|
||||||
{:encrypted {:round 4527, :freeze 2651, :thaw 1876, :size 16324}}
|
|
||||||
{:default {:round 3998, :freeze 2226, :thaw 1772, :size 16297}}
|
|
||||||
{:fast {:round 3408, :freeze 1745, :thaw 1663, :size 17069}}
|
|
||||||
|
|
||||||
;;; 2016 Apr 14, v2.12.0-SNAPSHOT, refactor + larger data + new hardware
|
|
||||||
{:reader {:round 52380, :freeze 17817, :thaw 34563, :size 27861}}
|
|
||||||
{:lzma2 {:round 43321, :freeze 28312, :thaw 15009, :size 11260}}
|
|
||||||
{:fressian {:round 6911, :freeze 5109, :thaw 1802, :size 17105}}
|
|
||||||
{:encrypted {:round 4726, :freeze 2951, :thaw 1775, :size 16308}}
|
|
||||||
{:default {:round 4299, :freeze 2655, :thaw 1644, :size 16278}}
|
|
||||||
{:fast {:round 3739, :freeze 2159, :thaw 1580, :size 17069}}
|
|
||||||
;; 12.184228890439638 :default
|
|
||||||
;; 14.009093340465364 :fast
|
|
||||||
|
|
||||||
;;; 2015 Oct 6, v2.11.0-alpha4
|
|
||||||
{:reader {:round 73409, :freeze 21823, :thaw 51586, :size 27672}}
|
|
||||||
{:lzma2 {:round 56689, :freeze 37222, :thaw 19467, :size 11252}}
|
|
||||||
{:fressian {:round 10666, :freeze 7737, :thaw 2929, :size 16985}}
|
|
||||||
{:encrypted {:round 6885, :freeze 4227, :thaw 2658, :size 16148}}
|
|
||||||
{:default {:round 6304, :freeze 3824, :thaw 2480, :size 16122}}
|
|
||||||
{:fast1 {:round 5352, :freeze 3272, :thaw 2080, :size 16976}}
|
|
||||||
{:fast2 {:round 5243, :freeze 3238, :thaw 2005, :size 16972}}
|
|
||||||
;; :reader/:default ratio: 11.64
|
|
||||||
;;
|
|
||||||
{:reader {:round 26, :freeze 17, :thaw 9, :size 2}}
|
|
||||||
{:lzma2 {:round 3648, :freeze 3150, :thaw 498, :size 68}}
|
|
||||||
{:fressian {:round 19, :freeze 7, :thaw 12, :size 1}}
|
|
||||||
{:encrypted {:round 63, :freeze 40, :thaw 23, :size 36}}
|
|
||||||
{:default {:round 24, :freeze 17, :thaw 7, :size 6}}
|
|
||||||
{:fast1 {:round 19, :freeze 12, :thaw 7, :size 6}}
|
|
||||||
{:fast2 {:round 4, :freeze 2, :thaw 2, :size 2}}
|
|
||||||
|
|
||||||
;;; 2015 Sep 29, after read/write API refactor
|
|
||||||
{:lzma2 {:round 51640, :freeze 33699, :thaw 17941, :size 11240}}
|
|
||||||
{:encrypted {:round 5922, :freeze 3734, :thaw 2188, :size 16132}}
|
|
||||||
{:default {:round 5588, :freeze 3658, :thaw 1930, :size 16113}}
|
|
||||||
{:fast {:round 4533, :freeze 2688, :thaw 1845, :size 16972}}
|
|
||||||
|
|
||||||
;;; 2015 Sep 28, small collection optimizations
|
|
||||||
{:lzma2 {:round 56307, :freeze 36475, :thaw 19832, :size 11244}}
|
|
||||||
{:encrypted {:round 6062, :freeze 3802, :thaw 2260, :size 16148}}
|
|
||||||
{:default {:round 5482, :freeze 3382, :thaw 2100, :size 16128}}
|
|
||||||
{:fast {:round 4729, :freeze 2826, :thaw 1903, :size 16972}}
|
|
||||||
|
|
||||||
;;; 2015 Sep 29, various micro optimizations (incl. &arg elimination)
|
|
||||||
{:reader {:round 63547, :freeze 19374, :thaw 44173, :size 27717}}
|
|
||||||
{:lzma2 {:round 51724, :freeze 33502, :thaw 18222, :size 11248}}
|
|
||||||
{:fressian {:round 8813, :freeze 6460, :thaw 2353, :size 16985}}
|
|
||||||
{:encrypted {:round 6005, :freeze 3768, :thaw 2237, :size 16164}}
|
|
||||||
{:default {:round 5417, :freeze 3354, :thaw 2063, :size 16145}}
|
|
||||||
{:fast {:round 4659, :freeze 2712, :thaw 1947, :size 17026}}
|
|
||||||
|
|
||||||
;;; 2015 Sep 15 - v2.10.0-alpha6, Clojure 1.7.0
|
|
||||||
{:reader {:round 94901, :freeze 25781, :thaw 69120, :size 27686}}
|
|
||||||
{:lzma2 {:round 65127, :freeze 43150, :thaw 21977, :size 11244}}
|
|
||||||
{:encrypted {:round 12590, :freeze 7565, :thaw 5025, :size 16148}}
|
|
||||||
{:fressian {:round 12085, :freeze 9168, :thaw 2917, :size 16972}}
|
|
||||||
{:default {:round 6974, :freeze 4582, :thaw 2392, :size 16123}}
|
|
||||||
{:fast {:round 6255, :freeze 3724, :thaw 2531, :size 17013}}
|
|
||||||
|
|
||||||
;;; 2015 Sep 14 - v2.10.0-alpha5, Clojure 1.7.0-RC1
|
|
||||||
{:default {:round 6870, :freeze 4376, :thaw 2494, :size 16227}}
|
|
||||||
{:fast {:round 6104, :freeze 3743, :thaw 2361, :size 17013}}
|
|
||||||
{:encrypted {:round 12155, :freeze 6908, :thaw 5247, :size 16244}}
|
|
||||||
|
|
||||||
;;; 2015 June 4 - v2.9.0, Clojure 1.7.0-RC1
|
|
||||||
{:reader {:round 155353, :freeze 44192, :thaw 111161, :size 27693}}
|
|
||||||
{:lzma2 {:round 102484, :freeze 68274, :thaw 34210, :size 11240}}
|
|
||||||
{:fressian {:round 44665, :freeze 34996, :thaw 9669, :size 16972}}
|
|
||||||
{:encrypted {:round 19791, :freeze 11354, :thaw 8437, :size 16148}}
|
|
||||||
{:default {:round 12302, :freeze 8310, :thaw 3992, :size 16126}}
|
|
||||||
{:fast {:round 9802, :freeze 5944, :thaw 3858, :size 17013}}
|
|
||||||
|
|
||||||
;;; 2015 Apr 17 w/ smart compressor selection, Clojure 1.7.0-beta1
|
|
||||||
{:default {:round 6163, :freeze 4095, :thaw 2068, :size 16121}}
|
|
||||||
{:fast {:round 5417, :freeze 3480, :thaw 1937, :size 17013}}
|
|
||||||
{:encrypted {:round 10950, :freeze 6400, :thaw 4550, :size 16148}}
|
|
||||||
|
|
||||||
;;; 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 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}}
|
|
||||||
{: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}})
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
(ns taoensso.nippy.compression
|
(ns ^:no-doc taoensso.nippy.compression
|
||||||
(:require [taoensso.encore :as enc])
|
"Private, implementation detail."
|
||||||
(:import [java.io ByteArrayInputStream ByteArrayOutputStream DataInputStream
|
(:require
|
||||||
DataOutputStream]))
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc])
|
||||||
|
(:import
|
||||||
|
[java.nio ByteBuffer]
|
||||||
|
[java.io
|
||||||
|
ByteArrayInputStream ByteArrayOutputStream
|
||||||
|
DataInputStream DataOutputStream]))
|
||||||
|
|
||||||
;;;; Interface
|
;;;; Interface
|
||||||
|
|
||||||
|
|
@ -10,24 +16,120 @@
|
||||||
(compress ^bytes [compressor ba])
|
(compress ^bytes [compressor ba])
|
||||||
(decompress ^bytes [compressor ba]))
|
(decompress ^bytes [compressor ba]))
|
||||||
|
|
||||||
;;;; Default implementations
|
(def ^:const standard-header-ids
|
||||||
|
"These support `:auto` thaw."
|
||||||
|
#{:zstd :lz4 #_:lzo :lzma2 :snappy})
|
||||||
|
|
||||||
(def standard-header-ids "These'll support :auto thaw" #{:snappy :lzma2 :lz4})
|
;;;; Misc utils
|
||||||
|
|
||||||
(deftype SnappyCompressor []
|
(defn- int-size->ba ^bytes [size]
|
||||||
|
(let [ba (byte-array 4)
|
||||||
|
baos (ByteArrayOutputStream. 4)
|
||||||
|
dos (DataOutputStream. baos)]
|
||||||
|
(.writeInt dos (int size))
|
||||||
|
(.toByteArray baos)))
|
||||||
|
|
||||||
|
(defn- ba->int-size [ba]
|
||||||
|
(let [bais (ByteArrayInputStream. ba)
|
||||||
|
dis (DataInputStream. bais)]
|
||||||
|
(.readInt dis)))
|
||||||
|
|
||||||
|
(comment (ba->int-size (int-size->ba 3737)))
|
||||||
|
|
||||||
|
;;;; Airlift
|
||||||
|
|
||||||
|
(defn- airlift-compress
|
||||||
|
^bytes [^io.airlift.compress.Compressor c ^bytes ba prepend-size?]
|
||||||
|
(let [in-len (alength ba)
|
||||||
|
max-out-len (.maxCompressedLength c in-len)]
|
||||||
|
|
||||||
|
(if prepend-size?
|
||||||
|
(let [ba-max-out (byte-array (int (+ 4 max-out-len)))
|
||||||
|
int-size-ba (int-size->ba in-len)
|
||||||
|
_ (System/arraycopy int-size-ba 0 ba-max-out 0 4)
|
||||||
|
out-len
|
||||||
|
(.compress c
|
||||||
|
ba 0 in-len
|
||||||
|
ba-max-out 4 max-out-len)]
|
||||||
|
|
||||||
|
(if (== out-len max-out-len)
|
||||||
|
(do ba-max-out)
|
||||||
|
(java.util.Arrays/copyOfRange ba-max-out 0 (+ 4 out-len))))
|
||||||
|
|
||||||
|
(let [ba-max-out (byte-array max-out-len)
|
||||||
|
out-len
|
||||||
|
(.compress c
|
||||||
|
ba 0 in-len
|
||||||
|
ba-max-out 0 max-out-len)]
|
||||||
|
|
||||||
|
(if (== out-len max-out-len)
|
||||||
|
(do ba-max-out)
|
||||||
|
(java.util.Arrays/copyOfRange ba-max-out 0 out-len))))))
|
||||||
|
|
||||||
|
(defn- airlift-decompress
|
||||||
|
^bytes [^io.airlift.compress.Decompressor d ^bytes ba max-out-len]
|
||||||
|
(if max-out-len
|
||||||
|
(let [max-out-len (int max-out-len)
|
||||||
|
ba-max-out (byte-array max-out-len)
|
||||||
|
out-len
|
||||||
|
(.decompress d
|
||||||
|
ba 0 (alength ba)
|
||||||
|
ba-max-out 0 max-out-len)]
|
||||||
|
|
||||||
|
(if (== out-len max-out-len)
|
||||||
|
(do ba-max-out)
|
||||||
|
(java.util.Arrays/copyOfRange ba-max-out 0 out-len)))
|
||||||
|
|
||||||
|
;; Prepended size
|
||||||
|
(let [out-len (ba->int-size ba)
|
||||||
|
ba-out (byte-array (int out-len))]
|
||||||
|
(.decompress d
|
||||||
|
ba 4 (- (alength ba) 4)
|
||||||
|
ba-out 0 out-len)
|
||||||
|
ba-out)))
|
||||||
|
|
||||||
|
(do
|
||||||
|
(enc/def* ^:private airlift-zstd-compressor_ (enc/thread-local (io.airlift.compress.zstd.ZstdCompressor.)))
|
||||||
|
(enc/def* ^:private airlift-zstd-decompressor_ (enc/thread-local (io.airlift.compress.zstd.ZstdDecompressor.)))
|
||||||
|
(deftype ZstdCompressor [prepend-size?]
|
||||||
|
ICompressor
|
||||||
|
(header-id [_] :zstd)
|
||||||
|
(compress [_ ba] (airlift-compress @airlift-zstd-compressor_ ba prepend-size?))
|
||||||
|
(decompress [_ ba] (airlift-decompress @airlift-zstd-decompressor_ ba
|
||||||
|
(when-not prepend-size?
|
||||||
|
(io.airlift.compress.zstd.ZstdDecompressor/getDecompressedSize ba
|
||||||
|
0 (alength ^bytes ba)))))))
|
||||||
|
|
||||||
|
(do
|
||||||
|
(enc/def* ^:private airlift-lz4-compressor_ (enc/thread-local (io.airlift.compress.lz4.Lz4Compressor.)))
|
||||||
|
(enc/def* ^:private airlift-lz4-decompressor_ (enc/thread-local (io.airlift.compress.lz4.Lz4Decompressor.)))
|
||||||
|
(deftype LZ4Compressor []
|
||||||
|
ICompressor
|
||||||
|
(header-id [_] :lz4)
|
||||||
|
(compress [_ ba] (airlift-compress @airlift-lz4-compressor_ ba true))
|
||||||
|
(decompress [_ ba] (airlift-decompress @airlift-lz4-decompressor_ ba nil))))
|
||||||
|
|
||||||
|
(do
|
||||||
|
(enc/def* ^:private airlift-lzo-compressor_ (enc/thread-local (io.airlift.compress.lzo.LzoCompressor.)))
|
||||||
|
(enc/def* ^:private airlift-lzo-decompressor_ (enc/thread-local (io.airlift.compress.lzo.LzoDecompressor.)))
|
||||||
|
(deftype LZOCompressor []
|
||||||
|
ICompressor
|
||||||
|
(header-id [_] :lzo)
|
||||||
|
(compress [_ ba] (airlift-compress @airlift-lzo-compressor_ ba true))
|
||||||
|
(decompress [_ ba] (airlift-decompress @airlift-lzo-decompressor_ ba nil))))
|
||||||
|
|
||||||
|
(do
|
||||||
|
(enc/def* ^:private airlift-snappy-compressor_ (enc/thread-local (io.airlift.compress.snappy.SnappyCompressor.)))
|
||||||
|
(enc/def* ^:private airlift-snappy-decompressor_ (enc/thread-local (io.airlift.compress.snappy.SnappyDecompressor.)))
|
||||||
|
(deftype SnappyCompressor [prepend-size?]
|
||||||
ICompressor
|
ICompressor
|
||||||
(header-id [_] :snappy)
|
(header-id [_] :snappy)
|
||||||
(compress [_ ba] (org.iq80.snappy.Snappy/compress ba))
|
(compress [_ ba] (airlift-compress @airlift-snappy-compressor_ ba prepend-size?))
|
||||||
(decompress [_ ba] (org.iq80.snappy.Snappy/uncompress ba 0 (alength ^bytes ba))))
|
(decompress [_ ba] (airlift-decompress @airlift-snappy-decompressor_ ba
|
||||||
|
(when-not prepend-size?
|
||||||
|
(io.airlift.compress.snappy.SnappyDecompressor/getUncompressedLength ba 0))))))
|
||||||
|
|
||||||
(def snappy-compressor
|
;;;; LZMA2
|
||||||
"Default org.iq80.snappy.Snappy compressor:
|
|
||||||
Ratio: low.
|
|
||||||
Write speed: very high.
|
|
||||||
Read speed: very high.
|
|
||||||
|
|
||||||
A good general-purpose compressor."
|
|
||||||
(SnappyCompressor.))
|
|
||||||
|
|
||||||
(deftype LZMA2Compressor [compression-level]
|
(deftype LZMA2Compressor [compression-level]
|
||||||
;; Compression level ∈ℕ[0,9] (low->high) with 6 LZMA2 default (we use 0)
|
;; Compression level ∈ℕ[0,9] (low->high) with 6 LZMA2 default (we use 0)
|
||||||
|
|
@ -56,86 +158,61 @@
|
||||||
(.read xzs ba 0 len-decomp)
|
(.read xzs ba 0 len-decomp)
|
||||||
(if (== -1 (.read xzs)) ; Good practice as extra safety measure
|
(if (== -1 (.read xzs)) ; Good practice as extra safety measure
|
||||||
nil
|
nil
|
||||||
(throw (ex-info "LZMA2 Decompress failed: corrupt data?" {:ba ba})))
|
(truss/ex-info! "LZMA2 Decompress failed: corrupt data?" {:ba ba}))
|
||||||
ba)))
|
ba)))
|
||||||
|
|
||||||
(def lzma2-compressor
|
;;;; Public API
|
||||||
"Default org.tukaani.xz.LZMA2 compressor:
|
|
||||||
Ratio: high.
|
|
||||||
Write speed: _very_ slow (also currently single-threaded).
|
|
||||||
Read speed: slow.
|
|
||||||
|
|
||||||
A specialized compressor for large, low-write data in space-sensitive
|
(def zstd-compressor
|
||||||
environments."
|
"Default `Zstd` (`Zstandard`) compressor:
|
||||||
|
- Compression ratio: `B` (0.53 on reference benchmark).
|
||||||
|
- Compression speed: `C` (1300 msecs on reference benchmark).
|
||||||
|
- Decompression speed: `B` (400 msecs on reference benchmark).
|
||||||
|
|
||||||
|
Good general-purpose compressor, balances ratio & speed.
|
||||||
|
See `taoensso.nippy-benchmarks` for detailed comparative benchmarks."
|
||||||
|
(ZstdCompressor. false))
|
||||||
|
|
||||||
|
(def lz4-compressor
|
||||||
|
"Default `LZ4` compressor:
|
||||||
|
- Compression ratio: `C` (0.58 on reference benchmark).
|
||||||
|
- Compression speed: `A` (240 msecs on reference benchmark).
|
||||||
|
- Decompression speed: `A+` (30 msecs on reference benchmark).
|
||||||
|
|
||||||
|
Good general-purpose compressor, favours speed.
|
||||||
|
See `taoensso.nippy-benchmarks` for detailed comparative benchmarks."
|
||||||
|
(LZ4Compressor.))
|
||||||
|
|
||||||
|
(def lzo-compressor
|
||||||
|
"Default `LZO` compressor:
|
||||||
|
- Compression ratio: `C` (0.58 on reference benchmark).
|
||||||
|
- Compression speed: `A` (220 msecs on reference benchmark).
|
||||||
|
- Decompression speed: `A` (40 msecs on reference benchmark).
|
||||||
|
|
||||||
|
Good general-purpose compressor, favours speed.
|
||||||
|
See `taoensso.nippy-benchmarks` for detailed comparative benchmarks."
|
||||||
|
(LZOCompressor.))
|
||||||
|
|
||||||
|
(def lzma2-compressor
|
||||||
|
"Default `LZMA2` compressor:
|
||||||
|
- Compression ratio: `A+` (0.4 on reference benchmark).
|
||||||
|
- Compression speed: `E` (18.5 secs on reference benchmark).
|
||||||
|
- Decompression speed: `D` (12 secs on reference benchmark).
|
||||||
|
|
||||||
|
Specialized compressor, strongly favours ratio.
|
||||||
|
See `taoensso.nippy-benchmarks` for detailed comparative benchmarks."
|
||||||
(LZMA2Compressor. 0))
|
(LZMA2Compressor. 0))
|
||||||
|
|
||||||
(deftype LZ4Compressor [compressor_ decompressor_]
|
(enc/def* snappy-compressor
|
||||||
ICompressor
|
"Default `Snappy` compressor:
|
||||||
(header-id [_] :lz4)
|
- Compression ratio: `C` (0.58 on reference benchmark).
|
||||||
(compress [_ ba]
|
- Compression speed: `A+` (210 msecs on reference benchmark).
|
||||||
(let [^net.jpountz.lz4.LZ4Compressor compressor @compressor_
|
- Decompression speed: `B` (130 msecs on reference benchmark).
|
||||||
len-decomp (alength ^bytes ba)
|
Good general-purpose compressor, favours speed.
|
||||||
max-len-comp (.maxCompressedLength compressor len-decomp)
|
See `taoensso.nippy-benchmarks` for detailed comparative benchmarks."
|
||||||
ba-comp* (byte-array max-len-comp) ; Over-sized
|
(SnappyCompressor. false))
|
||||||
len-comp (.compress compressor ^bytes 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]
|
(enc/def* ^:no-doc lz4hc-compressor
|
||||||
(let [^net.jpountz.lz4.LZ4Decompressor decompressor @decompressor_
|
"Different LZ4 modes no longer supported, prefer `lz4-compressor`."
|
||||||
bais (ByteArrayInputStream. ba)
|
{:deprecated "v3.4.0-RC1 (2024-02-06)"}
|
||||||
dis (DataInputStream. bais)
|
(LZ4Compressor.))
|
||||||
;;
|
|
||||||
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 4 ba-decomp 0 len-decomp)]
|
|
||||||
ba-decomp)))
|
|
||||||
|
|
||||||
(def ^:private lz4-factory_ (delay (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.
|
|
||||||
|
|
||||||
Thanks to Max Penet (@mpenet) for our first implementation,
|
|
||||||
Ref. https://github.com/mpenet/nippy-lz4"
|
|
||||||
(LZ4Compressor.
|
|
||||||
(delay (.fastCompressor ^net.jpountz.lz4.LZ4Factory @lz4-factory_))
|
|
||||||
(delay (.fastDecompressor ^net.jpountz.lz4.LZ4Factory @lz4-factory_))))
|
|
||||||
|
|
||||||
(def lz4hc-compressor
|
|
||||||
"Like `lz4-compressor` but trades some write speed for ratio."
|
|
||||||
(LZ4Compressor.
|
|
||||||
(delay (.highCompressor ^net.jpountz.lz4.LZ4Factory @lz4-factory_))
|
|
||||||
(delay (.fastDecompressor ^net.jpountz.lz4.LZ4Factory @lz4-factory_))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(def ba-bench (.getBytes (apply str (repeatedly 1000 rand)) "UTF-8"))
|
|
||||||
(defn bench1 [compressor]
|
|
||||||
{:time (enc/bench 10000 {:nlaps-warmup 10000}
|
|
||||||
(->> ba-bench (compress compressor) (decompress compressor)))
|
|
||||||
:ratio (enc/round2 (/ (count (compress compressor ba-bench))
|
|
||||||
(count ba-bench)))})
|
|
||||||
|
|
||||||
(println
|
|
||||||
{:snappy (bench1 snappy-compressor)
|
|
||||||
;:lzma2 (bench1 lzma2-compressor) ; Slow!
|
|
||||||
:lz4 (bench1 lz4-compressor)
|
|
||||||
:lz4hc (bench1 lz4hc-compressor)})
|
|
||||||
|
|
||||||
;;; 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}})
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
(ns taoensso.nippy.crypto
|
(ns ^:no-doc taoensso.nippy.crypto
|
||||||
"Low-level crypto utils.
|
"Low-level crypto utils.
|
||||||
Private & alpha, very likely to change!"
|
Private & alpha, very likely to change!"
|
||||||
(:refer-clojure :exclude [rand-nth])
|
(:refer-clojure :exclude [rand-nth])
|
||||||
(:require [taoensso.encore :as enc]))
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]))
|
||||||
|
|
||||||
;; Note that AES128 may be preferable to AES256 due to known attack
|
;; Note that AES128 may be preferable to AES256 due to known attack
|
||||||
;; vectors specific to AES256, Ref. https://goo.gl/qU4CCV
|
;; vectors specific to AES256, Ref. <https://goo.gl/qU4CCV>
|
||||||
;; or for a counter argument, Ref. https://goo.gl/9LA9Yb
|
;; or for a counter argument, Ref. <https://goo.gl/9LA9Yb>
|
||||||
|
|
||||||
;;;; Randomness
|
;;;; Randomness
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@
|
||||||
(defn take-ba ^bytes [n ^bytes ba] (java.util.Arrays/copyOf ba ^int n)) ; Pads if ba too small
|
(defn take-ba ^bytes [n ^bytes ba] (java.util.Arrays/copyOf ba ^int n)) ; Pads if ba too small
|
||||||
(defn utf8->ba ^bytes [^String s] (.getBytes s "UTF-8"))
|
(defn utf8->ba ^bytes [^String s] (.getBytes s "UTF-8"))
|
||||||
(defn- add-salt ^bytes [?salt-ba ba] (if ?salt-ba (enc/ba-concat ?salt-ba ba) ba))
|
(defn- add-salt ^bytes [?salt-ba ba] (if ?salt-ba (enc/ba-concat ?salt-ba ba) ba))
|
||||||
(defn pwd-as-ba ^bytes [utf8-or-ba] (if (string? utf8-or-ba) (utf8->ba utf8-or-ba) (enc/have enc/bytes? utf8-or-ba)))
|
(defn pwd-as-ba ^bytes [utf8-or-ba] (if (string? utf8-or-ba) (utf8->ba utf8-or-ba) (truss/have enc/bytes? utf8-or-ba)))
|
||||||
|
|
||||||
(comment (seq (pwd-as-ba "foo")))
|
(comment (seq (pwd-as-ba "foo")))
|
||||||
|
|
||||||
|
|
@ -70,7 +72,7 @@
|
||||||
(get-key-spec ^javax.crypto.spec.SecretKeySpec [_ ba] "Returns a `javax.crypto.spec.SecretKeySpec`.")
|
(get-key-spec ^javax.crypto.spec.SecretKeySpec [_ ba] "Returns a `javax.crypto.spec.SecretKeySpec`.")
|
||||||
(get-param-spec ^java.security.spec.AlgorithmParameterSpec [_ iv-ba] "Returns a `java.security.spec.AlgorithmParameters`."))
|
(get-param-spec ^java.security.spec.AlgorithmParameterSpec [_ iv-ba] "Returns a `java.security.spec.AlgorithmParameters`."))
|
||||||
|
|
||||||
;; Prefer GCM > CBC, Ref. https://goo.gl/jpZoj8
|
;; Prefer GCM > CBC, Ref. <https://goo.gl/jpZoj8>
|
||||||
(def ^:private gcm-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/GCM/NoPadding")))
|
(def ^:private gcm-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/GCM/NoPadding")))
|
||||||
(def ^:private cbc-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding")))
|
(def ^:private cbc-cipher* (enc/thread-local-proxy (javax.crypto.Cipher/getInstance "AES/CBC/PKCS5Padding")))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
(ns taoensso.nippy.encryption
|
(ns ^:no-doc taoensso.nippy.encryption
|
||||||
"Simple no-nonsense crypto with reasonable defaults"
|
"Private, implementation detail."
|
||||||
(:require
|
(:require
|
||||||
|
[taoensso.truss :as truss]
|
||||||
[taoensso.encore :as enc]
|
[taoensso.encore :as enc]
|
||||||
[taoensso.nippy.crypto :as crypto]))
|
[taoensso.nippy.crypto :as crypto]))
|
||||||
|
|
||||||
(def standard-header-ids
|
(def ^:const standard-header-ids
|
||||||
"These'll support :auto thaw"
|
"These support `:auto` thaw."
|
||||||
#{:aes128-cbc-sha512
|
#{:aes128-cbc-sha512 :aes128-gcm-sha512})
|
||||||
:aes128-gcm-sha512})
|
|
||||||
|
|
||||||
(defprotocol IEncryptor
|
(defprotocol IEncryptor
|
||||||
(header-id [encryptor])
|
(header-id [encryptor])
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
(decrypt ^bytes [encryptor pwd ba]))
|
(decrypt ^bytes [encryptor pwd ba]))
|
||||||
|
|
||||||
(defn- throw-destructure-ex [typed-password]
|
(defn- throw-destructure-ex [typed-password]
|
||||||
(throw (ex-info
|
(truss/ex-info!
|
||||||
(str "Expected password form: "
|
(str "Expected password form: "
|
||||||
"[<#{:salted :cached}> <password-string>].\n "
|
"[<#{:salted :cached}> <password-string>].\n "
|
||||||
"See `aes128-encryptor` docstring for details!")
|
"See `aes128-encryptor` docstring for details!")
|
||||||
{:typed-password typed-password})))
|
{:typed-password typed-password}))
|
||||||
|
|
||||||
(defn- destructure-typed-pwd [typed-password]
|
(defn- destructure-typed-pwd [typed-password]
|
||||||
(if (vector? typed-password)
|
(if (vector? typed-password)
|
||||||
|
|
|
||||||
273
src/taoensso/nippy/impl.clj
Normal file
273
src/taoensso/nippy/impl.clj
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
(ns ^:no-doc taoensso.nippy.impl
|
||||||
|
"Private, implementation detail."
|
||||||
|
(:require
|
||||||
|
[clojure.string :as str]
|
||||||
|
[taoensso.truss :as truss]
|
||||||
|
[taoensso.encore :as enc]))
|
||||||
|
|
||||||
|
;;;; Fallback type tests
|
||||||
|
|
||||||
|
(defn cache-by-type [f]
|
||||||
|
(let [cache_ (enc/latom {})] ; {<type> <result_>}
|
||||||
|
(fn [x]
|
||||||
|
(let [t (if (fn? x) ::fn (type x))]
|
||||||
|
(if-let [result_ (get (cache_) t)]
|
||||||
|
@result_
|
||||||
|
(if-let [uncacheable-type? (re-find #"\d" (str t))]
|
||||||
|
(do (f x))
|
||||||
|
@(cache_ t #(or % (delay (f x))))))))))
|
||||||
|
|
||||||
|
(def seems-readable?
|
||||||
|
(cache-by-type
|
||||||
|
(fn [x]
|
||||||
|
(try
|
||||||
|
(enc/read-edn (enc/pr-edn x))
|
||||||
|
true
|
||||||
|
(catch Throwable _ false)))))
|
||||||
|
|
||||||
|
(def seems-serializable?
|
||||||
|
(cache-by-type
|
||||||
|
(fn [x]
|
||||||
|
(enc/cond
|
||||||
|
(fn? x) false ; Falsely reports as Serializable
|
||||||
|
|
||||||
|
(instance? java.io.Serializable x)
|
||||||
|
(try
|
||||||
|
(let [c (Class/forName (.getName (class x))) ; Try 1st (fail fast)
|
||||||
|
bas (java.io.ByteArrayOutputStream.)
|
||||||
|
_ (.writeObject (java.io.ObjectOutputStream. bas) x)
|
||||||
|
ba (.toByteArray bas)]
|
||||||
|
#_
|
||||||
|
(cast c
|
||||||
|
(.readObject ; Unsafe + usu. unnecessary to check
|
||||||
|
(ObjectInputStream. (ByteArrayInputStream. ba))))
|
||||||
|
true)
|
||||||
|
(catch Throwable _ false))
|
||||||
|
|
||||||
|
:else false))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(enc/qb 1e6 ; [60.83 61.16 59.86 57.37]
|
||||||
|
(seems-readable? "Hello world")
|
||||||
|
(seems-serializable? "Hello world")
|
||||||
|
(seems-readable? (fn []))
|
||||||
|
(seems-serializable? (fn []))))
|
||||||
|
|
||||||
|
;;;; Java Serializable
|
||||||
|
|
||||||
|
(def ^:const ^:private allow-and-record "allow-and-record")
|
||||||
|
(defn- allow-and-record? [x] (= x allow-and-record))
|
||||||
|
|
||||||
|
(defn- classname-set
|
||||||
|
"Returns ?#{<classname>}."
|
||||||
|
[x]
|
||||||
|
(when x
|
||||||
|
(if (string? x)
|
||||||
|
(if (= x "") #{} (set (mapv str/trim (str/split x #"[,:]"))))
|
||||||
|
(truss/have set? x))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(mapv classname-set [nil #{"foo"} "" "foo, bar:baz"])
|
||||||
|
(.getName (.getSuperclass (.getClass (java.util.concurrent.TimeoutException.)))))
|
||||||
|
|
||||||
|
(defn parse-allowlist
|
||||||
|
"Returns #{<classname>}, or `allow-and-record`."
|
||||||
|
[default base add]
|
||||||
|
(if (or
|
||||||
|
(allow-and-record? base)
|
||||||
|
(allow-and-record? add))
|
||||||
|
allow-and-record
|
||||||
|
(into
|
||||||
|
(or (classname-set base) default)
|
||||||
|
(do (classname-set add)))))
|
||||||
|
|
||||||
|
(comment (parse-allowlist #{"default"} "base1,base2" "add1"))
|
||||||
|
|
||||||
|
(let [nmax 1000
|
||||||
|
ngc 16000
|
||||||
|
state_ (enc/latom {}) ; {<class-name> <frequency>}
|
||||||
|
lock_ (enc/latom nil) ; ?promise
|
||||||
|
trim
|
||||||
|
(fn [nmax state]
|
||||||
|
(persistent!
|
||||||
|
(enc/reduce-top nmax val enc/rcompare conj!
|
||||||
|
(transient {}) state)))]
|
||||||
|
|
||||||
|
;; Note: trim strategy isn't perfect: it can be tough for new
|
||||||
|
;; classes to break into the top set since frequencies are being
|
||||||
|
;; reset only for classes outside the top set.
|
||||||
|
;;
|
||||||
|
;; In practice this is probably good enough since the main objective
|
||||||
|
;; is to discard one-off anonymous classes to protect state from
|
||||||
|
;; endlessly growing. Also `gc-rate` allows state to temporarily grow
|
||||||
|
;; significantly beyond `nmax` size, which helps to give new classes
|
||||||
|
;; some chance to accumulate a competitive frequency before next GC.
|
||||||
|
|
||||||
|
(defn ^{:-state_ state_} ; Undocumented
|
||||||
|
allow-and-record-any-serializable-class-unsafe
|
||||||
|
"A predicate (fn allow-class? [class-name]) fn that can be assigned
|
||||||
|
to `*freeze-serializable-allowlist*` and/or
|
||||||
|
`*thaw-serializable-allowlist*` that:
|
||||||
|
|
||||||
|
- Will allow ANY class to use Nippy's `Serializable` support (unsafe).
|
||||||
|
- And will record {<class-name> <frequency-allowed>} for the <=1000
|
||||||
|
classes that ~most frequently made use of this support.
|
||||||
|
|
||||||
|
`get-recorded-serializable-classes` returns the recorded state.
|
||||||
|
|
||||||
|
This predicate is provided as a convenience for users upgrading from
|
||||||
|
previous versions of Nippy that allowed the use of `Serializable` for all
|
||||||
|
classes by default.
|
||||||
|
|
||||||
|
While transitioning from an unsafe->safe configuration, you can use
|
||||||
|
this predicate (unsafe) to record information about which classes have
|
||||||
|
been using Nippy's `Serializable` support in your environment.
|
||||||
|
|
||||||
|
Once some time has passed, you can check the recorded state. If you're
|
||||||
|
satisfied that all recorded classes are safely `Serializable`, you can
|
||||||
|
then merge the recorded classes into Nippy's default allowlist/s, e.g.:
|
||||||
|
|
||||||
|
(alter-var-root #'thaw-serializable-allowlist*
|
||||||
|
(fn [_] (into default-thaw-serializable-allowlist
|
||||||
|
(keys (get-recorded-serializable-classes)))))"
|
||||||
|
|
||||||
|
[class-name]
|
||||||
|
(when-let [p (lock_)] @p)
|
||||||
|
(let [n (count (state_ #(assoc % class-name (inc (long (or (get % class-name) 0))))))]
|
||||||
|
;; Garbage collection (GC): may be serializing anonymous classes, etc.
|
||||||
|
;; so input domain could be infinite
|
||||||
|
(when (> n ngc) ; Too many classes recorded, uncommon
|
||||||
|
(let [p (promise)]
|
||||||
|
(when (compare-and-set! lock_ nil p) ; Acquired GC lock
|
||||||
|
(try
|
||||||
|
(do (reset! state_ (trim nmax (state_)))) ; GC state
|
||||||
|
(finally (reset! lock_ nil) (deliver p nil))))))
|
||||||
|
n))
|
||||||
|
|
||||||
|
(defn get-recorded-serializable-classes
|
||||||
|
"Returns {<class-name> <frequency>} of the <=1000 classes that ~most
|
||||||
|
frequently made use of Nippy's `Serializable` support via
|
||||||
|
`allow-and-record-any-serializable-class-unsafe`.
|
||||||
|
|
||||||
|
See that function's docstring for more info."
|
||||||
|
[] (trim nmax (state_))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(count (get-recorded-serializable-classes))
|
||||||
|
(enc/reduce-n
|
||||||
|
(fn [_ n] (allow-and-record-any-serializable-class-unsafe (str n)))
|
||||||
|
nil 0 1e5))
|
||||||
|
|
||||||
|
(let [compile
|
||||||
|
(enc/fmemoize
|
||||||
|
(fn [x]
|
||||||
|
(if (allow-and-record? x)
|
||||||
|
allow-and-record-any-serializable-class-unsafe
|
||||||
|
(enc/name-filter x))))
|
||||||
|
|
||||||
|
fn? fn?
|
||||||
|
conform?
|
||||||
|
(fn [x cn]
|
||||||
|
(if (fn? x)
|
||||||
|
(x cn) ; Intentionally uncached, can be handy
|
||||||
|
((compile x) cn)))]
|
||||||
|
|
||||||
|
(defn serializable-allowed? [allow-list class-name]
|
||||||
|
(conform? allow-list class-name)))
|
||||||
|
|
||||||
|
;;;; Release targeting
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(set! *print-length* nil)
|
||||||
|
(vec (sort (keys taoensso.nippy/public-types-spec)))
|
||||||
|
|
||||||
|
;; To help support release targeting, we track new type ids added over time
|
||||||
|
(let [id-history ; {<release> #{type-ids}}
|
||||||
|
{350 ; v3.5.0 (2025-04-15), added 5x
|
||||||
|
;; #{int-array-lg long-array-lg float-array-lg double-array-lg string-array-lg}
|
||||||
|
#{0 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}
|
||||||
|
|
||||||
|
340 ; v3.4.0 (2024-04-30), added 2x
|
||||||
|
;; #{map-entry meta-protocol-key}
|
||||||
|
#{0 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 110 111 112 113 114 115}
|
||||||
|
|
||||||
|
330 ; v3.3.0 (2023-10-11), added 11x
|
||||||
|
;; #{long-pos-sm long-pos-md long-pos-lg long-neg-sm long-neg-md long-neg-lg
|
||||||
|
;; str-sm* vec-sm* set-sm* map-sm* sql-date}
|
||||||
|
#{0 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 105 106
|
||||||
|
110 111 112 113 114 115}
|
||||||
|
|
||||||
|
320 ; v3.2.0 (2022-07-18), added none
|
||||||
|
#{0 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 90 91 100 101 102 105 106 110 111 112 113 114 115}
|
||||||
|
|
||||||
|
313 ; v3.1.3 (2022-06-23), added 5x
|
||||||
|
;; #{time-instant time-duration time-period kw-md sym-md}
|
||||||
|
#{0 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 90 91 100 101 102 105 106 110 111 112 113 114 115}
|
||||||
|
|
||||||
|
300 ; v3.0.0 (2020-09-20), baseline
|
||||||
|
#{0 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 80
|
||||||
|
81 82 90 91 100 101 102 105 106 110 111 112 113 114 115}}
|
||||||
|
|
||||||
|
diff
|
||||||
|
(fn [new-release old-release]
|
||||||
|
(vec (sort (clojure.set/difference (id-history new-release) (id-history old-release)))))]
|
||||||
|
|
||||||
|
(diff 350 340)))
|
||||||
|
|
||||||
|
(let [target-release
|
||||||
|
(enc/get-env {:as :edn, :default 320}
|
||||||
|
:taoensso.nippy.target-release)
|
||||||
|
|
||||||
|
target>=
|
||||||
|
(fn [min-release]
|
||||||
|
(if target-release
|
||||||
|
(>= (long target-release) (long min-release))
|
||||||
|
true))]
|
||||||
|
|
||||||
|
(defmacro target-release< [min-release] (not (target>= min-release)))
|
||||||
|
(defmacro target-release>=
|
||||||
|
"Returns true iff `target-release` is nil or >= given `min-release`.
|
||||||
|
Used to help ease data migration for changes to core data types.
|
||||||
|
|
||||||
|
When support is added for a new type in Nippy version X, it necessarily means
|
||||||
|
that data containing that new type and frozen with Nippy version X is unthawable
|
||||||
|
with Nippy versions < X.
|
||||||
|
|
||||||
|
Earlier versions of Nippy will throw an exception on thawing affected data:
|
||||||
|
\"Unrecognized type id (<n>). Data frozen with newer Nippy version?\"
|
||||||
|
|
||||||
|
This can present a challenge when updating to new versions of Nippy, e.g.:
|
||||||
|
|
||||||
|
- Rolling updates could lead to old and new versions of Nippy temporarily co-existing.
|
||||||
|
- Data written with new types could limit your ability to revert a Nippy update.
|
||||||
|
|
||||||
|
There's no easy solution to this in GENERAL, but we CAN at least help reduce the
|
||||||
|
burden related to CHANGES in core data types by introducing changes over 2 phases:
|
||||||
|
|
||||||
|
1. Nippy vX reads new (changed) type, writes old type
|
||||||
|
2. Nippy vX+1 writes new (changed) type
|
||||||
|
|
||||||
|
When relevant, we can then warn users in the CHANGELOG to not leapfrog
|
||||||
|
(e.g. Nippy vX -> Nippy vX+2) when doing rolling updates."
|
||||||
|
[min-release] (target>= min-release)))
|
||||||
|
|
||||||
|
(comment (macroexpand '(target-release>= 340)))
|
||||||
|
|
@ -1,60 +1,68 @@
|
||||||
(ns taoensso.nippy.tools
|
(ns taoensso.nippy.tools
|
||||||
"Utils for 3rd-party tools that want to add user-configurable Nippy support.
|
"Utils for community tools that want to add user-configurable Nippy support.
|
||||||
Used by Carmine, Faraday, etc."
|
Used by Carmine, Faraday, etc."
|
||||||
(:require [taoensso.nippy :as nippy]))
|
(:require
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.nippy :as nippy]))
|
||||||
|
|
||||||
(def ^:dynamic *freeze-opts* nil)
|
(def ^:dynamic *freeze-opts* nil)
|
||||||
(def ^:dynamic *thaw-opts* nil)
|
(def ^:dynamic *thaw-opts* nil)
|
||||||
|
|
||||||
(defmacro with-freeze-opts [opts & body] `(binding [*freeze-opts* ~opts] ~@body))
|
(do
|
||||||
(defmacro with-thaw-opts [opts & body] `(binding [*thaw-opts* ~opts] ~@body))
|
(defmacro with-freeze-opts [opts & body] `(binding [*freeze-opts* ~opts ] ~@body))
|
||||||
|
(defmacro with-freeze-opts+ [opts & body] `(binding [*freeze-opts* (enc/merge *freeze-opts* ~opts)] ~@body))
|
||||||
|
(defmacro with-thaw-opts [opts & body] `(binding [*thaw-opts* ~opts ] ~@body))
|
||||||
|
(defmacro with-thaw-opts+ [opts & body] `(binding [*thaw-opts* (enc/merge *thaw-opts* ~opts)] ~@body)))
|
||||||
|
|
||||||
(deftype WrappedForFreezing [val opts])
|
(deftype WrappedForFreezing [val opts])
|
||||||
(defn wrapped-for-freezing? [x] (instance? WrappedForFreezing x))
|
(defn wrapped-for-freezing? [x] (instance? WrappedForFreezing x))
|
||||||
(defn wrap-for-freezing
|
|
||||||
"Ensures that given arg (any freezable data type) is wrapped so that
|
|
||||||
(tools/freeze <wrapped-arg>) will serialize as
|
|
||||||
(nippy/freeze <unwrapped-arg> <opts>).
|
|
||||||
|
|
||||||
See also `nippy.tools/freeze`, `nippy.tools/thaw`."
|
(defn wrap-for-freezing
|
||||||
|
"Captures (merge `tools/*thaw-opts*` `wrap-opts`), and returns
|
||||||
|
the given argument in a wrapped form so that `tools/freeze` will
|
||||||
|
use the captured options when freezing the wrapper argument.
|
||||||
|
|
||||||
|
See also `tools/freeze`."
|
||||||
([x ] (wrap-for-freezing x nil))
|
([x ] (wrap-for-freezing x nil))
|
||||||
([x opts]
|
([x wrap-opts]
|
||||||
|
(let [captured-opts (enc/merge *freeze-opts* wrap-opts)] ; wrap > dynamic
|
||||||
(if (instance? WrappedForFreezing x)
|
(if (instance? WrappedForFreezing x)
|
||||||
(let [^WrappedForFreezing x x]
|
(let [^WrappedForFreezing x x]
|
||||||
(if (= (.-opts x) opts)
|
(if (= (.-opts x) captured-opts)
|
||||||
x
|
x
|
||||||
(WrappedForFreezing. (.-val x) opts)))
|
(WrappedForFreezing. (.-val x) captured-opts)))
|
||||||
(WrappedForFreezing. x opts))))
|
(WrappedForFreezing. x captured-opts)))))
|
||||||
|
|
||||||
(defn freeze
|
(defn freeze
|
||||||
"Like `nippy/freeze` but uses as opts the following merged in order of
|
"Like `nippy/freeze` but uses as options the following, merged in
|
||||||
ascending preference:
|
order of ascending preference:
|
||||||
|
|
||||||
- Optional `default-opts` arg given to this fn (default nil).
|
1. `default-opts` given to this fn (default nil).
|
||||||
- Optional `*freeze-opts*` dynamic value (default nil).
|
2. `tools/*freeze-opts*` dynamic value (default nil).
|
||||||
- Optional opts provided to `wrap-for-freezing` (default nil)."
|
3. Opts captured by `tools/wrap-for-freezing` (default nil).
|
||||||
|
|
||||||
|
See also `tools/wrap-for-freezing`."
|
||||||
([x ] (freeze x nil))
|
([x ] (freeze x nil))
|
||||||
([x default-opts]
|
([x default-opts]
|
||||||
(let [default-opts (get default-opts :default-opts default-opts) ; For back compatibility
|
(let [default-opts (get default-opts :default-opts default-opts) ; Back compatibility
|
||||||
merged-opts (conj (or default-opts {}) *freeze-opts*)]
|
active-opts (enc/merge default-opts *freeze-opts*)] ; dynamic > default
|
||||||
|
|
||||||
(if (instance? WrappedForFreezing x)
|
(if (instance? WrappedForFreezing x)
|
||||||
(let [^WrappedForFreezing x x]
|
(let [^WrappedForFreezing x x]
|
||||||
(nippy/freeze (.-val x) (conj merged-opts (.-opts x))))
|
(nippy/freeze (.-val x) (enc/merge active-opts (.-opts x)))) ; captured > active!
|
||||||
(nippy/freeze x merged-opts)))))
|
(nippy/freeze x active-opts)))))
|
||||||
|
|
||||||
(defn thaw
|
(defn thaw
|
||||||
"Like `nippy/thaw` but uses as opts the following merged in order of
|
"Like `nippy/thaw` but uses as options the following, merged in
|
||||||
ascending preference:
|
order of ascending preference:
|
||||||
|
|
||||||
- Optional `default-opts` arg given to this fn (default nil).
|
|
||||||
- Optional `*thaw-opts*` dynamic value (default nil)."
|
|
||||||
|
|
||||||
|
1. `default-opts` given to this fn (default nil).
|
||||||
|
2. `tools/*thaw-opts*` dynamic value (default nil)."
|
||||||
([ba ] (thaw ba nil))
|
([ba ] (thaw ba nil))
|
||||||
([ba default-opts]
|
([ba default-opts]
|
||||||
(let [default-opts (get default-opts :default-opts default-opts) ; For back compatibility
|
(let [default-opts (get default-opts :default-opts default-opts) ; Back compatibility
|
||||||
merged-opts (conj (or default-opts {}) *thaw-opts*)]
|
active-opts (enc/merge default-opts *thaw-opts*)] ; dynamic > default
|
||||||
(nippy/thaw ba merged-opts))))
|
|
||||||
|
(nippy/thaw ba active-opts))))
|
||||||
|
|
||||||
(comment (thaw (freeze (wrap-for-freezing "wrapped"))))
|
(comment (thaw (freeze (wrap-for-freezing "wrapped"))))
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
(ns taoensso.nippy.utils
|
|
||||||
(:require [clojure.string :as str]
|
|
||||||
[taoensso.encore :as enc])
|
|
||||||
(:import [java.io ByteArrayInputStream ByteArrayOutputStream Serializable
|
|
||||||
ObjectOutputStream ObjectInputStream]))
|
|
||||||
|
|
||||||
;;;; Fallback type tests
|
|
||||||
;; Unfortunately the only ~reliable way we can tell if something's
|
|
||||||
;; really serializable/readable is to actually try a full roundtrip.
|
|
||||||
|
|
||||||
(let [swap-cache! enc/-swap-val!]
|
|
||||||
(defn- memoize-type-test [test-fn]
|
|
||||||
(let [cache (atom {})] ; {<type> <type-okay?>}
|
|
||||||
(fn [x]
|
|
||||||
(let [t (type x)
|
|
||||||
;; This is a bit hackish, but no other obvious solutions (?):
|
|
||||||
cacheable? (not (re-find #"__\d+" (str t))) ; gensym form
|
|
||||||
test (fn [] (try (test-fn x) (catch Exception _ false)))]
|
|
||||||
(if cacheable?
|
|
||||||
@(swap-cache! cache t #(if % % (delay (test))))
|
|
||||||
(test)))))))
|
|
||||||
|
|
||||||
(def readable? (memoize-type-test (fn [x] (-> x enc/pr-edn enc/read-edn) true)))
|
|
||||||
(def serializable?
|
|
||||||
(let [mtt
|
|
||||||
(memoize-type-test
|
|
||||||
(fn [x]
|
|
||||||
(let [class-name (.getName (class x))
|
|
||||||
c (Class/forName class-name) ; Try 1st (fail fast)
|
|
||||||
bas (ByteArrayOutputStream.)
|
|
||||||
_ (.writeObject (ObjectOutputStream. bas) x)
|
|
||||||
ba (.toByteArray bas)]
|
|
||||||
|
|
||||||
#_
|
|
||||||
(cast c
|
|
||||||
(.readObject ; Unsafe + usu. unnecessary to check
|
|
||||||
(ObjectInputStream.
|
|
||||||
(ByteArrayInputStream. ba))))
|
|
||||||
|
|
||||||
true)))]
|
|
||||||
|
|
||||||
(fn [x]
|
|
||||||
(if (instance? Serializable x)
|
|
||||||
(if (fn? x)
|
|
||||||
false ; Reports as true but is unreliable
|
|
||||||
(mtt x))
|
|
||||||
false))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(enc/qb 1e4
|
|
||||||
(readable? "Hello world") ; Cacheable
|
|
||||||
(serializable? "Hello world") ; Cacheable
|
|
||||||
(readable? (fn [])) ; Uncacheable
|
|
||||||
(serializable? (fn [])) ; Uncacheable
|
|
||||||
)) ; [5.65 5.88 1129.46 1.4]
|
|
||||||
|
|
||||||
;;;;
|
|
||||||
|
|
||||||
(defn- is-coll?
|
|
||||||
"Checks for _explicit_ IPersistentCollection types with Nippy support.
|
|
||||||
Checking for explicit concrete types is tedious but preferable since a
|
|
||||||
`freezable?` false positive would be much worse than a false negative."
|
|
||||||
[x]
|
|
||||||
(let [is? #(when (instance? % x) %)]
|
|
||||||
(or
|
|
||||||
(is? clojure.lang.APersistentVector)
|
|
||||||
(is? clojure.lang.APersistentMap)
|
|
||||||
(is? clojure.lang.APersistentSet)
|
|
||||||
(is? clojure.lang.PersistentList)
|
|
||||||
(is? clojure.lang.PersistentList$EmptyList) ; (type '())
|
|
||||||
(is? clojure.lang.PersistentQueue)
|
|
||||||
(is? clojure.lang.PersistentTreeSet)
|
|
||||||
(is? clojure.lang.PersistentTreeMap)
|
|
||||||
(is? clojure.lang.PersistentVector$ChunkedSeq)
|
|
||||||
|
|
||||||
(is? clojure.lang.IRecord) ; TODO Possible to avoid the interface check?
|
|
||||||
(is? clojure.lang.LazySeq)
|
|
||||||
|
|
||||||
;; Too non-specific: could result in false positives (which would be a
|
|
||||||
;; serious problem here):
|
|
||||||
;; (is? clojure.lang.ISeq)
|
|
||||||
|
|
||||||
)))
|
|
||||||
|
|
||||||
(comment (is-coll? (clojure.lang.PersistentVector$ChunkedSeq. [1 2 3] 0 0)))
|
|
||||||
|
|
||||||
(defmacro ^:private is? [x c] `(when (instance? ~c ~x) ~c))
|
|
||||||
|
|
||||||
(defn freezable?
|
|
||||||
"Alpha - subject to change.
|
|
||||||
Returns truthy iff Nippy *appears* to support freezing the given argument.
|
|
||||||
|
|
||||||
`:allow-clojure-reader?` and `:allow-java-serializable?` options may be
|
|
||||||
used to enable the relevant roundtrip fallback test(s). These tests are
|
|
||||||
only **moderately reliable** since they're cached by arg type and don't
|
|
||||||
test for pre/post serialization value equality (there's no good general
|
|
||||||
way of doing so)."
|
|
||||||
|
|
||||||
;; TODO Not happy with this approach in general, could do with a refactor.
|
|
||||||
;; Maybe return true/false/nil (nil => maybe)?
|
|
||||||
|
|
||||||
([x] (freezable? x nil))
|
|
||||||
([x {:keys [allow-clojure-reader? allow-java-serializable?]}]
|
|
||||||
(if (is-coll? x)
|
|
||||||
(when (enc/revery? freezable? x) (type x))
|
|
||||||
(or
|
|
||||||
(is? x clojure.lang.Keyword)
|
|
||||||
(is? x java.lang.String)
|
|
||||||
(is? x java.lang.Long)
|
|
||||||
(is? x java.lang.Double)
|
|
||||||
(nil? x)
|
|
||||||
|
|
||||||
(is? x clojure.lang.BigInt)
|
|
||||||
(is? x clojure.lang.Ratio)
|
|
||||||
|
|
||||||
(is? x java.lang.Boolean)
|
|
||||||
(is? x java.lang.Integer)
|
|
||||||
(is? x java.lang.Short)
|
|
||||||
(is? x java.lang.Byte)
|
|
||||||
(is? x java.lang.Character)
|
|
||||||
(is? x java.math.BigInteger)
|
|
||||||
(is? x java.math.BigDecimal)
|
|
||||||
(is? x #=(java.lang.Class/forName "[B"))
|
|
||||||
|
|
||||||
(is? x clojure.lang.Symbol)
|
|
||||||
|
|
||||||
(is? x java.util.Date)
|
|
||||||
(is? x java.util.UUID)
|
|
||||||
(is? x java.util.regex.Pattern)
|
|
||||||
|
|
||||||
(when (and allow-clojure-reader? (readable? x)) :clojure-reader)
|
|
||||||
(when (and allow-java-serializable? (serializable? x)) :java-serializable)))))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(enc/qb 10000 (freezable? "hello")) ; 0.79
|
|
||||||
(freezable? [:a :b])
|
|
||||||
(freezable? [:a (fn [x] (* x x))])
|
|
||||||
(freezable? (.getBytes "foo"))
|
|
||||||
(freezable? (java.util.Date.) {:allow-clojure-reader? true})
|
|
||||||
(freezable? (Exception. "_") {:allow-clojure-reader? true})
|
|
||||||
(freezable? (Exception. "_") {:allow-java-serializable? true})
|
|
||||||
(freezable? (atom {}) {:allow-clojure-reader? true
|
|
||||||
:allow-java-serializable? true}))
|
|
||||||
5
test/taoensso/graal_tests.clj
Normal file
5
test/taoensso/graal_tests.clj
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(ns taoensso.graal-tests
|
||||||
|
(:require [taoensso.nippy :as nippy])
|
||||||
|
(:gen-class))
|
||||||
|
|
||||||
|
(defn -main [& args] (println "Namespace loaded successfully"))
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
(ns taoensso.nippy.tests.main
|
|
||||||
(:require
|
|
||||||
[clojure.test :as test :refer [deftest testing is]]
|
|
||||||
[clojure.test.check :as tc]
|
|
||||||
[clojure.test.check.generators :as tc-gens]
|
|
||||||
[clojure.test.check.properties :as tc-props]
|
|
||||||
[taoensso.encore :as enc :refer []]
|
|
||||||
[taoensso.nippy :as nippy :refer [freeze thaw]]
|
|
||||||
[taoensso.nippy.benchmarks :as benchmarks]))
|
|
||||||
|
|
||||||
(comment (test/run-tests))
|
|
||||||
|
|
||||||
(def test-data nippy/stress-data-comparable)
|
|
||||||
(def tc-num-tests 120)
|
|
||||||
(def tc-gens
|
|
||||||
"Like `tc-gens/any` but removes NaN (which breaks equality tests)"
|
|
||||||
(tc-gens/recursive-gen tc-gens/container-type #_simple-type
|
|
||||||
(tc-gens/one-of
|
|
||||||
[tc-gens/int tc-gens/large-integer #_tc-gens/double
|
|
||||||
(tc-gens/double* {:NaN? false})
|
|
||||||
tc-gens/char tc-gens/string tc-gens/ratio tc-gens/boolean tc-gens/keyword
|
|
||||||
tc-gens/keyword-ns tc-gens/symbol tc-gens/symbol-ns tc-gens/uuid])))
|
|
||||||
|
|
||||||
(comment (tc-gens/sample tc-gens 10))
|
|
||||||
|
|
||||||
;;;; Core
|
|
||||||
|
|
||||||
(deftest _core
|
|
||||||
(is (do (println (str "Clojure version: " *clojure-version*)) true))
|
|
||||||
(is (= test-data ((comp thaw freeze) test-data)))
|
|
||||||
(is (= test-data ((comp #(thaw % {:no-header? true
|
|
||||||
:compressor nippy/lz4-compressor
|
|
||||||
:encryptor nil})
|
|
||||||
#(freeze % {:no-header? true}))
|
|
||||||
test-data)))
|
|
||||||
|
|
||||||
(is (= test-data ((comp #(thaw % {:password [:salted "p"]})
|
|
||||||
#(freeze % {:password [:salted "p"]}))
|
|
||||||
test-data)))
|
|
||||||
|
|
||||||
(is (= (vec (:objects nippy/stress-data))
|
|
||||||
((comp vec thaw freeze) (:objects nippy/stress-data))))
|
|
||||||
|
|
||||||
(is (= test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor})
|
|
||||||
#(freeze % {:compressor nippy/lzma2-compressor}))
|
|
||||||
test-data)))
|
|
||||||
|
|
||||||
(is (= test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor
|
|
||||||
:password [:salted "p"]})
|
|
||||||
#(freeze % {:compressor nippy/lzma2-compressor
|
|
||||||
:password [:salted "p"]}))
|
|
||||||
test-data)))
|
|
||||||
|
|
||||||
(is (= test-data ((comp #(thaw % {:compressor nippy/lz4-compressor})
|
|
||||||
#(freeze % {:compressor nippy/lz4hc-compressor}))
|
|
||||||
test-data)))
|
|
||||||
|
|
||||||
(is ; Try roundtrip anything that simple-check can dream up
|
|
||||||
(:result (tc/quick-check tc-num-tests
|
|
||||||
(tc-props/for-all [val tc-gens]
|
|
||||||
(= val (thaw (freeze val)))))))
|
|
||||||
|
|
||||||
(is (thrown? Exception (thaw (freeze test-data {:password "malformed"}))))
|
|
||||||
(is (thrown? Exception (thaw (freeze test-data {:password [:salted "p"]})
|
|
||||||
{;; Necessary to prevent against JVM segfault due to
|
|
||||||
;; https://goo.gl/t0OUIo:
|
|
||||||
:v1-compatibility? false})))
|
|
||||||
(is (thrown? Exception (thaw (freeze test-data {:password [:salted "p"]})
|
|
||||||
{:v1-compatibility? false ; Ref. https://goo.gl/t0OUIo
|
|
||||||
:compressor nil})))
|
|
||||||
|
|
||||||
(is ; Snappy lib compatibility (for legacy versions of Nippy)
|
|
||||||
(let [^bytes raw-ba (freeze test-data {:compressor nil})
|
|
||||||
^bytes xerial-ba (org.xerial.snappy.Snappy/compress raw-ba)
|
|
||||||
^bytes iq80-ba (org.iq80.snappy.Snappy/compress raw-ba)]
|
|
||||||
(= (thaw raw-ba)
|
|
||||||
(thaw (org.xerial.snappy.Snappy/uncompress xerial-ba))
|
|
||||||
(thaw (org.xerial.snappy.Snappy/uncompress iq80-ba))
|
|
||||||
(thaw (org.iq80.snappy.Snappy/uncompress iq80-ba 0 (alength iq80-ba)))
|
|
||||||
(thaw (org.iq80.snappy.Snappy/uncompress xerial-ba 0 (alength xerial-ba))))))
|
|
||||||
|
|
||||||
(is ; CBC auto-encryptor compatibility
|
|
||||||
(= "payload"
|
|
||||||
(thaw (freeze "payload" {:password [:salted "pwd"] :encryptor nippy/aes128-cbc-encryptor})
|
|
||||||
(do {:password [:salted "pwd"]})))))
|
|
||||||
|
|
||||||
;;;; Custom types & records
|
|
||||||
|
|
||||||
(deftype MyType [basic_field fancy-field!]) ; Note `fancy-field!` field name will be munged
|
|
||||||
(defrecord MyRec [basic_field fancy-field!])
|
|
||||||
|
|
||||||
(deftest _types
|
|
||||||
(testing "Extend to custom type"
|
|
||||||
(is
|
|
||||||
(thrown? Exception ; No thaw extension yet
|
|
||||||
(do
|
|
||||||
(alter-var-root #'nippy/*custom-readers* (constantly {}))
|
|
||||||
(nippy/extend-freeze MyType 1 [x s]
|
|
||||||
(.writeUTF s (.basic_field x))
|
|
||||||
(.writeUTF s (.fancy-field! x)))
|
|
||||||
|
|
||||||
(thaw (freeze (MyType. "basic" "fancy"))))))
|
|
||||||
|
|
||||||
(is
|
|
||||||
(do
|
|
||||||
(nippy/extend-thaw 1 [s] (MyType. (.readUTF s) (.readUTF s)))
|
|
||||||
(let [mt1 (MyType. "basic" "fancy")
|
|
||||||
^MyType mt2 (thaw (freeze mt1))]
|
|
||||||
(=
|
|
||||||
[(.basic_field mt1) (.fancy-field! mt1)]
|
|
||||||
[(.basic_field mt2) (.fancy-field! mt2)])))))
|
|
||||||
|
|
||||||
(testing "Extend to custom Record"
|
|
||||||
(is
|
|
||||||
(do
|
|
||||||
(nippy/extend-freeze MyRec 2 [x s]
|
|
||||||
(.writeUTF s (str "foo-" (:basic_field x)))
|
|
||||||
(.writeUTF s (str "foo-" (:fancy-field! x))))
|
|
||||||
|
|
||||||
(nippy/extend-thaw 2 [s] (MyRec. (.readUTF s) (.readUTF s)))
|
|
||||||
(=
|
|
||||||
(do (MyRec. "foo-basic" "foo-fancy"))
|
|
||||||
(thaw (freeze (MyRec. "basic" "fancy")))))))
|
|
||||||
|
|
||||||
(testing "Keyword (prefixed) extensions"
|
|
||||||
(is
|
|
||||||
(do
|
|
||||||
(nippy/extend-freeze MyRec :nippy-tests/MyRec [x s]
|
|
||||||
(.writeUTF s (:basic_field x))
|
|
||||||
(.writeUTF s (:fancy-field! x)))
|
|
||||||
|
|
||||||
(nippy/extend-thaw :nippy-tests/MyRec [s] (MyRec. (.readUTF s) (.readUTF s)))
|
|
||||||
(let [mr (MyRec. "basic" "fancy")]
|
|
||||||
(= mr (thaw (freeze mr))))))))
|
|
||||||
|
|
||||||
;;;; Caching
|
|
||||||
|
|
||||||
(deftest _caching
|
|
||||||
(let [stress [nippy/stress-data-comparable
|
|
||||||
nippy/stress-data-comparable
|
|
||||||
nippy/stress-data-comparable
|
|
||||||
nippy/stress-data-comparable]
|
|
||||||
cached (mapv nippy/cache stress)
|
|
||||||
cached (mapv nippy/cache stress) ; <=1 wrap auto-enforced
|
|
||||||
]
|
|
||||||
|
|
||||||
(is (= stress (thaw (freeze stress {:compressor nil}))))
|
|
||||||
(is (= stress (thaw (freeze cached {:compressor nil}))))
|
|
||||||
(let [size-stress (count (freeze stress {:compressor nil}))
|
|
||||||
size-cached (count (freeze cached {:compressor nil}))]
|
|
||||||
(is (>= size-stress (* 3 size-cached)))
|
|
||||||
(is (< size-stress (* 4 size-cached))))))
|
|
||||||
|
|
||||||
(deftest _caching-metadata
|
|
||||||
(let [v1 (with-meta [] {:id :v1})
|
|
||||||
v2 (with-meta [] {:id :v2})
|
|
||||||
|
|
||||||
frozen-without-caching (freeze [v1 v2 v1 v2])
|
|
||||||
frozen-with-caching
|
|
||||||
(freeze [(nippy/cache v1)
|
|
||||||
(nippy/cache v2)
|
|
||||||
(nippy/cache v1)
|
|
||||||
(nippy/cache v2)])]
|
|
||||||
|
|
||||||
(is (> (count frozen-without-caching)
|
|
||||||
(count frozen-with-caching)))
|
|
||||||
|
|
||||||
(is (= (thaw frozen-without-caching)
|
|
||||||
(thaw frozen-with-caching)))
|
|
||||||
|
|
||||||
(is (= (mapv meta (thaw frozen-with-caching))
|
|
||||||
[{:id :v1} {:id :v2} {:id :v1} {:id :v2}]))))
|
|
||||||
|
|
||||||
;;;; Stable binary representation of vals
|
|
||||||
|
|
||||||
(deftest _stable-bin
|
|
||||||
|
|
||||||
(is (= (seq (freeze test-data))
|
|
||||||
(seq (freeze test-data)))) ; f(x)=f(y) | x=y
|
|
||||||
|
|
||||||
;; As above, but try multiple times to catch possible protocol interface races:
|
|
||||||
(is (every? true?
|
|
||||||
(repeatedly 1000 (fn [] (= (seq (freeze test-data))
|
|
||||||
(seq (freeze test-data)))))))
|
|
||||||
|
|
||||||
;; NB abandoning - no way to do this reliably w/o appropriate contracts from
|
|
||||||
;; (seq <unordered-coll>):
|
|
||||||
;;
|
|
||||||
;; (is (= (seq (-> test-data freeze))
|
|
||||||
;; (seq (-> test-data freeze thaw freeze)))) ; f(x)=f(f-1(f(x)))
|
|
||||||
;;
|
|
||||||
;; As above, but with repeated refreeze to catch possible protocol interface races:
|
|
||||||
;; (is (= (seq (freeze test-data))
|
|
||||||
;; (seq (reduce (fn [frozen _] (freeze (thaw frozen)))
|
|
||||||
;; (freeze test-data) (range 1000)))))
|
|
||||||
)
|
|
||||||
|
|
||||||
(defn qc-prop-bijection [& [n]]
|
|
||||||
(let [bin->val (atom {})
|
|
||||||
val->bin (atom {})]
|
|
||||||
(merge
|
|
||||||
(tc/quick-check (or n 1)
|
|
||||||
(tc-props/for-all [val tc-gens]
|
|
||||||
(let [;; Nb need `seq` for Clojure hash equality:
|
|
||||||
bin (hash (seq (freeze val)))]
|
|
||||||
(and
|
|
||||||
(if (contains? val->bin val)
|
|
||||||
(= (get val->bin val) bin) ; x=y => f(x)=f(y) by clj=
|
|
||||||
(do (swap! val->bin assoc val bin)
|
|
||||||
true))
|
|
||||||
|
|
||||||
(if (contains? bin->val bin)
|
|
||||||
(= (get bin->val bin) val) ; f(x)=f(y) => x=y by clj=
|
|
||||||
(do (swap! bin->val assoc bin val)
|
|
||||||
true))))))
|
|
||||||
#_{:bin->val @bin->val
|
|
||||||
:val->bin @val->bin}
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(comment
|
|
||||||
(tc-gens/sample tc-gens 10)
|
|
||||||
(:result (qc-prop-bijection 80))
|
|
||||||
(let [{:keys [result bin->val val->bin]} (qc-prop-bijection 10)]
|
|
||||||
[result (vals bin->val)]))
|
|
||||||
|
|
||||||
(deftest _gc-prop-bijection
|
|
||||||
(is (:result (qc-prop-bijection tc-num-tests))))
|
|
||||||
|
|
||||||
;;;; Thread safety
|
|
||||||
|
|
||||||
(deftest _thread-safe
|
|
||||||
(is
|
|
||||||
(let [futures
|
|
||||||
(mapv
|
|
||||||
(fn [_]
|
|
||||||
(future
|
|
||||||
(= (thaw (freeze test-data)) test-data)))
|
|
||||||
(range 50))]
|
|
||||||
(every? deref futures)))
|
|
||||||
|
|
||||||
(is
|
|
||||||
(let [futures
|
|
||||||
(mapv
|
|
||||||
(fn [_]
|
|
||||||
(future
|
|
||||||
(= (thaw (freeze test-data {:password [:salted "password"]})
|
|
||||||
{:password [:salted "password"]})
|
|
||||||
test-data)))
|
|
||||||
(range 50))]
|
|
||||||
(every? deref futures)))
|
|
||||||
|
|
||||||
(is
|
|
||||||
(let [futures
|
|
||||||
(mapv
|
|
||||||
(fn [_]
|
|
||||||
(future
|
|
||||||
(= (thaw (freeze test-data {:password [:cached "password"]})
|
|
||||||
{:password [:cached "password"]})
|
|
||||||
test-data)))
|
|
||||||
(range 50))]
|
|
||||||
(every? deref futures))))
|
|
||||||
|
|
||||||
;;;; Redefs
|
|
||||||
|
|
||||||
(defrecord MyFoo [] Object (toString [_] "v1"))
|
|
||||||
(str (thaw (freeze (MyFoo.))))
|
|
||||||
(defrecord MyFoo [] Object (toString [_] "v2"))
|
|
||||||
|
|
||||||
(deftest _redefs
|
|
||||||
(is (= (str (thaw (freeze (MyFoo.)))) "v2")))
|
|
||||||
|
|
||||||
;;;; Serializable
|
|
||||||
|
|
||||||
(do
|
|
||||||
(def ^:private semcn "java.util.concurrent.Semaphore")
|
|
||||||
(def ^:private sem (java.util.concurrent.Semaphore. 1))
|
|
||||||
(defn- sem? [x] (instance? java.util.concurrent.Semaphore x)))
|
|
||||||
|
|
||||||
(deftest _serializable
|
|
||||||
(is (= nippy/*thaw-serializable-allowlist* #{"base.1" "base.2" "add.1" "add.2"})
|
|
||||||
"JVM properties override initial allowlist values")
|
|
||||||
|
|
||||||
(is (thrown? Exception (nippy/freeze sem {:serializable-allowlist #{}}))
|
|
||||||
"Can't freeze Serializable objects unless approved by allowlist")
|
|
||||||
|
|
||||||
(is (sem?
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze sem {:serializable-allowlist #{semcn}})
|
|
||||||
{:serializable-allowlist #{semcn}}))
|
|
||||||
|
|
||||||
"Can freeze and thaw Serializable objects if approved by allowlist")
|
|
||||||
|
|
||||||
(is (sem?
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze sem {:serializable-allowlist #{"java.util.concurrent.*"}})
|
|
||||||
{:serializable-allowlist #{"java.util.concurrent.*"}}))
|
|
||||||
|
|
||||||
"Strings in allowlist sets may contain \"*\" wildcards")
|
|
||||||
|
|
||||||
(let [ba (nippy/freeze sem #_{:serializable-allowlist "*"})
|
|
||||||
thawed (nippy/thaw ba {:serializable-allowlist #{}})]
|
|
||||||
|
|
||||||
(is (= :quarantined (get-in thawed [:nippy/unthawable :cause]))
|
|
||||||
"Serializable objects will be quarantined when approved for freezing but not thawing.")
|
|
||||||
|
|
||||||
(is (sem? (nippy/read-quarantined-serializable-object-unsafe! thawed))
|
|
||||||
"Quarantined Serializable objects can still be manually force-read.")
|
|
||||||
|
|
||||||
(is (sem? (nippy/read-quarantined-serializable-object-unsafe!
|
|
||||||
(nippy/thaw (nippy/freeze thawed))))
|
|
||||||
"Quarantined Serializable objects are themselves safely transportable."))
|
|
||||||
|
|
||||||
(let [obj
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze sem)
|
|
||||||
{:serializable-allowlist "allow-and-record"})]
|
|
||||||
|
|
||||||
(is (sem? obj)
|
|
||||||
"Special \"allow-and-record\" allowlist permits any class")
|
|
||||||
|
|
||||||
(is
|
|
||||||
(contains? (nippy/get-recorded-serializable-classes) semcn)
|
|
||||||
"Special \"allow-and-record\" allowlist records classes")))
|
|
||||||
|
|
||||||
;;;; Metadata
|
|
||||||
|
|
||||||
(deftest _metadata
|
|
||||||
|
|
||||||
(is
|
|
||||||
(:has-meta?
|
|
||||||
(meta
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? true})
|
|
||||||
{:incl-metadata? true}
|
|
||||||
)))
|
|
||||||
|
|
||||||
"Metadata successfully included")
|
|
||||||
|
|
||||||
(is
|
|
||||||
(nil?
|
|
||||||
(meta
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? true})
|
|
||||||
{:incl-metadata? false}
|
|
||||||
)))
|
|
||||||
|
|
||||||
"Metadata successfully excluded by thaw")
|
|
||||||
|
|
||||||
(is
|
|
||||||
(nil?
|
|
||||||
(meta
|
|
||||||
(nippy/thaw
|
|
||||||
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? false})
|
|
||||||
{:incl-metadata? true}
|
|
||||||
)))
|
|
||||||
|
|
||||||
"Metadata successfully excluded by freeze"))
|
|
||||||
|
|
||||||
;;;; Benchmarks
|
|
||||||
|
|
||||||
(deftest _benchmarks
|
|
||||||
(is (benchmarks/bench {})) ; Also tests :cached passwords
|
|
||||||
)
|
|
||||||
207
test/taoensso/nippy_benchmarks.clj
Normal file
207
test/taoensso/nippy_benchmarks.clj
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
(ns taoensso.nippy-benchmarks
|
||||||
|
(:require
|
||||||
|
[clojure.data.fressian :as fress]
|
||||||
|
[taoensso.encore :as enc]
|
||||||
|
[taoensso.nippy :as nippy]
|
||||||
|
[taoensso.nippy.compression :as compr]))
|
||||||
|
|
||||||
|
;;;; Reader
|
||||||
|
|
||||||
|
(defn- freeze-reader [x] (enc/pr-edn x))
|
||||||
|
(defn- thaw-reader [edn] (enc/read-edn edn))
|
||||||
|
|
||||||
|
;;;; Fressian
|
||||||
|
|
||||||
|
(defn- freeze-fress [x]
|
||||||
|
(let [^java.nio.ByteBuffer bb (fress/write x)
|
||||||
|
len (.remaining bb)
|
||||||
|
ba (byte-array len)]
|
||||||
|
(.get bb ba 0 len)
|
||||||
|
(do ba)))
|
||||||
|
|
||||||
|
(defn- thaw-fress [^bytes ba]
|
||||||
|
(let [bb (java.nio.ByteBuffer/wrap ba)]
|
||||||
|
(fress/read bb)))
|
||||||
|
|
||||||
|
(comment (-> data freeze-fress thaw-fress))
|
||||||
|
|
||||||
|
;;;; Bench data
|
||||||
|
|
||||||
|
(def default-bench-data
|
||||||
|
"Subset of stress data suitable for benching."
|
||||||
|
(let [sd (nippy/stress-data {:comparable? true})]
|
||||||
|
(reduce-kv
|
||||||
|
(fn [m k v]
|
||||||
|
(try
|
||||||
|
(-> v freeze-reader thaw-reader)
|
||||||
|
(-> v freeze-fress thaw-fress)
|
||||||
|
m
|
||||||
|
(catch Throwable _ (dissoc m k))))
|
||||||
|
sd sd)))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(clojure.set/difference
|
||||||
|
(set (keys (nippy/stress-data {:comparable? true})))
|
||||||
|
(set (keys default-bench-data))))
|
||||||
|
|
||||||
|
;;;; Serialization
|
||||||
|
|
||||||
|
(defn- bench1-serialization
|
||||||
|
[freezer thawer sizer
|
||||||
|
{:keys [laps warmup bench-data]
|
||||||
|
:or
|
||||||
|
{laps 1e4
|
||||||
|
warmup 25e3
|
||||||
|
bench-data default-bench-data}}]
|
||||||
|
|
||||||
|
(let [data-frozen (freezer bench-data)
|
||||||
|
time-freeze (enc/bench laps {:warmup-laps warmup} (freezer bench-data))
|
||||||
|
time-thaw (enc/bench laps {:warmup-laps warmup} (thawer data-frozen))
|
||||||
|
data-size (sizer data-frozen)]
|
||||||
|
|
||||||
|
{:round (+ time-freeze time-thaw)
|
||||||
|
:freeze time-freeze
|
||||||
|
:thaw time-thaw
|
||||||
|
:size data-size}))
|
||||||
|
|
||||||
|
(comment (bench1-serialization nippy/freeze nippy/thaw count {}))
|
||||||
|
|
||||||
|
(defn- printed-results [results]
|
||||||
|
(println "\nBenchmark results:")
|
||||||
|
(doseq [[k v] results] (println k " " v))
|
||||||
|
(do results))
|
||||||
|
|
||||||
|
(defn bench-serialization
|
||||||
|
[{:keys [all? reader? fressian? fressian? lzma2? laps warmup bench-data]
|
||||||
|
:as opts
|
||||||
|
:or
|
||||||
|
{laps 1e4
|
||||||
|
warmup 25e3}}]
|
||||||
|
|
||||||
|
(println "\nRunning benchmarks...")
|
||||||
|
|
||||||
|
(let [results_ (atom {})]
|
||||||
|
(when (or all? reader?)
|
||||||
|
(println " With Reader...")
|
||||||
|
(swap! results_ assoc :reader
|
||||||
|
(bench1-serialization freeze-reader thaw-reader
|
||||||
|
(fn [^String s] (count (.getBytes s "UTF-8")))
|
||||||
|
(assoc opts :laps laps, :warmup warmup))))
|
||||||
|
|
||||||
|
(when (or all? fressian?)
|
||||||
|
(println " With Fressian...")
|
||||||
|
(swap! results_ assoc :fressian
|
||||||
|
(bench1-serialization freeze-fress thaw-fress count
|
||||||
|
(assoc opts :laps laps, :warmup warmup))))
|
||||||
|
|
||||||
|
(when (or all? lzma2?)
|
||||||
|
(println " With Nippy/LZMA2...")
|
||||||
|
(swap! results_ assoc :nippy/lzma2
|
||||||
|
(bench1-serialization
|
||||||
|
#(nippy/freeze % {:compressor nippy/lzma2-compressor})
|
||||||
|
#(nippy/thaw % {:compressor nippy/lzma2-compressor})
|
||||||
|
count
|
||||||
|
(assoc opts :laps laps, :warmup warmup))))
|
||||||
|
|
||||||
|
(println " With Nippy/encrypted...")
|
||||||
|
(swap! results_ assoc :nippy/encrypted
|
||||||
|
(bench1-serialization
|
||||||
|
#(nippy/freeze % {:password [:cached "p"]})
|
||||||
|
#(nippy/thaw % {:password [:cached "p"]})
|
||||||
|
count
|
||||||
|
(assoc opts :laps laps, :warmup warmup)))
|
||||||
|
|
||||||
|
(println " With Nippy/default...")
|
||||||
|
(swap! results_ assoc :nippy/default
|
||||||
|
(bench1-serialization nippy/freeze nippy/thaw count
|
||||||
|
(assoc opts :laps laps, :warmup warmup)))
|
||||||
|
|
||||||
|
(println " With Nippy/fast...")
|
||||||
|
(swap! results_ assoc :nippy/fast
|
||||||
|
(bench1-serialization nippy/fast-freeze nippy/fast-thaw count
|
||||||
|
(assoc opts :laps laps, :warmup warmup)))
|
||||||
|
|
||||||
|
(println "\nBenchmarks done:")
|
||||||
|
(printed-results @results_)))
|
||||||
|
|
||||||
|
;;;; Compression
|
||||||
|
|
||||||
|
(defn- bench1-compressor
|
||||||
|
[compressor
|
||||||
|
{:keys [laps warmup bench-data]
|
||||||
|
:or
|
||||||
|
{laps 1e4
|
||||||
|
warmup 2e4
|
||||||
|
bench-data default-bench-data}}]
|
||||||
|
|
||||||
|
(let [data-frozen (nippy/freeze bench-data {:compressor nil})
|
||||||
|
data-compressed (compr/compress compressor data-frozen)
|
||||||
|
time-compress (enc/bench laps {:warmup-laps warmup} (compr/compress compressor data-frozen))
|
||||||
|
time-decompress (enc/bench laps {:warmup-laps warmup} (compr/decompress compressor data-compressed))]
|
||||||
|
|
||||||
|
{:round (+ time-compress time-decompress)
|
||||||
|
:compress time-compress
|
||||||
|
:decompress time-decompress
|
||||||
|
:ratio (enc/round2 (/ (count data-compressed) (count data-frozen)))}))
|
||||||
|
|
||||||
|
(defn bench-compressors [opts lzma-opts]
|
||||||
|
(printed-results
|
||||||
|
(merge
|
||||||
|
(let [bench1 #(bench1-compressor % opts)]
|
||||||
|
{:zstd/prepended (bench1 (compr/->ZstdCompressor true))
|
||||||
|
:zstd/unprepended (bench1 (compr/->ZstdCompressor false))
|
||||||
|
:lz4 (bench1 (compr/->LZ4Compressor))
|
||||||
|
:lzo (bench1 (compr/->LZOCompressor))
|
||||||
|
:snappy/prepended (bench1 (compr/->SnappyCompressor true))
|
||||||
|
:snappy/unprepended (bench1 (compr/->SnappyCompressor false))})
|
||||||
|
|
||||||
|
(let [bench1 #(bench1-compressor % (merge opts lzma-opts))]
|
||||||
|
{:lzma2/level0 (bench1 (compr/->LZMA2Compressor 0))
|
||||||
|
:lzma2/level3 (bench1 (compr/->LZMA2Compressor 3))
|
||||||
|
:lzma2/level6 (bench1 (compr/->LZMA2Compressor 6))
|
||||||
|
:lzma2/level9 (bench1 (compr/->LZMA2Compressor 9))}))))
|
||||||
|
|
||||||
|
;;;; Results
|
||||||
|
|
||||||
|
(comment
|
||||||
|
{:last-updated "2024-01-16"
|
||||||
|
:system "2020 Macbook Pro M1, 16 GB memory"
|
||||||
|
:clojure-version "1.11.1"
|
||||||
|
:java-version "OpenJDK 21"
|
||||||
|
:deps
|
||||||
|
'[[com.taoensso/nippy "3.4.0-beta1"]
|
||||||
|
[org.clojure/tools.reader "1.3.7"]
|
||||||
|
[org.clojure/data.fressian "1.0.0"]
|
||||||
|
[org.tukaani/xz "1.9"]
|
||||||
|
[io.airlift/aircompressor "0.25"]]}
|
||||||
|
|
||||||
|
(bench-serialization {:all? true})
|
||||||
|
|
||||||
|
{:reader {:round 13496, :freeze 5088, :thaw 8408, :size 15880}
|
||||||
|
:fressian {:round 3898, :freeze 2350, :thaw 1548, :size 12222}
|
||||||
|
:nippy/lzma2 {:round 12341, :freeze 7809, :thaw 4532, :size 3916}
|
||||||
|
:nippy/encrypted {:round 2939, :freeze 1505, :thaw 1434, :size 8547}
|
||||||
|
:nippy/default {:round 2704, :freeze 1330, :thaw 1374, :size 8519}
|
||||||
|
:nippy/fast {:round 2425, :freeze 1117, :thaw 1308, :size 17088}}
|
||||||
|
|
||||||
|
(enc/round2 (/ 2704 13496)) ; 0.20 of reader roundtrip time
|
||||||
|
(enc/round2 (/ 2704 3898)) ; 0.69 of fressian roundtrip time
|
||||||
|
|
||||||
|
(enc/round2 (/ 8519 15880)) ; 0.54 of reader output size
|
||||||
|
(enc/round2 (/ 8519 12222)) ; 0.70 of fressian output size
|
||||||
|
|
||||||
|
(bench-compressors
|
||||||
|
{:laps 1e4 :warmup 2e4}
|
||||||
|
{:laps 1e2 :warmup 2e2})
|
||||||
|
|
||||||
|
;; Note that ratio depends on compressibility of stress data
|
||||||
|
{:lz4 {:round 293, :compress 234, :decompress 59, :ratio 0.5}
|
||||||
|
:lzo {:round 483, :compress 349, :decompress 134, :ratio 0.46}
|
||||||
|
:snappy/prepended {:round 472, :compress 296, :decompress 176, :ratio 0.43}
|
||||||
|
:snappy/unprepended {:round 420, :compress 260, :decompress 160, :ratio 0.43}
|
||||||
|
:zstd/prepended {:round 2105, :compress 1419, :decompress 686, :ratio 0.3}
|
||||||
|
:zstd/unprepended {:round 1261, :compress 921, :decompress 340, :ratio 0.3}
|
||||||
|
:lzma2/level0 {:round 158, :compress 121, :decompress 37, :ratio 0.24}
|
||||||
|
:lzma2/level3 {:round 536, :compress 436, :decompress 100, :ratio 0.22}
|
||||||
|
:lzma2/level6 {:round 1136, :compress 1075, :decompress 61, :ratio 0.21}
|
||||||
|
:lzma2/level9 {:round 2391, :compress 2096, :decompress 295, :ratio 0.21}})
|
||||||
471
test/taoensso/nippy_tests.clj
Normal file
471
test/taoensso/nippy_tests.clj
Normal file
|
|
@ -0,0 +1,471 @@
|
||||||
|
(ns taoensso.nippy-tests
|
||||||
|
(:require
|
||||||
|
[clojure.test :as test :refer [deftest testing is]]
|
||||||
|
[clojure.test.check :as tc]
|
||||||
|
[clojure.test.check.generators :as tc-gens]
|
||||||
|
[clojure.test.check.properties :as tc-props]
|
||||||
|
[taoensso.truss :as truss :refer [throws?]]
|
||||||
|
[taoensso.encore :as enc :refer [ba=]]
|
||||||
|
[taoensso.nippy :as nippy :refer [freeze thaw]]
|
||||||
|
[taoensso.nippy.impl :as impl]
|
||||||
|
[taoensso.nippy.tools :as tools]
|
||||||
|
[taoensso.nippy.compression :as compr]
|
||||||
|
[taoensso.nippy.crypto :as crypto]
|
||||||
|
[taoensso.nippy-benchmarks :as benchmarks]))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(remove-ns 'taoensso.nippy-tests)
|
||||||
|
(test/run-tests 'taoensso.nippy-tests))
|
||||||
|
|
||||||
|
;;;; Config, etc.
|
||||||
|
|
||||||
|
(def test-data (nippy/stress-data {:comparable? true}))
|
||||||
|
(def tc-gen-recursive-any-equatable
|
||||||
|
(tc-gens/recursive-gen tc-gens/container-type
|
||||||
|
tc-gens/any-equatable))
|
||||||
|
|
||||||
|
(defmacro gen-test [num-tests [data-sym] & body]
|
||||||
|
`(let [tc-result#
|
||||||
|
(tc/quick-check ~num-tests
|
||||||
|
(tc-props/for-all [~data-sym tc-gen-recursive-any-equatable]
|
||||||
|
~@body))]
|
||||||
|
(true? (:pass? tc-result#))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(tc-gens/sample tc-gen-recursive-any-equatable 10)
|
||||||
|
(gen-test 10 [gen-data] true))
|
||||||
|
|
||||||
|
;;;; Core
|
||||||
|
|
||||||
|
(deftest _core
|
||||||
|
(println (str "Clojure version: " *clojure-version*))
|
||||||
|
[(is (= test-data test-data) "Test data is comparable")
|
||||||
|
(is (=
|
||||||
|
(nippy/stress-data {:comparable? true})
|
||||||
|
(nippy/stress-data {:comparable? true}))
|
||||||
|
"Stress data is deterministic")
|
||||||
|
|
||||||
|
(is (= test-data ((comp thaw freeze) test-data)))
|
||||||
|
(is (= test-data ((comp #(thaw % {:no-header? true
|
||||||
|
:compressor nippy/lz4-compressor
|
||||||
|
:encryptor nil})
|
||||||
|
#(freeze % {:no-header? true}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (= test-data ((comp #(thaw % {:password [:salted "p"]})
|
||||||
|
#(freeze % {:password [:salted "p"]}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (= test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor})
|
||||||
|
#(freeze % {:compressor nippy/lzma2-compressor}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (= test-data ((comp #(thaw % {:compressor nippy/lzma2-compressor
|
||||||
|
:password [:salted "p"]})
|
||||||
|
#(freeze % {:compressor nippy/lzma2-compressor
|
||||||
|
:password [:salted "p"]}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (= test-data ((comp #(thaw % {:compressor nippy/lz4-compressor})
|
||||||
|
#(freeze % {:compressor nippy/lz4-compressor}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (= test-data ((comp #(thaw % {:compressor nippy/zstd-compressor})
|
||||||
|
#(freeze % {:compressor nippy/zstd-compressor}))
|
||||||
|
test-data)))
|
||||||
|
|
||||||
|
(is (throws? Exception (thaw (freeze test-data {:password "malformed"}))))
|
||||||
|
(is (throws? Exception (thaw (freeze test-data {:password [:salted "p"]}))))
|
||||||
|
(is (throws? Exception (thaw (freeze test-data {:password [:salted "p"]}))))
|
||||||
|
|
||||||
|
(is
|
||||||
|
(= "payload"
|
||||||
|
(thaw (freeze "payload" {:password [:salted "pwd"] :encryptor nippy/aes128-cbc-encryptor})
|
||||||
|
(do {:password [:salted "pwd"]})))
|
||||||
|
"CBC auto-encryptor compatibility")
|
||||||
|
|
||||||
|
(testing "Unsigned long types"
|
||||||
|
(let [range-ushort+ (+ (long @#'nippy/range-ushort) 128)
|
||||||
|
range-uint+ (+ (long @#'nippy/range-uint) 128)]
|
||||||
|
|
||||||
|
[(let [r (range (long -2.5e6) (long 2.5e6))] (= (thaw (freeze r)) r))
|
||||||
|
(let [r (range (- range-ushort+) range-ushort+)] (= (thaw (freeze r)) r))
|
||||||
|
(let [n range-uint+] (= (thaw (freeze n)) n))
|
||||||
|
(let [n (- range-uint+)] (= (thaw (freeze n)) n))]))
|
||||||
|
|
||||||
|
(is (throws? :ex-info "Failed to freeze type" (nippy/freeze (fn []))))
|
||||||
|
|
||||||
|
(testing "Clojure v1.10+ metadata protocol extensions"
|
||||||
|
[(is (throws? :ex-info "Failed to freeze type" (nippy/freeze (with-meta [] {:a :A, 'b (fn [])}))))
|
||||||
|
(is (= {:a :A} (meta (nippy/thaw (nippy/freeze (with-meta [] {:a :A, 'b/c (fn [])}))))))
|
||||||
|
(is (= nil (meta (nippy/thaw (nippy/freeze (with-meta [] { 'b/c (fn [])})))))
|
||||||
|
"Don't attach empty metadata")])
|
||||||
|
|
||||||
|
(let [d (nippy/stress-data {})]
|
||||||
|
[(is (= (vec (:bytes d)) ((comp vec thaw freeze) (:bytes d))))
|
||||||
|
(is (= (vec (:objects d)) ((comp vec thaw freeze) (:objects d))))])
|
||||||
|
|
||||||
|
(testing "Arrays"
|
||||||
|
(binding [nippy/*thaw-serializable-allowlist* nippy/default-freeze-serializable-allowlist]
|
||||||
|
(mapv (fn [[k aval]] (is (= (vec aval) (-> aval nippy/freeze nippy/thaw vec)) (name k)))
|
||||||
|
(get-in (nippy/stress-data {}) [:non-comparable :arrays]))))
|
||||||
|
|
||||||
|
(is (gen-test 1600 [gen-data] (= gen-data (thaw (freeze gen-data)))) "Generative")])
|
||||||
|
|
||||||
|
;;;; Custom types & records
|
||||||
|
|
||||||
|
(deftype MyType [basic_field fancy-field!]) ; Note `fancy-field!` field name will be munged
|
||||||
|
(defrecord MyRec [basic_field fancy-field!])
|
||||||
|
|
||||||
|
(deftest _types
|
||||||
|
[(testing "Extend to custom type"
|
||||||
|
[(is
|
||||||
|
(throws? Exception ; No thaw extension yet
|
||||||
|
(do
|
||||||
|
(alter-var-root #'nippy/*custom-readers* (constantly {}))
|
||||||
|
(nippy/extend-freeze MyType 1 [x s]
|
||||||
|
(.writeUTF s (.basic_field x))
|
||||||
|
(.writeUTF s (.fancy-field! x)))
|
||||||
|
|
||||||
|
(thaw (freeze (MyType. "basic" "fancy"))))))
|
||||||
|
|
||||||
|
(is
|
||||||
|
(do
|
||||||
|
(nippy/extend-thaw 1 [s] (MyType. (.readUTF s) (.readUTF s)))
|
||||||
|
(let [mt1 (MyType. "basic" "fancy")
|
||||||
|
^MyType mt2 (thaw (freeze mt1))]
|
||||||
|
(=
|
||||||
|
[(.basic_field mt1) (.fancy-field! mt1)]
|
||||||
|
[(.basic_field mt2) (.fancy-field! mt2)]))))])
|
||||||
|
|
||||||
|
(testing "Extend to custom Record"
|
||||||
|
(is
|
||||||
|
(do
|
||||||
|
(nippy/extend-freeze MyRec 2 [x s]
|
||||||
|
(.writeUTF s (str "foo-" (:basic_field x)))
|
||||||
|
(.writeUTF s (str "foo-" (:fancy-field! x))))
|
||||||
|
|
||||||
|
(nippy/extend-thaw 2 [s] (MyRec. (.readUTF s) (.readUTF s)))
|
||||||
|
(=
|
||||||
|
(do (MyRec. "foo-basic" "foo-fancy"))
|
||||||
|
(thaw (freeze (MyRec. "basic" "fancy")))))))
|
||||||
|
|
||||||
|
(testing "Keyword (prefixed) extensions"
|
||||||
|
(is
|
||||||
|
(do
|
||||||
|
(nippy/extend-freeze MyRec :nippy-tests/MyRec [x s]
|
||||||
|
(.writeUTF s (:basic_field x))
|
||||||
|
(.writeUTF s (:fancy-field! x)))
|
||||||
|
|
||||||
|
(nippy/extend-thaw :nippy-tests/MyRec [s] (MyRec. (.readUTF s) (.readUTF s)))
|
||||||
|
(let [mr (MyRec. "basic" "fancy")]
|
||||||
|
(= mr (thaw (freeze mr)))))))])
|
||||||
|
|
||||||
|
;;;; Caching
|
||||||
|
|
||||||
|
(deftest _caching
|
||||||
|
(let [test-data* [test-data test-data test-data test-data] ; Data with duplicates
|
||||||
|
cached (mapv nippy/cache test-data*)
|
||||||
|
cached (mapv nippy/cache test-data*) ; <=1 wrap auto-enforced
|
||||||
|
]
|
||||||
|
|
||||||
|
[(is (= test-data* (thaw (freeze test-data* {:compressor nil}))))
|
||||||
|
(is (= test-data* (thaw (freeze cached {:compressor nil}))))
|
||||||
|
(let [size-stress (count (freeze test-data* {:compressor nil}))
|
||||||
|
size-cached (count (freeze cached {:compressor nil}))]
|
||||||
|
(is (>= size-stress (* 3 size-cached)))
|
||||||
|
(is (< size-stress (* 4 size-cached))))]))
|
||||||
|
|
||||||
|
(deftest _caching-metadata
|
||||||
|
(let [v1 (with-meta [] {:id :v1})
|
||||||
|
v2 (with-meta [] {:id :v2})
|
||||||
|
|
||||||
|
frozen-without-caching (freeze [v1 v2 v1 v2])
|
||||||
|
frozen-with-caching
|
||||||
|
(freeze [(nippy/cache v1)
|
||||||
|
(nippy/cache v2)
|
||||||
|
(nippy/cache v1)
|
||||||
|
(nippy/cache v2)])]
|
||||||
|
|
||||||
|
[(is (> (count frozen-without-caching)
|
||||||
|
(count frozen-with-caching)))
|
||||||
|
|
||||||
|
(is (= (thaw frozen-without-caching)
|
||||||
|
(thaw frozen-with-caching)))
|
||||||
|
|
||||||
|
(is (= (mapv meta (thaw frozen-with-caching))
|
||||||
|
[{:id :v1} {:id :v2} {:id :v1} {:id :v2}]))]))
|
||||||
|
|
||||||
|
;;;; Serialized output
|
||||||
|
|
||||||
|
(defn ba-hash [^bytes ba] (hash (seq ba)))
|
||||||
|
|
||||||
|
(defn gen-hashes [] (enc/map-vals (fn [v] (ba-hash (freeze v))) test-data))
|
||||||
|
(defn cmp-hashes [new old] (vec (sort (reduce-kv (fn [s k v] (if (= (get old k) v) s (conj s k))) #{} new))))
|
||||||
|
|
||||||
|
(def ref-hashes-v341
|
||||||
|
{:deftype -148586793, :lazy-seq-empty 1277437598, :true -1809580601, :long 598276629, :double -454270428, :lazy-seq -1039619789, :short 1152993378, :meta -858252893, :str-long -1970041891, :instant -1401948864, :many-keywords 665654816, :bigint 2033662230, :sym-ns 769802402, :queue 447747779, :float 603100813, :sorted-set 2005004017, :many-strings 1738215727, :nested -1350538572, :queue-empty 1760934486, :duration -775528642, :false 1506926383, :vector 813550992, :util-date 1326218051, :kw 389651898, :sym -1742024487, :str-short -921330463, :subvec 709331681, :kw-long 852232872, :integer 624865727, :sym-long -1535730190, :list -1207486853, :ratio 1186850097, :byte -1041979678, :bigdec -1846988137, :nil 2005042235, :defrecord -553848560, :sorted-map -1160380145, :sql-date 80018667, :map-entry 1219306839, :false-boxed 1506926383, :uri 870148616, :period -2043530540, :many-longs -1109794519, :uuid -338331115, :set 1649942133, :kw-ns 1050084331, :map 1989337680, :many-doubles -827569787, :char 858269588})
|
||||||
|
|
||||||
|
(def ref-hashes-v340
|
||||||
|
{:deftype 1529147805, :lazy-seq-empty 1277437598, :true -1809580601, :long 219451189, :double -454270428, :lazy-seq -1039619789, :short 1152993378, :meta 352218350, :str-long -1970041891, :instant -1401948864, :many-keywords 665654816, :bigint 2033662230, :sym-ns 769802402, :queue 447747779, :float 603100813, :sorted-set 1443292905, :many-strings 1777678883, :nested -1590473924, :queue-empty 1760934486, :duration -775528642, :false 1506926383, :vector 89425525, :util-date 1326218051, :kw 389651898, :sym -1742024487, :str-short -1097575232, :subvec -2047667173, :kw-long 852232872, :integer 624865727, :sym-long -1535730190, :list -1113199651, :ratio 1186850097, :byte -1041979678, :bigdec -1846988137, :nil 2005042235, :defrecord 287634761, :sorted-map 1464032648, :sql-date 80018667, :map-entry -1353323498, :false-boxed 1506926383, :uri -1374752165, :period -2043530540, :many-longs 759118414, :uuid -338331115, :set -1515144175, :kw-ns 1050084331, :map 358912619, :many-doubles -827569787, :char 858269588})
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(cmp-hashes ref-hashes-v341 ref-hashes-v340)
|
||||||
|
[:defrecord :deftype :list :long :many-longs :many-strings :map :map-entry :meta :nested :set :sorted-map :sorted-set :str-short :subvec :uri :vector])
|
||||||
|
|
||||||
|
(deftest _stable-serialized-output
|
||||||
|
(testing "Stable serialized output"
|
||||||
|
|
||||||
|
(testing "x=y => f(x)=f(y) for SOME inputs, SOMETIMES"
|
||||||
|
;; `x=y => f(x)=f(y)` is unfortunately NOT true in general, and NOT something we
|
||||||
|
;; promise. Still, we do unofficially try our best to maintain this property when
|
||||||
|
;; possible - and to warn when it'll be violated for common/elementary types.
|
||||||
|
[(is (not (ba= (freeze {:a 1 :b 1}) (freeze {:b 1 :a 1}))) "Small (array) map (not= (seq {:a 1 :b 1}) (seq {:b 1 :a 1}))")
|
||||||
|
(is (not (ba= (freeze [[]]) (freeze ['()]))) "(= [] '()) is true")
|
||||||
|
(is (ba= (freeze (sorted-map :a 1 :b 1))
|
||||||
|
(freeze (sorted-map :b 1 :a 1))) "Sorted structures are generally safe")
|
||||||
|
|
||||||
|
;; Track serialized output of stress data so that we can detect unintentional changes,
|
||||||
|
;; and warn about intended ones. Hashes will need to be recalculated on changes to stress data.
|
||||||
|
(let [reference-hashes ref-hashes-v341
|
||||||
|
failures ; #{{:keys [k v]}}
|
||||||
|
(reduce-kv
|
||||||
|
(fn [failures k v]
|
||||||
|
(or
|
||||||
|
(when (not= v :taoensso.nippy/skip)
|
||||||
|
(let [frozen (freeze v)
|
||||||
|
actual (ba-hash frozen)
|
||||||
|
ref (get reference-hashes k)]
|
||||||
|
(when (not= actual ref)
|
||||||
|
(conj failures
|
||||||
|
{:k k,
|
||||||
|
:v {:type (type v), :value v}
|
||||||
|
:actual actual
|
||||||
|
:ref ref
|
||||||
|
:frozen (vec frozen)}))))
|
||||||
|
failures))
|
||||||
|
#{}
|
||||||
|
test-data)]
|
||||||
|
|
||||||
|
(is (empty? failures)))])
|
||||||
|
|
||||||
|
(testing "x==y => f(x)=f(y)"
|
||||||
|
;; This weaker version of `x=y => f(x)=f(y)` does hold
|
||||||
|
[(is (ba= (freeze test-data)
|
||||||
|
(freeze test-data)))
|
||||||
|
|
||||||
|
(is (every? true? (repeatedly 1000 (fn [] (ba= (freeze test-data)
|
||||||
|
(freeze test-data)))))
|
||||||
|
"Try repeatedly to catch possible protocol interface races")
|
||||||
|
|
||||||
|
(is (gen-test 400 [gen-data]
|
||||||
|
(ba= (freeze gen-data)
|
||||||
|
(freeze gen-data))) "Generative")])
|
||||||
|
|
||||||
|
(testing "f(x)=f(f-1(f(x)))"
|
||||||
|
[(is (ba= (-> test-data freeze)
|
||||||
|
(-> test-data freeze thaw freeze)))
|
||||||
|
|
||||||
|
(is (ba= (freeze test-data)
|
||||||
|
(reduce (fn [frozen _] (freeze (thaw frozen))) (freeze test-data) (range 1000)))
|
||||||
|
"Try repeatedly to catch possible protocol interface races")
|
||||||
|
|
||||||
|
(is (gen-test 400 [gen-data]
|
||||||
|
(ba= (-> gen-data freeze)
|
||||||
|
(-> gen-data freeze thaw freeze))) "Generative")])
|
||||||
|
|
||||||
|
(testing "f(x)=f(y) => x=y"
|
||||||
|
(let [vals_ (atom {})]
|
||||||
|
(gen-test 400 [gen-data]
|
||||||
|
(let [out (freeze gen-data)
|
||||||
|
ref (get @vals_ gen-data ::nx)]
|
||||||
|
(swap! vals_ assoc out gen-data)
|
||||||
|
(or (= ref ::nx) (= ref out))))))))
|
||||||
|
|
||||||
|
;;;; Thread safety
|
||||||
|
|
||||||
|
(deftest _thread-safe
|
||||||
|
[(is
|
||||||
|
(let [futures (mapv (fn [_] (future (= (thaw (freeze test-data)) test-data)))
|
||||||
|
(range 50))]
|
||||||
|
(every? deref futures)))
|
||||||
|
|
||||||
|
(is
|
||||||
|
(let [futures
|
||||||
|
(mapv
|
||||||
|
(fn [_]
|
||||||
|
(future
|
||||||
|
(= (thaw (freeze test-data {:password [:salted "password"]})
|
||||||
|
{:password [:salted "password"]})
|
||||||
|
test-data)))
|
||||||
|
(range 50))]
|
||||||
|
(every? deref futures)))
|
||||||
|
|
||||||
|
(is
|
||||||
|
(let [futures
|
||||||
|
(mapv
|
||||||
|
(fn [_]
|
||||||
|
(future
|
||||||
|
(= (thaw (freeze test-data {:password [:cached "password"]})
|
||||||
|
{:password [:cached "password"]})
|
||||||
|
test-data)))
|
||||||
|
(range 50))]
|
||||||
|
(every? deref futures)))])
|
||||||
|
|
||||||
|
;;;; Redefs
|
||||||
|
|
||||||
|
(defrecord MyFoo [] Object (toString [_] "v1"))
|
||||||
|
(defrecord MyFoo [] Object (toString [_] "v2"))
|
||||||
|
|
||||||
|
(deftest _redefs
|
||||||
|
(is (= (str (thaw (freeze (MyFoo.)))) "v2")))
|
||||||
|
|
||||||
|
;;;; Serializable
|
||||||
|
|
||||||
|
(do
|
||||||
|
(def ^:private semcn "java.util.concurrent.Semaphore")
|
||||||
|
(def ^:private sem (java.util.concurrent.Semaphore. 1))
|
||||||
|
(defn- sem? [x] (instance? java.util.concurrent.Semaphore x)))
|
||||||
|
|
||||||
|
(deftest _serializable
|
||||||
|
[(is (= nippy/*thaw-serializable-allowlist* #{"base.1" "base.2" "add.1" "add.2"})
|
||||||
|
"JVM properties override initial allowlist values")
|
||||||
|
|
||||||
|
(is (throws? Exception (nippy/freeze sem {:serializable-allowlist #{}}))
|
||||||
|
"Can't freeze Serializable objects unless approved by allowlist")
|
||||||
|
|
||||||
|
(is (sem?
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze sem {:serializable-allowlist #{semcn}})
|
||||||
|
{:serializable-allowlist #{semcn}}))
|
||||||
|
|
||||||
|
"Can freeze and thaw Serializable objects if approved by allowlist")
|
||||||
|
|
||||||
|
(is (sem?
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze sem {:serializable-allowlist #{"java.util.concurrent.*"}})
|
||||||
|
{:serializable-allowlist #{"java.util.concurrent.*"}}))
|
||||||
|
|
||||||
|
"Strings in allowlist sets may contain \"*\" wildcards")
|
||||||
|
|
||||||
|
(let [ba (nippy/freeze sem #_{:serializable-allowlist "*"})
|
||||||
|
thawed (nippy/thaw ba {:serializable-allowlist #{}})]
|
||||||
|
|
||||||
|
[(is (= :quarantined (get-in thawed [:nippy/unthawable :cause]))
|
||||||
|
"Serializable objects will be quarantined when approved for freezing but not thawing.")
|
||||||
|
|
||||||
|
(is (sem? (nippy/read-quarantined-serializable-object-unsafe! thawed))
|
||||||
|
"Quarantined Serializable objects can still be manually force-read.")
|
||||||
|
|
||||||
|
(is (sem? (nippy/read-quarantined-serializable-object-unsafe!
|
||||||
|
(nippy/thaw (nippy/freeze thawed))))
|
||||||
|
"Quarantined Serializable objects are themselves safely transportable.")])
|
||||||
|
|
||||||
|
(let [obj
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze sem)
|
||||||
|
{:serializable-allowlist "allow-and-record"})]
|
||||||
|
|
||||||
|
[(is (sem? obj)
|
||||||
|
"Special \"allow-and-record\" allowlist permits any class")
|
||||||
|
|
||||||
|
(is
|
||||||
|
(contains? (nippy/get-recorded-serializable-classes) semcn)
|
||||||
|
"Special \"allow-and-record\" allowlist records classes")])])
|
||||||
|
|
||||||
|
;;;; Metadata
|
||||||
|
|
||||||
|
(def my-var "Just a string")
|
||||||
|
|
||||||
|
(deftest _metadata
|
||||||
|
[(is
|
||||||
|
(:has-meta?
|
||||||
|
(meta
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? true})
|
||||||
|
{:incl-metadata? true}
|
||||||
|
)))
|
||||||
|
|
||||||
|
"Metadata successfully included")
|
||||||
|
|
||||||
|
(is
|
||||||
|
(nil?
|
||||||
|
(meta
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? true})
|
||||||
|
{:incl-metadata? false}
|
||||||
|
)))
|
||||||
|
|
||||||
|
"Metadata successfully excluded by thaw")
|
||||||
|
|
||||||
|
(is
|
||||||
|
(nil?
|
||||||
|
(meta
|
||||||
|
(nippy/thaw
|
||||||
|
(nippy/freeze (with-meta [] {:has-meta? true}) {:incl-metadata? false})
|
||||||
|
{:incl-metadata? true}
|
||||||
|
)))
|
||||||
|
|
||||||
|
"Metadata successfully excluded by freeze")
|
||||||
|
|
||||||
|
(is (var? (nippy/read-quarantined-serializable-object-unsafe!
|
||||||
|
(nippy/thaw (nippy/freeze #'my-var))))
|
||||||
|
|
||||||
|
"Don't try to preserve metadata on vars")])
|
||||||
|
|
||||||
|
;;;; Freezable?
|
||||||
|
|
||||||
|
(deftest _freezable?
|
||||||
|
[(is (= (nippy/freezable? :foo) :native))
|
||||||
|
(is (= (nippy/freezable? [:a :b]) :native))
|
||||||
|
(is (= (nippy/freezable? [:a (fn [])]) nil))
|
||||||
|
(is (= (nippy/freezable? [:a (byte-array [1 2 3])]) :native))
|
||||||
|
(is (= (nippy/freezable? [:a (java.util.Date.)]) :native))
|
||||||
|
(is (= (nippy/freezable? (Exception.)) nil))
|
||||||
|
(is (= (nippy/freezable? (MyType. "a" "b")) :native))
|
||||||
|
(is (= (nippy/freezable? (MyRec. "a" "b")) :native))
|
||||||
|
(is (= (nippy/freezable? (Exception.) {:allow-java-serializable? true})
|
||||||
|
:maybe-java-serializable))])
|
||||||
|
|
||||||
|
;;;; thaw-xform
|
||||||
|
|
||||||
|
(deftest _thaw-xform
|
||||||
|
[(is (= (binding [nippy/*thaw-xform* nil] (thaw (freeze [1 2 :secret 3 4]))) [1 2 :secret 3 4]))
|
||||||
|
(is (= (binding [nippy/*thaw-xform* (map (fn [x] (if (= x :secret) :redacted x)))] (thaw (freeze [1 2 :secret 3 4]))) [1 2 :redacted 3 4]))
|
||||||
|
|
||||||
|
(is (= (binding [nippy/*thaw-xform* (remove (fn [x] (and (map-entry? x) (and (= (key x) :x) (val x)))))]
|
||||||
|
(thaw (freeze {:a :A, :b :B, :x :X, :c {:x :X}, :d #{:d1 :d2 {:d3 :D3, :x :X}}})))
|
||||||
|
{:a :A, :b :B, :c {}, :d #{:d1 :d2 {:d3 :D3}}}))
|
||||||
|
|
||||||
|
(is (= (binding [nippy/*thaw-xform* (remove (fn [x] (and (map? x) (contains? x :x))))]
|
||||||
|
(thaw (freeze {:a :A, :b :B, :x :X, :c {:x :X}, :d #{:d1 :d2 {:d3 :D3, :x :X}}})))
|
||||||
|
{:a :A, :b :B, :x :X, :c {:x :X}, :d #{:d1 :d2}}))
|
||||||
|
|
||||||
|
(is (= (binding [nippy/*thaw-xform* (map (fn [x] (/ 1 0)))] (thaw (freeze []))) []) "rf not run on empty colls")
|
||||||
|
|
||||||
|
(let [ex (truss/throws :default (binding [nippy/*thaw-xform* (map (fn [x] (/ 1 0)))] (thaw (freeze [:a :b]))))]
|
||||||
|
(is (= (-> ex ex-cause ex-cause ex-data :call) '(rf acc in)) "Error thrown via `*thaw-xform*`"))])
|
||||||
|
|
||||||
|
;;;; Compressors
|
||||||
|
|
||||||
|
(deftest _compressors
|
||||||
|
(println "\nTesting decompression of random data...")
|
||||||
|
(doseq [c [compr/zstd-compressor
|
||||||
|
compr/lz4-compressor
|
||||||
|
compr/lzo-compressor
|
||||||
|
compr/snappy-compressor
|
||||||
|
compr/lzma2-compressor]]
|
||||||
|
|
||||||
|
(print (str " With " (name (compr/header-id c)))) (flush)
|
||||||
|
(dotimes [_ 5] ; Slow, a few k laps should be sufficient for CI
|
||||||
|
(print ".") (flush)
|
||||||
|
(dotimes [_ 1000]
|
||||||
|
(is
|
||||||
|
(nil? (truss/catching :all (compr/decompress c (crypto/rand-bytes 1024))))
|
||||||
|
"Decompression never crashes JVM, even against invalid data")))
|
||||||
|
(println)))
|
||||||
|
|
||||||
|
;;;; Benchmarks
|
||||||
|
|
||||||
|
(deftest _benchmarks
|
||||||
|
(is (benchmarks/bench-serialization {:all? true})))
|
||||||
1
wiki/.gitignore
vendored
Normal file
1
wiki/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
README.md
|
||||||
145
wiki/1 Getting-started.md
Normal file
145
wiki/1 Getting-started.md
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Setup
|
||||||
|
|
||||||
|
Add the [relevant dependency](../#latest-releases) to your project:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
Leiningen: [com.taoensso/nippy "x-y-z"] ; or
|
||||||
|
deps.edn: com.taoensso/nippy {:mvn/version "x-y-z"}
|
||||||
|
```
|
||||||
|
|
||||||
|
And setup your namespace imports:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns my-app (:require [taoensso.nippy :as nippy]))
|
||||||
|
```
|
||||||
|
|
||||||
|
# De/serializing
|
||||||
|
|
||||||
|
As an example of what it can do, let's take a look at Nippy's own reference [stress data](https://taoensso.github.io/nippy/taoensso.nippy.html#var-stress-data):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{:nil nil
|
||||||
|
:true true
|
||||||
|
:false false
|
||||||
|
:false-boxed (Boolean. false)
|
||||||
|
|
||||||
|
:char \ಬ
|
||||||
|
:str-short "ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸ"
|
||||||
|
:str-long (reduce str (range 1024))
|
||||||
|
:kw :keyword
|
||||||
|
:kw-ns ::keyword
|
||||||
|
:sym 'foo
|
||||||
|
:sym-ns 'foo/bar
|
||||||
|
:kw-long (keyword (reduce str "_" (range 128)) (reduce str "_" (range 128)))
|
||||||
|
:sym-long (symbol (reduce str "_" (range 128)) (reduce str "_" (range 128)))
|
||||||
|
|
||||||
|
:byte (byte 16)
|
||||||
|
:short (short 42)
|
||||||
|
:integer (int 3)
|
||||||
|
:long (long 3)
|
||||||
|
:float (float 3.1415926535897932384626433832795)
|
||||||
|
:double (double 3.1415926535897932384626433832795)
|
||||||
|
:bigdec (bigdec 3.1415926535897932384626433832795)
|
||||||
|
:bigint (bigint 31415926535897932384626433832795)
|
||||||
|
:ratio 22/7
|
||||||
|
|
||||||
|
:list (list 1 2 3 4 5 (list 6 7 8 (list 9 10 (list) ())))
|
||||||
|
:vector [1 2 3 4 5 [6 7 8 [9 10 [[]]]]]
|
||||||
|
:subvec (subvec [1 2 3 4 5 6 7 8] 2 8)
|
||||||
|
:map {:a 1 :b 2 :c 3 :d {:e 4 :f {:g 5 :h 6 :i 7 :j {{} {}}}}}
|
||||||
|
:map-entry (clojure.lang.MapEntry/create "key" "val")
|
||||||
|
:set #{1 2 3 4 5 #{6 7 8 #{9 10 #{#{}}}}}
|
||||||
|
:meta (with-meta {:a :A} {:metakey :metaval})
|
||||||
|
:nested [#{{1 [:a :b] 2 [:c :d] 3 [:e :f]} [#{{[] ()}}] #{:a :b}}
|
||||||
|
#{{1 [:a :b] 2 [:c :d] 3 [:e :f]} [#{{[] ()}}] #{:a :b}}
|
||||||
|
[1 [1 2 [1 2 3 [1 2 3 4 [1 2 3 4 5 "ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸ"] {} #{} [] ()]]]]]
|
||||||
|
|
||||||
|
:regex #"^(https?:)?//(www\?|\?)?"
|
||||||
|
:sorted-set (sorted-set 1 2 3 4 5)
|
||||||
|
:sorted-map (sorted-map :b 2 :a 1 :d 4 :c 3)
|
||||||
|
:lazy-seq-empty (map identity ())
|
||||||
|
:lazy-seq (repeatedly 64 #(do nil))
|
||||||
|
:queue-empty (into clojure.lang.PersistentQueue/EMPTY [:a :b :c :d :e :f :g])
|
||||||
|
:queue clojure.lang.PersistentQueue/EMPTY
|
||||||
|
|
||||||
|
:uuid (java.util.UUID. 7232453380187312026 -7067939076204274491)
|
||||||
|
:uri (java.net.URI. "https://clojure.org")
|
||||||
|
:defrecord (nippy/StressRecord. "data")
|
||||||
|
:deftype (nippy/StressType. "data")
|
||||||
|
:bytes (byte-array [(byte 1) (byte 2) (byte 3)])
|
||||||
|
:objects (object-array [1 "two" {:data "data"}])
|
||||||
|
|
||||||
|
:util-date (java.util.Date. 1577884455500)
|
||||||
|
:sql-date (java.sql.Date. 1577884455500)
|
||||||
|
:instant (java.time.Instant/parse "2020-01-01T13:14:15.50Z")
|
||||||
|
:duration (java.time.Duration/ofSeconds 100 100)
|
||||||
|
:period (java.time.Period/of 1 1 1)
|
||||||
|
|
||||||
|
:throwable (Throwable. "Msg")
|
||||||
|
:exception (Exception. "Msg")
|
||||||
|
:ex-info (ex-info "Msg" {:data "data"})
|
||||||
|
|
||||||
|
:many-longs (vec (repeatedly 512 #(rand-nth (range 10))))
|
||||||
|
:many-doubles (vec (repeatedly 512 #(double (rand-nth (range 10)))))
|
||||||
|
:many-strings (vec (repeatedly 512 #(rand-nth ["foo" "bar" "baz" "qux"])))
|
||||||
|
:many-keywords (vec (repeatedly 512
|
||||||
|
#(keyword
|
||||||
|
(rand-nth ["foo" "bar" "baz" "qux" nil])
|
||||||
|
(rand-nth ["foo" "bar" "baz" "qux" ]))))}
|
||||||
|
```
|
||||||
|
|
||||||
|
Serialize it:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(def frozen-stress-data (nippy/freeze (nippy/stress-data {})))
|
||||||
|
=> #<byte[] [B@3253bcf3>
|
||||||
|
```
|
||||||
|
|
||||||
|
Deserialize it:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(nippy/thaw frozen-stress-data)
|
||||||
|
=> {:bytes (byte-array [(byte 1) (byte 2) (byte 3)])
|
||||||
|
:nil nil
|
||||||
|
:boolean true
|
||||||
|
<...> }
|
||||||
|
```
|
||||||
|
|
||||||
|
Couldn't be simpler!
|
||||||
|
|
||||||
|
# Streaming
|
||||||
|
|
||||||
|
- To serialize directly to a `java.io.DataInput`, see [`freeze-to-out!`](https://taoensso.github.io/nippy/taoensso.nippy.html#var-freeze-to-out.21).
|
||||||
|
- To deserialize directly from a `java.io.DataOutput`, see [`thaw-from-in!`](https://taoensso.github.io/nippy/taoensso.nippy.html#var-thaw-from-in.21).
|
||||||
|
|
||||||
|
# Encryption
|
||||||
|
|
||||||
|
> You may want to consider using Nippy with [Tempel](https://www.taoensso.com/tempel) for more comprehensive encryption options.
|
||||||
|
|
||||||
|
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
|
||||||
|
(nippy/thaw <encrypted-data> {:password [:salted "my-password"]}) ; Decrypt
|
||||||
|
```
|
||||||
|
|
||||||
|
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 [`aes128-encryptor`](https://taoensso.github.io/nippy/taoensso.nippy.html#var-aes128-encryptor) for a detailed explanation of why/when you'd want one or the other.
|
||||||
|
|
||||||
|
# Custom types
|
||||||
|
|
||||||
|
It's easy to extend Nippy to your own data types:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(defrecord MyType [data])
|
||||||
|
|
||||||
|
(nippy/extend-freeze MyType :my-type/foo ; A unique (namespaced) type identifier
|
||||||
|
[x data-output]
|
||||||
|
(.writeUTF data-output (:data x)))
|
||||||
|
|
||||||
|
(nippy/extend-thaw :my-type/foo ; Same type id
|
||||||
|
[data-input]
|
||||||
|
(MyType. (.readUTF data-input)))
|
||||||
|
|
||||||
|
(nippy/thaw (nippy/freeze (MyType. "Joe"))) => #taoensso.nippy.MyType{:data "Joe"}
|
||||||
|
```
|
||||||
30
wiki/2 Operational-considerations.md
Normal file
30
wiki/2 Operational-considerations.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Data longevity
|
||||||
|
|
||||||
|
Nippy is widely used to store **long-lived** data and promises (as always) that **data serialized today should be readable by all future versions of Nippy**.
|
||||||
|
|
||||||
|
But please note that the **converse is not generally true**:
|
||||||
|
|
||||||
|
- Nippy `vX` **should** be able to read all data from Nippy `vY<=X` (backwards compatibility)
|
||||||
|
- Nippy `vX` **may/not** be able to read all data from Nippy `vY>X` (forwards compatibility)
|
||||||
|
|
||||||
|
# Rolling updates and rollback
|
||||||
|
|
||||||
|
From time to time, Nippy may introduce:
|
||||||
|
|
||||||
|
- Support for serializing **new types**
|
||||||
|
- Optimizations to the serialization of **pre-existing types**
|
||||||
|
|
||||||
|
To help ease **rolling updates** and to better support **rollback**, Nippy (since version v3.4.1) will always introduce such changes over **two version releases**:
|
||||||
|
|
||||||
|
- Release 1: to add **read support** for the new types
|
||||||
|
- Release 2: to add **write support** for the new types
|
||||||
|
|
||||||
|
Starting from v3.4.1, Nippy's release notes will **always clearly indicate** if a particular update sequence is recommended.
|
||||||
|
|
||||||
|
# Stability of byte output
|
||||||
|
|
||||||
|
It has **never been an objective** of Nippy to offer **predictable byte output**, and I'd generally **recommend against** depending on specific byte output.
|
||||||
|
|
||||||
|
However, I know that a small minority of users *do* have specialized needs in this area.
|
||||||
|
|
||||||
|
So starting with Nippy v3.4, Nippy's release notes will **always clearly indicate** if any changes to byte output are expected.
|
||||||
278
wiki/3 Evolving-your-Nippy-data.md
Normal file
278
wiki/3 Evolving-your-Nippy-data.md
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
> This article is **community content** kindly contributed by a Nippy user (@Outrovurt)
|
||||||
|
|
||||||
|
This article describes a number of use cases where you need to make changes to your code which will have some impact on data you have already frozen using Nippy, and how best to manage each specific case. We will also discuss custom freezing and thawing.
|
||||||
|
|
||||||
|
It is assumed you have working knowledge of a good editor (e.g. emacs) and know how to start a Clojure REPL (e.g. via CIDER).
|
||||||
|
|
||||||
|
Throughout this article we will refer to serialization as *freezing*, and deserialization as *thawing*, which are the terms used by Nippy. (The word "thawer" will be used throughout the article, it's obviously made up, but please bear with it!)
|
||||||
|
|
||||||
|
# Project setup
|
||||||
|
|
||||||
|
If you want to follow along, you can create a fresh project using whichever Clojure project management tool you are currently most comfortable with (lein, Clojure deps), and [include Nippy in the dependencies](https://github.com/taoensso/nippy/wiki#setup). Create the following namespaces:
|
||||||
|
|
||||||
|
* nippy.evolve
|
||||||
|
* nippy.other
|
||||||
|
|
||||||
|
and delete any default code which is generated for you by your project management tool.
|
||||||
|
|
||||||
|
We will start in `nippy.evolve`. Ensure your `(ns ...)` looks like this:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns nippy.evolve
|
||||||
|
(:require
|
||||||
|
[taoensso.nippy
|
||||||
|
:refer (freeze
|
||||||
|
thaw)]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Start a REPL and switch to `nippy.evolve` if you aren't already there.
|
||||||
|
|
||||||
|
# Freezing a record
|
||||||
|
|
||||||
|
Create the following record, either in your source code or in the REPL, and create a new instance of it:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(defrecord FirstRec [])
|
||||||
|
(def x1 (FirstRec.))
|
||||||
|
```
|
||||||
|
|
||||||
|
Now freeze it:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(def f1 (freeze x1))
|
||||||
|
```
|
||||||
|
|
||||||
|
The first thing you'll notice is that you don't have to set anything up at all, Nippy will freeze the record instance out of the box. It does this by determining that `x1` is a record, and uses a built-in freezer to freeze it.
|
||||||
|
|
||||||
|
## A frozen record
|
||||||
|
|
||||||
|
Let's take a quick look at `f1` at the REPL:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (type f1)
|
||||||
|
[B
|
||||||
|
nippy.evolve> f1
|
||||||
|
[78, 80, 89, 0, 48, 21, 110, 105, 112, 112, 121, 46, 101, 118, 111,
|
||||||
|
108, 118, 101, 46, 70, 105, 114, 115, 116, 82, 101, 99, 19]
|
||||||
|
```
|
||||||
|
|
||||||
|
The "[B" indicates that this is a Java Byte array, which you can also create using the Clojure function `byte-array`.
|
||||||
|
|
||||||
|
The value of `f1` is an array of bytes, consisting of two parts:
|
||||||
|
|
||||||
|
* the envelope
|
||||||
|
- a 4-byte header, followed by
|
||||||
|
- a 1-byte type id
|
||||||
|
* the packet itself, whatever you are encoding
|
||||||
|
|
||||||
|
The header provides sanity checking, the Nippy version number that created it, and information related to any compression or encryption employed. We won't go into detail here as to how the header is composed as it is not relevant to our discussion.
|
||||||
|
|
||||||
|
The type id tells us what type of data we have frozen. A positive value indicates a built-in type. In this case we have 48, which refers to a record, specifically a "small record" (small referring to the total number of bytes used to encode the body of the record.) This is all explained in the Nippy source code, where you can find a list of all built-in types that Nippy recognizes together with the type id used to represent it. In the latest version of Nippy this list can be found in the var `public-types-spec`.
|
||||||
|
|
||||||
|
So when we froze an instance of `FirstRec`, we used a built-in freezer for records to produce the above byte array.
|
||||||
|
|
||||||
|
What about the rest of the array? There are a total of 28 bytes, 5 of which are for the envelope, so the remaining 23 bytes are for the packet itself. But wait a minute. If we are basically freezing an empty record with no fields, why do we need 23 bytes to freeze it? The answer is that while the envelope encodes the fact that the remaining data is for a record, it also needs to encode the type of record that we have frozen. Otherwise there is no way to thaw it back to a record of type `FirstRec`. Since all envelopes contain 5 bytes, this information can only be encoded in one place: the packet itself.
|
||||||
|
|
||||||
|
So if you type the following at the REPL, you'll get a clue as to what is happening:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (freeze "nippy.evolve.FirstRec")
|
||||||
|
[78, 80, 89, 0, 105, 21, 110, 105, 112, 112, 121, 46, 101, 118, 111,
|
||||||
|
108, 118, 101, 46, 70, 105, 114, 115, 116, 82, 101, 99]
|
||||||
|
```
|
||||||
|
|
||||||
|
Once again the first 4-bytes represent the header. The fifth byte, 105, tells us that this is a string (specifically a "small" string of under 128 bytes), and the remaining packet is a 22-byte array:
|
||||||
|
|
||||||
|
`[21, 110, 105, 112, 112, 121, 46, 101, 118, 111,
|
||||||
|
108, 118, 101, 46, 70, 105, 114, 115, 116, 82, 101, 99]`
|
||||||
|
|
||||||
|
If you look back at `f1` above, you will see that of the 23 bytes of the packet, the first 22 bytes are identical to the 22 bytes of the frozen string above. In other words, Nippy encodes the fully-qualified record name at the very start of the packet itself.
|
||||||
|
|
||||||
|
That just leaves a single byte, 19, at the end of `f1`. You can probably guess what this represents, so let's try the following:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (freeze {})
|
||||||
|
[78, 80, 89, 0, 19]
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we have tried to freeze just an empty map, and that has produced what appears to be a byte array with just an envelope and no packet. Ignoring the header, the fifth byte is 19, which corresponds to an empty map (:map-0 in the code), so there is no need for any packet.
|
||||||
|
|
||||||
|
To summarise, when Nippy freezes an empty record, it encodes it with:
|
||||||
|
|
||||||
|
* a 4-byte header
|
||||||
|
* a 1-byte type-id of 48 indicated a small record
|
||||||
|
* a 23-byte packet, the first 22 bytes of which represent the string "nippy.evolve.FirstRec", and the final byte which represents an empty map
|
||||||
|
|
||||||
|
This of course contains all the information Nippy requires to thaw the data back to an instance of `FirstRec`.
|
||||||
|
|
||||||
|
## Thawing a record
|
||||||
|
|
||||||
|
Now let us turn to thawing. Enter the following code into your source file or into the REPL:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(def t1 (thaw f1))
|
||||||
|
```
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> t1
|
||||||
|
{}
|
||||||
|
nippy.evolve> (type t1)
|
||||||
|
nippy.evolve.FirstRec
|
||||||
|
```
|
||||||
|
|
||||||
|
Exactly as we expected, `t1` returns what appears to be an empty map (though this depends on how your REPL is set up), but when we examine its type, we find that is has correctly been thawed as a `nippy.evolve.FirstRec`. This is entirely due to the way Nippy has interpreted all the information provided in the envelope and packet, described in the previous section.
|
||||||
|
|
||||||
|
# Evolving your code
|
||||||
|
|
||||||
|
So without setting anything up at all in your project, you can see how simple it is just to use Nippy's `freeze` and `thaw` functions to serialize instances of any record you care to create. However, if you have been following the above discussion, you will probably have noted that there are a number of problems here, one or more of which you might even have run into at some stage:
|
||||||
|
|
||||||
|
* a number of bytes are used to encode the name of the record in the packet; in our example 22 bytes are used
|
||||||
|
* since the name of the record is encoded in the packet, this means that if we change the name of the record or move it to another namespace, then try to thaw a previously frozen byte array, the operation will since Nippy will be unable to match up the previously encoded with the now renamed or moved record
|
||||||
|
|
||||||
|
We will look at moving and renaming first, and then consider how we can reduce the number of bytes in a packet afterwards.
|
||||||
|
|
||||||
|
## Moving or renaming a record
|
||||||
|
|
||||||
|
If we want to move or rename a record, for all data previously frozen using the record before renaming/moving it, Nippy will no longer be able to thaw that data since it can no longer match the record name encoded in the packet with a class generated by the record. To be exact, if you try this within the same session, while the REPL is open, then even if you have moved/renamed a record, the old record and the compiled class associated with it will still be available. This is a consequence of the way Clojure compiles records, and even if you try to do an `(ns-unamp 'nippy.evolve 'FirstRec)`, it will still be there. So to better understand this issue, we are going to first save the frozen byte array `f1` to a file, as follows.
|
||||||
|
|
||||||
|
Update the (ns) form to include the following:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns nippy.evolve
|
||||||
|
(:require
|
||||||
|
...
|
||||||
|
[clojure.java.io
|
||||||
|
:refer (file
|
||||||
|
output-stream
|
||||||
|
input-stream)]
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
Now enter the following code into the REPL:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (with-open [out (output-stream (file "./frozen-first-rec"))]
|
||||||
|
(.write out f1))
|
||||||
|
nil
|
||||||
|
```
|
||||||
|
|
||||||
|
This will result in the file `./frozen-first-rec` being created in the top-level of your project. We will come back to this file subsequently.
|
||||||
|
|
||||||
|
Next, move `FirstRec` to the namespace `nippy.other`, and delete any code which references it within `nippy.evolve`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns nippy.other)
|
||||||
|
|
||||||
|
(defrecord FirstRec [])
|
||||||
|
```
|
||||||
|
|
||||||
|
Now quit the REPL, and start a new one. If not already there, change to namespace `nippy.evolve` and type the following:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> FirstRec
|
||||||
|
Syntax error compiling at (*cider-repl clojure/evolve:localhost:36735(clj)*:0:0).
|
||||||
|
Unable to resolve symbol: FirstRec in this context
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the above error, which shows that FirstRec is no longer defined in `nippy.evolve`.
|
||||||
|
|
||||||
|
Still from within `nippy.evolve`, type the following:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (with-open [ina (input-stream (file "./frozen-first-rec"))]
|
||||||
|
(let [buf (byte-array 28)
|
||||||
|
n (.read in buf)]
|
||||||
|
(thaw buf)))
|
||||||
|
```
|
||||||
|
|
||||||
|
This code attempts to open the file `./frozen-first-rec` and read it into `buf`, a byte array. If you have been following everything exactly thus far, running the above should result in the following being returned:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
#:nippy{:unthawable
|
||||||
|
{:type :record,
|
||||||
|
:cause :exception,
|
||||||
|
:class-name "nippy.evolve.FirstRec",
|
||||||
|
:content {},
|
||||||
|
:exception #error {...}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once again, this is to be expected. We have tried to thaw a byte array corresponding to a record with a class-name of "nippy.evolve.FirstRec", which of course no longer exists as we have moved it to `nippy.other`.
|
||||||
|
|
||||||
|
Whenever you encounter a `:nippy/unthawable` as a result of thawing, one approach is to write custom code to fix it. For example in the above situation, you could parse the map, and for `:type :record`, `:class-name "nippy.evolve.FirstRec"`, you could then look at the `:content` and create that as a `nippy.other.FirstRec` record. If the `:nippy/unthawable` appears deeply nested within the returned structure, you could call `clojure.walk/prewalk` as a more general solution, and provide a mapping table of {<old-class-name-str> <new-class-name>}, in this case {"nippy.evolve.FirstRec" nippy.other.FirstRec}, and use that to create FirstRec records of the new type. In any cases where you don't have access to the frozen files containing the old format, for example where you have created a desktop application which saves files which contain a frozen data structure, this will be your only option. However, in cases where you do have access to the frozen data, there is an alternative, better approach.
|
||||||
|
|
||||||
|
## Custom freeze and thaw
|
||||||
|
|
||||||
|
Before we talk about custom freeze and thaw, it's worth taking a step back and looking at how each of these processes work from a high-level.
|
||||||
|
|
||||||
|
* **freezing** takes as its input a piece of data, and the process is driven entirely by the *type* of that data
|
||||||
|
* **thawing** takes as its input a piece of previously frozen data, and that process is driven by the *type-id* in the envelope and where appropriate some additional data in the packet, such as the name of the record's class for records
|
||||||
|
|
||||||
|
In general, any data we freeze, we want to be able to thaw back to its original form. In other words, the following should always hold true:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(= data
|
||||||
|
(-> data freeze thaw))
|
||||||
|
```
|
||||||
|
|
||||||
|
More accurately, at any given time we want to be able to restore any frozen data to its original state when we thaw it. Although this appears to be described by the above condition, there is a subtle but important distinction in that the above assumes that we are freezing and then thawing the data an instant later, whereas in reality *the thawing process can happen at any future time*. What this means is that when writing custom freeze and thaw code, it is important that only the thaw code matches the frozen data at any given time. This will become important soon.
|
||||||
|
|
||||||
|
In situations where we have access to previously frozen data, if we want to rename or move a record, we have an additional option to parsing the result of thaw and looking for the occurrence of any `:nippy/unthawable` maps nested in the resultant data (described above): custom freezing and thawing. Even if we have already frozen data using the built-in record freezer, we can still deal with this situation fairly easily. The trick is to understand that there is a sequence to be followed when it comes to implementing custom freezers and thawers.
|
||||||
|
|
||||||
|
In our above example, to avoid receiving the `:nippy/unthawable` result, we can start by moving the `defrecord` code back from `nippy.other` to `nippy.evolve`. Now if we evaluate the `nippy.evolve` namespace, then attempt to thaw the file we saved, we should get back our original data that we froze before, an instance of a record of type `nippy.evolve.FirstRec`. So far so good. Now the next step is to break the dependency between the frozen data and the name of the original record used to freeze it. To do this, we can write a custom freezer and thawer, and the best part is that we can have these active within nippy at the same time as the built-in record freezer/thawer. Here is how:
|
||||||
|
|
||||||
|
First, within `nippy.evolve`, update the `(ns)` form to include `extend-freeze` and `extend-thaw`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns nippy.evolve
|
||||||
|
(:require
|
||||||
|
[taoensso.nippy
|
||||||
|
:refer (...
|
||||||
|
extend-freeze
|
||||||
|
extend-thaw
|
||||||
|
freeze-to-out!
|
||||||
|
thaw-from-in!)]
|
||||||
|
...
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure that the the following code is included in `nippy.evolve`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(defrecord FirstRec [])
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the following code:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(extend-freeze FirstRec 1
|
||||||
|
[x data-out]
|
||||||
|
(freeze-to-out!
|
||||||
|
data-out
|
||||||
|
(into {}
|
||||||
|
(:data x))))
|
||||||
|
|
||||||
|
(extend-thaw 1 [data-input] (map->FirstRec (thaw-from-in! data-input)))
|
||||||
|
```
|
||||||
|
|
||||||
|
These two blocks create respectively a custom freezer for FirstRec, which writes out an envelope with custom id 1, and a custom thawer which is used only for packets with custom id 1. We can use any id in the range `1 <= id < = 127`. As stated at the start of this section, this code shows that the freezer is driven by the type of data being frozen, and the thawer is driven only by the custom id of the data being thawed.
|
||||||
|
|
||||||
|
Now evaluate the whole `nippy.evolve` namespace again. This will extend Nippy by providing it with a custom freeze and thaw for any instances of `FirstRec`:
|
||||||
|
|
||||||
|
* from this point onwards, any new data we create and freeze will be frozen using the custom freezer
|
||||||
|
* any newly frozen data will be thawed by the above custom thawer
|
||||||
|
* any previously frozen data, with type-id 48 for records (see above), will still be thawed by the built-in record thawer
|
||||||
|
|
||||||
|
This last point is important in that it allows us to simultaneously deal with legacy data while also being able to process new data.
|
||||||
|
|
||||||
|
Let's try freezing a new record:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
nippy.evolve> (freeze (FirstRec.))
|
||||||
|
[78, 80, 89, 0, -1, 19]
|
||||||
|
```
|
||||||
|
|
||||||
|
Once again we have our 4-byte header, but this time we have a negative number as our type-id. This is actually the negative of the custom-id we specified in our call to `extend-freeze`, and is how Nippy stores custom ids. We could have also used a (preferably namespaced) keyword, but that would have taken an extra 16-bits (a hash of the keyword) in the packet itself, and arguably doesn't provide any benefits over using an integer id. As long as we maintain a mapping between custom id and type in our code, and don't use the same custom id for a completely different type in the future, we shouldn't run into any issues with using a custom id over a keyword.
|
||||||
|
|
||||||
|
The packet in this case is just a single byte, 19, which refers to an empty map in nippy.clj type-ids. This map is then used by the thawer to reconstruct our original instance a `FirstRec`.
|
||||||
|
|
||||||
|
The first thing that should be evident is that this is much shorter, by 22-bytes, than the output produced by the built-in record freezer. This is because our custom freezer only stores the record as a map, with no string corresponding to the name of the record. This has resulted in decoupling the frozen data from any concrete record type (e.g. `nippy.evolve.FirstRec`), by instead coupling it to only an arbitrary custom id of our choosing (e.g. 1), and leaving us to provide the mapping between the custom id and some type within the custom thawer, which from now on we can do by updating the thawer with custom id 1.
|
||||||
8
wiki/Home.md
Normal file
8
wiki/Home.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
See the **menu to the right** for content 👉
|
||||||
|
|
||||||
|
# Contributions welcome
|
||||||
|
|
||||||
|
**PRs very welcome** to help improve this documentation!
|
||||||
|
See the [wiki](../tree/master/wiki) folder in the main repo for the relevant files.
|
||||||
|
|
||||||
|
\- [Peter Taoussanis](https://www.taoensso.com)
|
||||||
5
wiki/README.md
Normal file
5
wiki/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Attention!
|
||||||
|
|
||||||
|
This wiki is designed for viewing from [here](../../../wiki)!
|
||||||
|
|
||||||
|
Viewing from GitHub's file browser will result in **broken links**.
|
||||||
Loading…
Reference in a new issue