├── .circleci └── config.yml ├── .github └── CODEOWNERS ├── .gitignore ├── ChangeLog ├── Makefile ├── ORIGINATOR ├── README.md ├── bb.edn ├── deps.edn ├── project.clj ├── src ├── clj_commons │ └── digest.clj └── digest.clj ├── test ├── clj_commons │ └── digest_test.clj ├── digest_test.clj ├── quote.txt └── snail.png └── version.edn /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | # For a detailed guide to building and testing with clojure, read the docs: 4 | # https://circleci.com/docs/2.0/language-clojure/ for more details 5 | version: 2.1 6 | 7 | workflows: 8 | build-deploy: 9 | jobs: 10 | - build: 11 | filters: 12 | tags: 13 | only: /.*/ 14 | 15 | - deploy: 16 | requires: 17 | - build 18 | filters: 19 | tags: 20 | only: /Release-.*/ 21 | context: 22 | - CLOJARS_DEPLOY 23 | 24 | # Define a job to be invoked later in a workflow. 25 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 26 | jobs: 27 | build: 28 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 29 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 30 | docker: 31 | # specify the version you desire here 32 | - image: circleci/clojure:lein-2.9.5 33 | 34 | # Specify service dependencies here if necessary 35 | # CircleCI maintains a library of pre-built images 36 | # documented at https://circleci.com/docs/2.0/circleci-images/ 37 | # - image: circleci/postgres:9.4 38 | 39 | working_directory: ~/repo 40 | 41 | environment: 42 | LEIN_ROOT: "true" 43 | # Customize the JVM maximum heap limit 44 | JVM_OPTS: -Xmx3200m 45 | 46 | # Add steps to the job 47 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 48 | steps: 49 | - checkout 50 | 51 | # Download and cache dependencies 52 | - restore_cache: 53 | keys: 54 | - v1-dependencies-{{ checksum "project.clj" }} 55 | # fallback to using the latest cache if no exact match is found 56 | - v1-dependencies- 57 | 58 | - run: lein deps 59 | 60 | - save_cache: 61 | paths: 62 | - ~/.m2 63 | key: v1-dependencies-{{ checksum "project.clj" }} 64 | 65 | # run tests! 66 | - run: lein test 67 | 68 | deploy: 69 | docker: 70 | # specify the version you desire here 71 | - image: circleci/clojure:openjdk-8-lein-2.9.1 72 | # Specify service dependencies here if necessary 73 | # CircleCI maintains a library of pre-built images 74 | # documented at https://circleci.com/docs/2.0/circleci-images/ 75 | # - image: circleci/postgres:9.4 76 | 77 | working_directory: ~/repo 78 | 79 | environment: 80 | LEIN_ROOT: "true" 81 | # Customize the JVM maximum heap limit 82 | JVM_OPTS: -Xmx3200m 83 | 84 | steps: 85 | - checkout 86 | 87 | # Download and cache dependencies 88 | - restore_cache: 89 | keys: 90 | - v1-dependencies-{{ checksum "project.clj" }} 91 | # fallback to using the latest cache if no exact match is found 92 | - v1-dependencies- 93 | 94 | # Download and cache dependencies 95 | - restore_cache: 96 | keys: 97 | - v1-dependencies-{{ checksum "project.clj" }} 98 | # fallback to using the latest cache if no exact match is found 99 | - v1-dependencies- 100 | 101 | - run: 102 | name: Install babashka 103 | command: | 104 | curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install -o install.sh 105 | sudo bash install.sh 106 | rm install.sh 107 | - run: 108 | name: Install deployment-script 109 | command: | 110 | curl -s https://raw.githubusercontent.com/clj-commons/infra/main/deployment/circle-maybe-deploy.bb -o circle-maybe-deploy.bb 111 | chmod a+x circle-maybe-deploy.bb 112 | - run: lein deps 113 | 114 | - run: 115 | name: Setup GPG signing key 116 | command: | 117 | GNUPGHOME="$HOME/.gnupg" 118 | export GNUPGHOME 119 | mkdir -p "$GNUPGHOME" 120 | chmod 0700 "$GNUPGHOME" 121 | echo "$GPG_KEY" \ 122 | | base64 --decode --ignore-garbage \ 123 | | gpg --batch --allow-secret-key-import --import 124 | gpg --keyid-format LONG --list-secret-keys 125 | - save_cache: 126 | paths: 127 | - ~/.m2 128 | key: v1-dependencies-{{ checksum "project.clj" }} 129 | - run: 130 | name: Deploy 131 | command: | 132 | GPG_TTY=$(tty) 133 | export GPG_TTY 134 | echo $GPG_TTY 135 | ./circle-maybe-deploy.bb lein deploy clojars 136 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @borkdude @slipset 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .lein-* 2 | README.html 3 | classes 4 | digest-*.jar 5 | lib 6 | pom.xml 7 | pom.xml.asc 8 | target 9 | /.cpcache 10 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2021-10-11 version 1.4.93 2 | * The digest namespace has moved to clj-commons.digest. The old one is deprecated. 3 | 4 | 2018-03-25 version 1.4.10 5 | * Test using for public domain data (issue #11) 6 | 7 | 2018-03-25 version 1.4.8 8 | * Minor fixes (@laurio in PR #8) 9 | 10 | 2018-03-18 version 1.4.7 11 | * Minor simplifications, fix typo. 12 | * Clojure 1.9.0 13 | 14 | 2016-08-18 version 1.4.6 15 | * Include standard function metadata on digest functions (@holguinj in PR #6) 16 | 17 | 2016-06-16 version 1.4.5 18 | * Added licence to project.clj (@raxod502 in PR #4) 19 | * Clojure 1.8.0 20 | 21 | 2014-04-02 version 1.4.4 22 | * Clojure 1.6 23 | * README.rst -> README.md 24 | 25 | 2013-02-20 version 1.4.3 26 | * Using protocols (thanks DerGuteMoritz) 27 | 28 | 2012-11-24 version 1.4.2 29 | * Clojure 1.4 30 | 31 | 2012-11-24 version 1.4.1 32 | * Fix file descriptor leak (thanks Craig Ludington) 33 | 34 | 2012-03-04 version 1.4.0 35 | * Remove reflections (thanks naitik!) 36 | 37 | 2011-10-03 version 1.3.0 38 | * Clojure 1.3.0 39 | * def ^:dynamic 40 | 41 | 2011-02-27 version 1.2.1 42 | * Fixed bug in signature padding 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | $(error please pick a target) 3 | 4 | test: 5 | lein test 6 | 7 | publish: 8 | lein deploy clojars 9 | 10 | .PHONY: all test publish 11 | -------------------------------------------------------------------------------- /ORIGINATOR: -------------------------------------------------------------------------------- 1 | @tebeka 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-commons/digest 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/org.clj-commons/digest.svg)](https://clojars.org/org.clj-commons/digest) 4 | [![cljdoc badge](https://cljdoc.org/badge/org.clj-commons/digest)](https://cljdoc.org/d/org.clj-commons/digest) 5 | [![CircleCI Status](https://circleci.com/gh/clj-commons/digest.svg?style=svg)](https://circleci.com/gh/clj-commons/digest) 6 | 7 | `clj-commons/digest` - A message digest library for Clojure. Providing `md5`, `sha-256`, ... 8 | 9 | There are several digest functions (such as `md5`, `sha-256` ...) in this 10 | namespace. Each can handle the following input types: 11 | 12 | * `java.lang.String` 13 | * `byte array` 14 | * `java.io.File` 15 | * `java.io.InputStream` 16 | * Sequence of byte array 17 | 18 | # Usage 19 | 20 | ``` clojure 21 | user=> (require '[clj-commons.digest :as digest]) 22 | nil 23 | ; On a string 24 | user=> (digest/md5 "clojure") 25 | "32c0d97f82a20e67c6d184620f6bd322" 26 | ; On a file 27 | user=> (require '[clojure.java.io :as io]) 28 | nil 29 | user=> (digest/sha-256 (io/file "/tmp/hello.txt")) 30 | "163883d3e0e3b0c028d35b626b98564be8d9d649ed8adb8b929cb8c94c735c59" 31 | ``` 32 | 33 | # Installation 34 | 35 | ## deps.edn 36 | 37 | ``` clojure 38 | org.clj-commons/digest {:mvn/version "1.4.100"} 39 | ``` 40 | 41 | ## lein 42 | 43 | ``` clojure 44 | [org.clj-commons/digest "1.4.100"] 45 | ``` 46 | 47 | # Dev 48 | 49 | ## Deployment 50 | 51 | Run `bb deploy` to deploy using the clj-commons 52 | [release](https://github.com/clj-commons/infra/blob/main/deployment/release.bb) 53 | script or create a tag manually in the format `Release-1.4.` and 54 | push it. 55 | 56 | # License 57 | Copyright© 2017 Miki Tebeka 58 | 59 | Distributed under the Eclipse Public License (same as Clojure). 60 | 61 | Snail image in `tests` is public domain by Miki Tebeka 62 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clj-commons/infra {:git/url "https://github.com/clj-commons/infra" 2 | :git/sha "a3ebcff4d32c4b84f001f86cdee5c76358d15ea5"}} 3 | :tasks 4 | {:requires ([clojure.string :as str] 5 | [deployment.release :as r]) 6 | deploy 7 | {:task 8 | (if (r/all-good?) 9 | (do 10 | (let [v (r/version!) 11 | ;; commit count + 1 for README update 12 | cc (inc (Integer/parseInt (r/commit-count!))) 13 | rt (r/release-tag (r/commit-count-version v cc))] 14 | (spit "README.md" 15 | (str/replace (slurp "README.md") 16 | (re-pattern (format "(%s)\\.(\\d+)" v)) 17 | (fn [[_ version _]] 18 | (str version "." cc)))) 19 | (shell "git add README.md") 20 | (shell "git commit -m 'Bump version in README'") 21 | (shell "git push")) 22 | (r/release!)) 23 | (println "Unclean!"))}}} 24 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clj-commons/digest (or (System/getenv "PROJECT_VERSION") "1.4.10") 2 | :description "Digest algorithms (MD5, SHA ...) for Clojure" 3 | :author "Miki Tebeka " 4 | :url "https://github.com/clj-commons/clj-digest" 5 | :deploy-repositories [["clojars" {:url "https://repo.clojars.org" 6 | :username :env/clojars_username 7 | :password :env/clojars_password 8 | :sign-releases true}]] 9 | :license {:name "Eclipse Public License" 10 | :url "http://www.eclipse.org/legal/epl-v10.html"} 11 | :dependencies [[org.clojure/clojure "1.10.1"]]) 12 | -------------------------------------------------------------------------------- /src/clj_commons/digest.clj: -------------------------------------------------------------------------------- 1 | (ns 2 | ^{:author "Miki Tebeka " 3 | :doc "Message digest algorithms for Clojure"} 4 | clj-commons.digest 5 | (:require [clojure.string :refer [join lower-case split]]) 6 | (:import (java.io File FileInputStream InputStream) 7 | (java.security MessageDigest Provider Security) 8 | (java.util Arrays))) 9 | 10 | ; Default buffer size for reading 11 | (def ^:dynamic *buffer-size* 1024) 12 | 13 | (defn- read-some 14 | "Read some data from reader. Return [data size] if there's more to read, 15 | otherwise nil." 16 | [^InputStream reader] 17 | (let [^bytes buffer (make-array Byte/TYPE *buffer-size*) 18 | size (.read reader buffer)] 19 | (when (pos? size) 20 | (if (= size *buffer-size*) buffer (Arrays/copyOf buffer size))))) 21 | 22 | (defn- byte-seq 23 | "Return a sequence of [data size] from reader." 24 | [^InputStream reader] 25 | (take-while some? (repeatedly (partial read-some reader)))) 26 | 27 | (defn- signature 28 | "Get signature (string) of digest." 29 | [^MessageDigest algorithm] 30 | (let [size (* 2 (.getDigestLength algorithm)) 31 | sig (.toString (BigInteger. 1 (.digest algorithm)) 16) 32 | padding (join (repeat (- size (count sig)) "0"))] 33 | (str padding sig))) 34 | 35 | (defprotocol Digestible 36 | (-digest [message algorithm])) 37 | 38 | (extend-protocol Digestible 39 | (class (make-array Byte/TYPE 0)) 40 | (-digest [message algorithm] 41 | (-digest [message] algorithm)) 42 | 43 | java.util.Collection 44 | ;; Code "borrowed" from 45 | ;; * http://www.holygoat.co.uk/blog/entry/2009-03-26-1 46 | ;; * http://www.rgagnon.com/javadetails/java-0416.html 47 | (-digest [message algorithm] 48 | (let [^MessageDigest algo (MessageDigest/getInstance algorithm)] 49 | (.reset algo) 50 | (doseq [^bytes b message] (.update algo b)) 51 | (signature algo))) 52 | 53 | String 54 | (-digest [message algorithm] 55 | (-digest [(.getBytes message)] algorithm)) 56 | 57 | InputStream 58 | (-digest [reader algorithm] 59 | (-digest (byte-seq reader) algorithm)) 60 | 61 | File 62 | (-digest [file algorithm] 63 | (with-open [f (FileInputStream. file)] 64 | (-digest f algorithm))) 65 | 66 | nil 67 | (-digest [message algorithm] 68 | nil)) 69 | 70 | (defn digest 71 | "Returns digest for message with given algorithm." 72 | [algorithm message] 73 | (-digest message algorithm)) 74 | 75 | (defn algorithms 76 | "List supported digest algorithms." 77 | [] 78 | (let [providers (vec (Security/getProviders)) 79 | names (mapcat (fn [^Provider p] (enumeration-seq (.keys p))) providers) 80 | digest-names (filter #(re-find #"MessageDigest\.[A-Z0-9-]+$" %) names)] 81 | (set (map #(last (split % #"\.")) digest-names)))) 82 | 83 | (defn- create-fn! 84 | [algorithm-name] 85 | (let [update-meta (fn [meta] 86 | (assoc meta 87 | :doc (str "Encode the given message with the " algorithm-name " algorithm.") 88 | :arglists '([message])))] 89 | (-> (intern *ns* 90 | (symbol (lower-case algorithm-name)) 91 | (partial digest algorithm-name)) 92 | (alter-meta! update-meta)))) 93 | 94 | (defn- create-fns 95 | "Create utility function for each digest algorithms. 96 | For example will create an md5 function for MD5 algorithm." 97 | [] 98 | (doseq [algorithm (algorithms)] 99 | (create-fn! algorithm))) 100 | 101 | ; Create utility functions such as md5, sha-256 ... 102 | (create-fns) 103 | 104 | ;;;; Hints for clj-kondo 105 | 106 | (comment 107 | (declare sha3-384 sha-256 sha3-256 sha-384 sha3-512 sha-1 sha-224 sha1 sha-512 md2 sha sha3-224 md5) 108 | ) 109 | -------------------------------------------------------------------------------- /src/digest.clj: -------------------------------------------------------------------------------- 1 | (ns 2 | ^{:author "Miki Tebeka " 3 | :doc "Message digest algorithms for Clojure" 4 | ;; single segment namespace is deprecated, use clj-commons/digest 5 | :deprecated true 6 | :no-doc true} 7 | digest 8 | (:require [clojure.string :refer [join lower-case split]]) 9 | (:import (java.io File FileInputStream InputStream) 10 | (java.security MessageDigest Provider Security) 11 | (java.util Arrays))) 12 | 13 | ; Default buffer size for reading 14 | (def ^:dynamic *buffer-size* 1024) 15 | 16 | (defn- read-some 17 | "Read some data from reader. Return [data size] if there's more to read, 18 | otherwise nil." 19 | [^InputStream reader] 20 | (let [^bytes buffer (make-array Byte/TYPE *buffer-size*) 21 | size (.read reader buffer)] 22 | (when (pos? size) 23 | (if (= size *buffer-size*) buffer (Arrays/copyOf buffer size))))) 24 | 25 | (defn- byte-seq 26 | "Return a sequence of [data size] from reader." 27 | [^InputStream reader] 28 | (take-while some? (repeatedly (partial read-some reader)))) 29 | 30 | (defn- signature 31 | "Get signature (string) of digest." 32 | [^MessageDigest algorithm] 33 | (let [size (* 2 (.getDigestLength algorithm)) 34 | sig (.toString (BigInteger. 1 (.digest algorithm)) 16) 35 | padding (join (repeat (- size (count sig)) "0"))] 36 | (str padding sig))) 37 | 38 | (defprotocol Digestible 39 | (-digest [message algorithm])) 40 | 41 | (extend-protocol Digestible 42 | (class (make-array Byte/TYPE 0)) 43 | (-digest [message algorithm] 44 | (-digest [message] algorithm)) 45 | 46 | java.util.Collection 47 | ;; Code "borrowed" from 48 | ;; * http://www.holygoat.co.uk/blog/entry/2009-03-26-1 49 | ;; * http://www.rgagnon.com/javadetails/java-0416.html 50 | (-digest [message algorithm] 51 | (let [^MessageDigest algo (MessageDigest/getInstance algorithm)] 52 | (.reset algo) 53 | (doseq [^bytes b message] (.update algo b)) 54 | (signature algo))) 55 | 56 | String 57 | (-digest [message algorithm] 58 | (-digest [(.getBytes message)] algorithm)) 59 | 60 | InputStream 61 | (-digest [reader algorithm] 62 | (-digest (byte-seq reader) algorithm)) 63 | 64 | File 65 | (-digest [file algorithm] 66 | (with-open [f (FileInputStream. file)] 67 | (-digest f algorithm))) 68 | 69 | nil 70 | (-digest [message algorithm] 71 | nil)) 72 | 73 | (defn digest 74 | "Returns digest for message with given algorithm." 75 | [algorithm message] 76 | (-digest message algorithm)) 77 | 78 | (defn algorithms 79 | "List support digest algorithms." 80 | [] 81 | (let [providers (vec (Security/getProviders)) 82 | names (mapcat (fn [^Provider p] (enumeration-seq (.keys p))) providers) 83 | digest-names (filter #(re-find #"MessageDigest\.[A-Z0-9-]+$" %) names)] 84 | (set (map #(last (split % #"\.")) digest-names)))) 85 | 86 | (defn create-fn! 87 | [algorithm-name] 88 | (let [update-meta (fn [meta] 89 | (assoc meta 90 | :doc (str "Encode the given message with the " algorithm-name " algorithm.") 91 | :arglists '([message])))] 92 | (-> (intern 'digest 93 | (symbol (lower-case algorithm-name)) 94 | (partial digest algorithm-name)) 95 | (alter-meta! update-meta)))) 96 | 97 | (defn- create-fns 98 | "Create utility function for each digest algorithms. 99 | For example will create an md5 function for MD5 algorithm." 100 | [] 101 | (doseq [algorithm (algorithms)] 102 | (create-fn! algorithm))) 103 | 104 | ; Create utility functions such as md5, sha-256 ... 105 | (create-fns) 106 | -------------------------------------------------------------------------------- /test/clj_commons/digest_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.digest-test 2 | (:require [clj-commons.digest :as d] 3 | [clojure.string :refer [lower-case includes?]] 4 | [clojure.test :refer [deftest is]]) 5 | (:import java.io.File)) 6 | 7 | (deftest md5-test 8 | (is (= (d/digest "md5" "clojure") "32c0d97f82a20e67c6d184620f6bd322"))) 9 | 10 | (deftest sha-256-test 11 | (is (= (d/sha-256 "clojure") 12 | "4f3ea34e0a3a6196a18ec24b51c02b41d5f15bd04b4a94aa29e4f6badba0f5b0"))) 13 | 14 | (deftest algorithms-test 15 | (let [names (d/algorithms)] 16 | (is (seq names)) 17 | (is (names "SHA-1")))) 18 | 19 | (deftest utils-test 20 | (for [name (d/algorithms)] 21 | (dorun (is (ns-resolve *ns* (symbol (lower-case name))))))) 22 | 23 | (deftest function-metadata-test 24 | (is (includes? (:doc (meta #'d/sha-256)) 25 | "SHA-256")) 26 | (is (= '([message]) 27 | (:arglists (meta #'d/md5))))) 28 | 29 | (def ^:dynamic *image-md5* "49c39580caf91363e4a4cacfa5564489") 30 | (def ^:dynamic *image-sha1* 31 | "96f2328cf279b95ddb1dee36df0c91cd7821e741") 32 | 33 | (deftest file-test 34 | (let [f (File. "test/snail.png")] 35 | (is (= (d/md5 f) *image-md5*)) 36 | (is (= (d/sha-1 f) *image-sha1*)))) 37 | 38 | ; Just making sure that we don't explode on nil 39 | (deftest nil-test 40 | (d/md5 nil)) 41 | 42 | (deftest length-test 43 | (is (= (d/sha (File. "test/quote.txt")) 44 | "dc93ad3c1e212bf598b9bf700914e832c9bdade5"))) 45 | -------------------------------------------------------------------------------- /test/digest_test.clj: -------------------------------------------------------------------------------- 1 | (ns digest-test 2 | (:require [clojure.string :refer [lower-case includes?]] 3 | [clojure.test :refer :all] 4 | [digest :refer :all]) 5 | (:import java.io.File)) 6 | 7 | (deftest md5-test 8 | (is (= (digest "md5" "clojure") "32c0d97f82a20e67c6d184620f6bd322"))) 9 | 10 | (deftest sha-256-test 11 | (is (= (sha-256 "clojure") 12 | "4f3ea34e0a3a6196a18ec24b51c02b41d5f15bd04b4a94aa29e4f6badba0f5b0"))) 13 | 14 | (deftest algorithms-test 15 | (let [names (algorithms)] 16 | (is (not (empty? names))) 17 | (is (names "SHA-1")))) 18 | 19 | (deftest utils-test 20 | (for [name (algorithms)] 21 | (dorun (is (ns-resolve *ns* (symbol (lower-case name))))))) 22 | 23 | (deftest function-metadata-test 24 | (is (includes? (:doc (meta #'sha-256)) 25 | "SHA-256")) 26 | (is (= '([message]) 27 | (:arglists (meta #'md5))))) 28 | 29 | (def ^:dynamic *image-md5* "49c39580caf91363e4a4cacfa5564489") 30 | (def ^:dynamic *image-sha1* 31 | "96f2328cf279b95ddb1dee36df0c91cd7821e741") 32 | 33 | (deftest file-test 34 | (let [f (File. "test/snail.png")] 35 | (is (= (md5 f) *image-md5*)) 36 | (is (= (sha-1 f) *image-sha1*)))) 37 | 38 | ; Just making sure that we don't explode on nil 39 | (deftest nil-test 40 | (md5 nil)) 41 | 42 | (deftest length-test 43 | (is (= (sha (File. "test/quote.txt")) 44 | "dc93ad3c1e212bf598b9bf700914e832c9bdade5"))) 45 | -------------------------------------------------------------------------------- /test/quote.txt: -------------------------------------------------------------------------------- 1 | Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp. 2 | - Greenspun's tenth rule of programming 3 | -------------------------------------------------------------------------------- /test/snail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/digest/6366ce792684eaeaab2b61ec3f520378269ed920/test/snail.png -------------------------------------------------------------------------------- /version.edn: -------------------------------------------------------------------------------- 1 | "1.4" 2 | --------------------------------------------------------------------------------