├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── cljs_repl.clj ├── doc └── generate.sh ├── project.clj ├── src └── multihash │ ├── core.cljc │ └── digest.clj └── test ├── coveralls.sh └── multihash ├── core_test.cljc ├── digest_test.clj └── test_runner.cljs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | working_drectory: ~/clj-multihash 6 | docker: 7 | - image: circleci/clojure:lein-2.7.1 8 | environment: 9 | QT_QPA_PLATFORM: offscreen 10 | steps: 11 | - run: sudo apt-get update && sudo apt-get install -y phantomjs 12 | - checkout 13 | - restore_cache: 14 | keys: 15 | - clj-multihash-{{ checksum "project.clj" }} 16 | - clj-multihash- 17 | - run: lein deps 18 | - run: lein check 19 | - run: lein test 20 | - run: lein cljs-test 21 | - run: lein coverage --codecov 22 | - save_cache: 23 | key: clj-multihash-{{ checksum "project.clj" }} 24 | paths: 25 | - ~/.m2 26 | - store_artifacts: 27 | path: target/coverage 28 | destination: coverage 29 | - run: 30 | name: Publish Coverage 31 | command: "(curl -s https://codecov.io/bash > codecov) && bash codecov -f target/coverage/codecov.json" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /checkouts/ 3 | /.lein-* 4 | /.nrepl-port 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | All notable changes to this project will be documented in this file, which 5 | follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 6 | This project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ... 11 | 12 | ## [2.0.3] - 2018-01-22 13 | 14 | ### Added 15 | - Multihash objects implement the Java `Serializable` interface so they work 16 | better with Apache Spark. 17 | 18 | ## [2.0.2] - 2017-11-04 19 | 20 | ### Changed 21 | - Update various plugins and dependencies. 22 | - Upgrade to CircleCI 2.0. 23 | - Publish coverage to codecov. 24 | 25 | ## [2.0.1] - 2016-07-26 26 | 27 | ### Changed 28 | - Upgrade dependency versions. 29 | - Switch to CircleCI for tests. 30 | 31 | ## [2.0.0] - 2016-03-23 32 | 33 | This is a major release that migrates to a cross-compiled codebase supporting 34 | oth Clojure and ClojureScript. The base conversion logic has moved to 35 | [alphabase](https://github.com/greglook/alphabase), and the hash constructors in 36 | the core namespace have moved to `multihash.digest` since they differ 37 | significantly between languages. 38 | 39 | ### Changed 40 | - Change most files to `cljc` to support cross-compilation. 41 | - Change most exceptions to `ex-info`. 42 | - Move the `functions`, `test`, `sha1`, `sha2-256`, and `sha2-512` vars from 43 | `multihash.core` to `multihash.digest`. 44 | 45 | ### Removed 46 | - Drop `multihash.hex` and `multihash.base58` namespaces in favor of 47 | `mvxcvi/alphabase`. 48 | 49 | ## [1.1.0] - 2015-11-09 50 | 51 | ### Added 52 | - Make multihash hex digest available directly via the `:hex-digest` keyword. 53 | 54 | ### Changed 55 | - `hex/encode` returns an empty string for empty byte arrays instead of nil. 56 | - `hex/decode` returns an empty byte array for empty strings instead of nil. 57 | 58 | ## [1.0.0] - 2015-10-29 59 | 60 | ### Added 61 | - Hash functions now support Strings, ByteBuffers, and InputStreams in addition 62 | to byte arrays. 63 | - Add `functions` map of keywords to supported hash functions. 64 | - Add `test` function to validate a multihash against some content. 65 | 66 | ### Fixed 67 | - Add type hints to a number of functions. 68 | 69 | ## [0.2.0] - 2015-09-26 70 | 71 | ### Added 72 | - Add `multihash.base58` encoding functions. 73 | - Refactor into `multihash.hex` encoding functions. 74 | - Define a `print-method` for multihashes. 75 | - Add support for reading a multihash from a byte stream. 76 | 77 | ### Changed 78 | - Upgrade to clojure 1.7.0. 79 | - Change `Multihash` fields to be prefixed with an underscore. 80 | - Change `(:digest multihash)` to return a byte array. 81 | 82 | ### Removed 83 | - `(:bytes multihash)` no longer returns a byte array. 84 | 85 | ## [0.1.2] - 2015-06-06 86 | 87 | ### Changed 88 | - Rename `algorithms` map to `algorithm-codes`. 89 | - Improve conditionals in validation functions. 90 | 91 | ## 0.1.1 - 2015-03-06 92 | 93 | Initial project release. 94 | 95 | [Unreleased]: https://github.com/greglook/clj-multihash/compare/2.0.3...HEAD 96 | [2.0.3]: https://github.com/greglook/clj-multihash/compare/2.0.2...2.0.3 97 | [2.0.2]: https://github.com/greglook/clj-multihash/compare/2.0.1...2.0.2 98 | [2.0.1]: https://github.com/greglook/clj-multihash/compare/2.0.0...2.0.1 99 | [2.0.0]: https://github.com/greglook/clj-multihash/compare/1.1.0...2.0.0 100 | [1.1.0]: https://github.com/greglook/clj-multihash/compare/1.0.0...1.1.0 101 | [1.0.0]: https://github.com/greglook/clj-multihash/compare/0.2.0...1.0.0 102 | [0.2.0]: https://github.com/greglook/clj-multihash/compare/0.1.2...0.2.0 103 | [0.1.2]: https://github.com/greglook/clj-multihash/compare/0.1.1...0.1.2 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained and has been archived.** 2 | 3 | clj-multihash 4 | =============1 5 | 6 | [![Circle CI](https://img.shields.io/circleci/project/github/multiformats/clj-multihash.svg?style=flat-square)](https://circleci.com/gh/multiformats/clj-multihash) 7 | [![codecov.io](https://img.shields.io/codecov/c/github/multiformats/clj-multihash.svg?style=flat-square&branch=develop)](https://codecov.io/github/multiformats/clj-multihash?branch=develop) 8 | [![API codox](https://img.shields.io/badge/doc-API-blue.svg)](https://multiformats.github.io/clj-multihash/api/multihash.core.html) 9 | [![marginalia docs](https://img.shields.io/badge/doc-marginalia-blue.png)](https://multiformats.github.io/clj-multihash/marginalia/uberdoc.html) 10 | 11 | [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](https://webchat.freenode.net/?channels=%23ipfs) 12 | [![](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 13 | [![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)](https://github.com/multiformats/multiformats) 14 | 15 | > Clojure implementation of the Multihash spec 16 | 17 | A Clojure library implementing the 18 | [multihash](https://github.com/multiformats/multihash) standard. This provides an 19 | extensible, efficient binary representation for cryptographic hashes. 20 | 21 | ## Table of Contents 22 | 23 | - [Install](#install) 24 | - [Usage](#usage) 25 | - [Serialization](#serialization) 26 | - [Maintainers](#maintainers) 27 | - [Contribute](#contribute) 28 | - [License](#license) 29 | 30 | ## Install 31 | 32 | Library releases are published on Clojars. To use the latest version with 33 | Leiningen, add the following dependency to your project definition: 34 | 35 | [![Clojars Project](https://clojars.org/mvxcvi/multihash/latest-version.svg)](https://clojars.org/mvxcvi/multihash) 36 | 37 | ## Usage 38 | 39 | The main focus of this library is the `Multihash` type, which serves as an 40 | immutable representation of the output digest for a specific hash algorithm. 41 | Multihashes are sortable, support metadata, and implement value-based equality 42 | and hash-code generation. 43 | 44 | ```clojure 45 | (require '[multihash.core :as multihash] 46 | '[multihash.digest :as digest]) 47 | 48 | ; List the known hash functions: 49 | => (keys multihash/algorithm-codes) 50 | (:sha1 :sha2-256 :sha2-512 :sha3 :blake2b :blake2s) 51 | 52 | ; Manually create a multihash value: 53 | => (multihash/create :sha1 "0f1e2d3c4b5a6978") 54 | # 55 | 56 | ; Or use one of the constructors for built-in Java implementations: 57 | => (keys digest/functions) 58 | (:sha1 :sha2-256 :sha2-512) 59 | 60 | => (def mhash (digest/sha2-256 "foo bar baz")) 61 | #'user/mhash 62 | 63 | ; Properties can be accessed using keyword lookups: 64 | => (:algorithm mhash) 65 | :sha2-256 66 | => (:code mhash) 67 | 18 68 | => (:length mhash) 69 | 32 ; bytes 70 | 71 | ; :digest returns a *copy* of the digest bytes: 72 | => (:digest mhash) 73 | #bin "29MYwcRiruhy9BEJpN/TBIhxoD3t0P4OdXztV9rW8tc=" 74 | ``` 75 | 76 | ### Serialization 77 | 78 | One of the appeals of the multihash standard is that it can be serialized 79 | without specifying an encoding for the resulting byte representation. This 80 | library provides several formats for multihashes: 81 | 82 | ```clojure 83 | ; Multihashes render as URN-like strings: 84 | => (str mhash) 85 | "hash:sha2-256:dbd318c1c462aee872f41109a4dfd3048871a03dedd0fe0e757ced57dad6f2d7" 86 | 87 | ; Directly encode a multihash into a byte array: 88 | => (multihash/encode mhash) 89 | #bin "EiDb0xjBxGKu6HL0EQmk39MEiHGgPe3Q/g51fO1X2tby1w==" 90 | 91 | ; Read data to decode a multihash: 92 | => (= mhash (multihash/decode *1)) 93 | true 94 | 95 | ; Full hex encoding is supported: 96 | => (multihash/hex mhash) 97 | "1220dbd318c1c462aee872f41109a4dfd3048871a03dedd0fe0e757ced57dad6f2d7" 98 | => (= mhash (multihash/decode *1)) 99 | true 100 | 101 | ; As is base58 (compatible with IPFS): 102 | => (multihash/base58 mhash) 103 | "Qmd8kgzaFLGYtTS1zfF37qKGgYQd5yKcQMyBeSa8UkUz4W" 104 | => (= mhash (multihash/decode *1)) 105 | true 106 | ``` 107 | 108 | Decoding is implemented as a protocol, so it can be extended to other data 109 | source types as needed. 110 | 111 | ## Maintainers 112 | 113 | Captain: [@greglook](https://github.com/greglook). 114 | 115 | ## Contribute 116 | 117 | Contributions welcome. Please check out [the issues](https://github.com/multiformats/clj-multihash/issues). 118 | 119 | Check out our [contributing document](https://github.com/multiformats/multiformats/blob/master/contributing.md) 120 | for more information on how we work, and about contributing in general. Please 121 | be aware that all interactions related to multiformats are subject to the IPFS 122 | [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 123 | 124 | Small note: If editing the README, please conform to the 125 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 126 | 127 | ## License 128 | 129 | This is free and unencumbered software released into the public domain. 130 | See the [UNLICENSE](UNLICENSE) file for more information. 131 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /cljs_repl.clj: -------------------------------------------------------------------------------- 1 | (require 'cljs.repl) 2 | (require 'cljs.repl.nashorn) 3 | 4 | (cljs.repl/repl 5 | (cljs.repl.nashorn/repl-env) 6 | :watch "src" 7 | :output-dir "target") 8 | -------------------------------------------------------------------------------- /doc/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOC_BRANCH=gh-pages 4 | DOC_TARGET=target/doc 5 | 6 | if [[ ! -d $DOC_TARGET ]]; then 7 | GIT_REMOTE=$(git remote -v | head -1 | awk '{ print $2; }') 8 | echo "Cloning $DOC_BRANCH branch from $GIT_REMOTE into $DOC_TARGET ..." 9 | mkdir -p $(dirname $DOC_TARGET) || exit 2 10 | git clone $GIT_REMOTE $DOC_TARGET || exit 2 11 | pushd $DOC_TARGET 12 | if [[ $1 == "init" ]]; then 13 | git checkout --orphan $DOC_BRANCH 14 | git symbolic-ref HEAD refs/heads/$DOC_BRANCH 15 | rm .git/index 16 | git clean -fdx 17 | else 18 | git checkout $DOC_BRANCH || exit 3 19 | fi 20 | popd 21 | fi 22 | 23 | echo "Generating documentation in $DOC_TARGET ..." 24 | lein with-profile +doc do codox, marg --dir $DOC_TARGET/marginalia 25 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mvxcvi/multihash "2.1.0-SNAPSHOT" 2 | :description "Native Clojure implementation of the multihash standard." 3 | :url "https://github.com/multiformats/clj-multihash" 4 | :license {:name "Public Domain" 5 | :url "http://unlicense.org/"} 6 | 7 | :deploy-branches ["master"] 8 | :pedantic? :abort 9 | 10 | :aliases 11 | {"cljs-repl" ["run" "-m" "clojure.main" "cljs_repl.clj"] 12 | "cljs-test" ["doo" "phantom" "test" "once"] 13 | "coverage" ["with-profile" "+test,+coverage" "cloverage"]} 14 | 15 | :plugins 16 | [[lein-cljsbuild "1.1.7"] 17 | [lein-doo "0.1.8" :exclusions [org.clojure/clojurescript]]] 18 | 19 | :dependencies 20 | [[mvxcvi/alphabase "1.0.0"]] 21 | 22 | :cljsbuild 23 | {:builds {:test {:source-paths ["src" "test"] 24 | :compiler {:output-dir "target/cljs/out" 25 | :output-to "target/cljs/tests.js" 26 | :main multihash.test-runner 27 | :optimizations :whitespace}}}} 28 | 29 | :codox 30 | {:metadata {:doc/format :markdown} 31 | :source-uri "https://github.com/multiformats/clj-multihash/blob/master/{filepath}#L{line}" 32 | :output-path "target/doc/api"} 33 | 34 | :whidbey 35 | {:tag-types {'multihash.core.Multihash {'data/hash 'multihash.core/base58}}} 36 | 37 | :profiles 38 | {:dev 39 | {:dependencies [[org.clojure/clojure "1.8.0"] 40 | [org.clojure/clojurescript "1.9.946"]]} 41 | 42 | :coverage 43 | {:plugins [[lein-cloverage "1.0.10"]] 44 | :dependencies [[org.clojure/tools.reader "1.1.0"]]}}) 45 | -------------------------------------------------------------------------------- /src/multihash/core.cljc: -------------------------------------------------------------------------------- 1 | (ns multihash.core 2 | "Core multihash type definition and helper methods." 3 | (:require 4 | [alphabase.base58 :as b58] 5 | [alphabase.bytes :as bytes] 6 | [alphabase.hex :as hex]) 7 | #?(:clj (:import 8 | (clojure.lang ILookup IMeta IObj) 9 | java.io.InputStream))) 10 | 11 | 12 | ;; ## Hash Function Algorithms 13 | 14 | (def ^:const algorithm-codes 15 | "Map of information about the available content hashing algorithms." 16 | {:sha1 0x11 17 | :sha2-256 0x12 18 | :sha2-512 0x13 19 | :sha3 0x14 20 | :blake2b 0x40 21 | :blake2s 0x41}) 22 | 23 | 24 | (defn app-code? 25 | "True if the given code number is assigned to the application-specfic range. 26 | Returns nil if the argument is not an integer." 27 | [code] 28 | (when (integer? code) 29 | (< 0 code 0x10))) 30 | 31 | 32 | (defn get-algorithm 33 | "Looks up an algorithm by keyword name or code number. Returns `nil` if the 34 | value does not map to any valid algorithm." 35 | [value] 36 | (cond 37 | (keyword? value) 38 | (when-let [code (get algorithm-codes value)] 39 | {:code code, :name value}) 40 | 41 | (not (integer? value)) 42 | nil 43 | 44 | (app-code? value) 45 | {:code value, :name (keyword (str "app-" value))} 46 | 47 | :else 48 | (some #(when (= value (val %)) 49 | {:code value, :name (key %)}) 50 | algorithm-codes))) 51 | 52 | 53 | 54 | ;; ## Multihash Type 55 | 56 | ;; Multihash identifiers have two properties: 57 | ;; 58 | ;; - `code` is a numeric code for an algorithm entry in `algorithm-codes` or an 59 | ;; application-specific algorithm code. 60 | ;; - `hex-digest` is a string holding the hex-encoded algorithm output. 61 | ;; 62 | ;; Multihash values also support metadata. 63 | (deftype Multihash 64 | [^long code ^String hex-digest _meta] 65 | 66 | Object 67 | 68 | (toString 69 | [this] 70 | (str "hash:" (name (:name (get-algorithm code))) \: hex-digest)) 71 | 72 | #?(:clj java.io.Serializable) 73 | 74 | #?(:cljs IEquiv) 75 | 76 | (#?(:clj equals, :cljs -equiv) 77 | [this that] 78 | (cond 79 | (identical? this that) true 80 | (instance? Multihash that) 81 | (and (= code (:code that)) 82 | (= hex-digest (:hex-digest that))) 83 | :else false)) 84 | 85 | 86 | #?(:cljs IHash) 87 | 88 | (#?(:clj hashCode, :cljs -hash) 89 | [this] 90 | (hash-combine code hex-digest)) 91 | 92 | 93 | #?(:clj Comparable, :cljs IComparable) 94 | 95 | (#?(:clj compareTo, :cljs -compare) 96 | [this that] 97 | (cond 98 | (= this that) 0 99 | (< code (:code that)) -1 100 | (> code (:code that)) 1 101 | :else (compare hex-digest (:hex-digest that)))) 102 | 103 | 104 | ILookup 105 | 106 | (#?(:clj valAt, :cljs -lookup) 107 | [this k] 108 | (#?(:clj .valAt, :cljs -lookup) this k nil)) 109 | 110 | (#?(:clj valAt, :cljs -lookup) 111 | [this k not-found] 112 | (case k 113 | :code code 114 | :algorithm (:name (get-algorithm code)) 115 | :length (/ (count hex-digest) 2) 116 | :digest (hex/decode hex-digest) 117 | :hex-digest hex-digest 118 | not-found)) 119 | 120 | 121 | IMeta 122 | 123 | (#?(:clj meta, :cljs -meta) 124 | [this] 125 | _meta) 126 | 127 | 128 | #?(:clj IObj, :cljs IWithMeta) 129 | 130 | (#?(:clj withMeta, :cljs -with-meta) 131 | [this meta-map] 132 | (Multihash. code hex-digest meta-map))) 133 | 134 | 135 | (defn create 136 | "Constructs a new Multihash identifier. Accepts either a numeric algorithm 137 | code or a keyword name as the first argument. The digest may either by a byte 138 | array or a hex string." 139 | [algorithm digest] 140 | (let [algo (get-algorithm algorithm)] 141 | (when-not (integer? (:code algo)) 142 | (throw (ex-info 143 | (str "Argument " (pr-str algorithm) " does not " 144 | "represent a valid hash algorithm.") 145 | {:algorithm algorithm}))) 146 | (let [hex-digest (if (string? digest) digest (hex/encode digest)) 147 | byte-len (/ (count hex-digest) 2)] 148 | (when (< 127 byte-len) 149 | (throw (ex-info (str "Digest length must be less than 128 bytes: " 150 | byte-len) 151 | {:length byte-len}))) 152 | (when-let [err (hex/validate hex-digest)] 153 | (throw (ex-info err {:hex-digest hex-digest}))) 154 | (->Multihash (:code algo) hex-digest nil)))) 155 | 156 | 157 | 158 | ;; ## Encoding and Decoding 159 | 160 | (defn encode 161 | "Encodes a multihash into a binary representation." 162 | ^bytes 163 | [mhash] 164 | (let [length (:length mhash) 165 | encoded (bytes/byte-array (+ length 2))] 166 | (bytes/set-byte encoded 0 (:code mhash)) 167 | (bytes/set-byte encoded 1 length) 168 | (bytes/copy (:digest mhash) 0 encoded 2 length) 169 | encoded)) 170 | 171 | 172 | (defn hex 173 | "Encodes a multihash into a hexadecimal string." 174 | ^String 175 | [mhash] 176 | (when mhash 177 | (hex/encode (encode mhash)))) 178 | 179 | 180 | (defn base58 181 | "Encodes a multihash into a Base-58 string." 182 | ^String 183 | [mhash] 184 | (when mhash 185 | (b58/encode (encode mhash)))) 186 | 187 | 188 | (defn decode-array 189 | "Decodes a byte array directly into multihash. Throws `ex-info` with a `:type` 190 | of `:multihash/bad-input` if the data is malformed or invalid." 191 | [^bytes encoded] 192 | (let [encoded-size (alength encoded) 193 | min-size 3] 194 | (when (< encoded-size min-size) 195 | (throw (ex-info 196 | (str "Cannot read multihash from byte array: " encoded-size 197 | " is less than the minimum of " min-size) 198 | {:type :multihash/bad-input})))) 199 | (let [code (bytes/get-byte encoded 0) 200 | length (bytes/get-byte encoded 1) 201 | payload (- (alength encoded) 2)] 202 | (when-not (pos? length) 203 | (throw (ex-info 204 | (str "Encoded length " length " is invalid") 205 | {:type :multihash/bad-input}))) 206 | (when (< payload length) 207 | (throw (ex-info 208 | (str "Encoded digest length " length " exceeds actual " 209 | "remaining payload of " payload " bytes") 210 | {:type :multihash/bad-input}))) 211 | (let [digest (bytes/byte-array length)] 212 | (bytes/copy encoded 2 digest 0 length) 213 | (create code digest)))) 214 | 215 | 216 | #?(:clj 217 | (defn- read-stream-digest 218 | "Reads a byte digest array from an input stream. First reads a byte giving 219 | the length of the digest data to read. Throws an ex-info if the length is 220 | invalid or there is an error reading from the stream." 221 | ^bytes 222 | [^InputStream input] 223 | (let [length (.read input)] 224 | (when-not (pos? length) 225 | (throw (ex-info 226 | (format "Byte %02x is not a valid digest length." length) 227 | {:type :multihash/bad-input}))) 228 | (let [digest (byte-array length)] 229 | (loop [offset 0 230 | remaining length] 231 | (let [n (.read input digest offset remaining)] 232 | (if (< n remaining) 233 | (recur (+ offset n) (- remaining n)) 234 | digest))))))) 235 | 236 | 237 | (defprotocol Decodable 238 | "This protocol provides a method for data sources which a multihash can be 239 | read from." 240 | 241 | (decode 242 | [source] 243 | "Attempts to read a multihash value from the data source.")) 244 | 245 | 246 | (extend-protocol Decodable 247 | 248 | #?(:clj (class (byte-array 0)) 249 | :cljs js/Uint8Array) 250 | 251 | (decode 252 | [source] 253 | (decode-array source)) 254 | 255 | 256 | #?(:clj java.lang.String 257 | :cljs string) 258 | 259 | (decode 260 | [source] 261 | (decode-array 262 | (if (hex/valid? (str source)) 263 | (hex/decode (str source)) 264 | (b58/decode (str source))))) 265 | 266 | 267 | #?@(:clj 268 | [InputStream 269 | 270 | (decode 271 | [source] 272 | (let [code (.read source) 273 | digest (read-stream-digest source)] 274 | (create code digest)))])) 275 | -------------------------------------------------------------------------------- /src/multihash/digest.clj: -------------------------------------------------------------------------------- 1 | (ns multihash.digest 2 | "Digest functions for creating new multihash constructors." 3 | (:refer-clojure :exclude [test]) 4 | (:require 5 | [multihash.core :as multihash]) 6 | (:import 7 | (java.io 8 | InputStream 9 | IOException) 10 | java.nio.ByteBuffer 11 | java.security.MessageDigest)) 12 | 13 | 14 | (def functions 15 | "Map of supported multihash algorithm keys to hashing functions. Each function 16 | should take a source of binary data as the argument and return a multihash." 17 | {}) 18 | 19 | 20 | (defn- digest-content 21 | "Constructs a cryptographic digest for a given algorithm and content. Content 22 | may be in the form of a raw byte array, a `ByteBuffer`, or a string. Returns 23 | a byte array with the digest." 24 | ^bytes 25 | [digest-name content] 26 | (let [algo (MessageDigest/getInstance digest-name)] 27 | (condp instance? content 28 | String 29 | (.update algo (.getBytes ^String content)) 30 | (Class/forName "[B") 31 | (.update algo ^bytes content) 32 | ByteBuffer 33 | (.update algo ^ByteBuffer content) 34 | InputStream 35 | (let [buffer (byte-array 1024)] 36 | (loop [] 37 | (let [n (.read ^InputStream content buffer 0 (count buffer))] 38 | (when (pos? n) 39 | (.update algo buffer 0 n) 40 | (recur))))) 41 | (throw (IllegalArgumentException. 42 | (str "Don't know how to compute digest from " 43 | (class content))))) 44 | (.digest algo))) 45 | 46 | 47 | (defmacro defhash 48 | "Defines a new convenience hashing function for the given algorithm and system 49 | digest name." 50 | [algorithm digest-name] 51 | (let [fn-sym (symbol (name algorithm))] 52 | `(do 53 | (defn ~fn-sym 54 | ~(str "Calculates the " digest-name " digest of the given byte array or " 55 | "buffer and returns a multihash.") 56 | [~'content] 57 | (multihash/create ~algorithm (digest-content ~digest-name ~'content))) 58 | (alter-var-root #'functions assoc ~algorithm ~fn-sym)))) 59 | 60 | 61 | (defhash :sha1 "SHA-1") 62 | (defhash :sha2-256 "SHA-256") 63 | (defhash :sha2-512 "SHA-512") 64 | 65 | 66 | (defn test 67 | "Determines whether a multihash is a correct identifier for some content by 68 | recomputing the digest for the algorithm specified in the multihash. Returns 69 | nil if either argument is nil, true if the digest matches, or false if not. 70 | Throws an exception if the multihash specifies an unsupported algorithm." 71 | [mhash content] 72 | (when (and mhash content) 73 | (if-let [hash-fn (get functions (:algorithm mhash))] 74 | (= mhash (hash-fn content)) 75 | (throw (ex-info 76 | (format "No supported hashing function for algorithm %s to validate %s" 77 | (:algorithm mhash) mhash) 78 | {:algorithm (:algorithm mhash)}))))) 79 | -------------------------------------------------------------------------------- /test/coveralls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COVERALLS_URL='https://coveralls.io/api/v1/jobs' 4 | CLOVERAGE_VERSION='1.0.6' lein2 cloverage --coveralls 5 | curl -F 'json_file=@target/coverage/coveralls.json' "$COVERALLS_URL" 6 | -------------------------------------------------------------------------------- /test/multihash/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multihash.core-test 2 | (:require 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [deftest is testing]]) 5 | [alphabase.bytes :as bytes] 6 | [clojure.string :as str] 7 | [multihash.core :as multihash]) 8 | #?(:clj (:import 9 | clojure.lang.ExceptionInfo 10 | java.io.ByteArrayInputStream 11 | java.nio.ByteBuffer))) 12 | 13 | 14 | (deftest app-specific-codes 15 | (is (nil? (multihash/app-code? 17.3))) 16 | (is (false? (multihash/app-code? 0x00))) 17 | (doseq [code (range 0x01 0x10)] 18 | (is (true? (multihash/app-code? code)) 19 | (str "Algorithm code " code " is app-specific"))) 20 | (doseq [code (range 0x10 0x100)] 21 | (is (false? (multihash/app-code? code)) 22 | (str "Algorithm code " code " is not app-specific")))) 23 | 24 | 25 | (deftest get-algorithms 26 | (is (nil? (multihash/get-algorithm 0))) 27 | (is (nil? (multihash/get-algorithm 0x100))) 28 | (is (nil? (multihash/get-algorithm :foo/bar))) 29 | (doseq [code (range 0x01 0x10)] 30 | (let [algorithm (multihash/get-algorithm code)] 31 | (is (= code (:code algorithm))) 32 | (is (keyword? (:name algorithm))))) 33 | (doseq [[algorithm code] multihash/algorithm-codes] 34 | (let [by-name (multihash/get-algorithm algorithm) 35 | by-code (multihash/get-algorithm code)] 36 | (is (= algorithm (:name by-name))) 37 | (is (= code (:code by-code))) 38 | (is (= by-name by-code))))) 39 | 40 | 41 | (deftest constructor-validation 42 | (is (thrown? ExceptionInfo 43 | (multihash/create :no-such-algo "0beec7b8")) 44 | "Unknown algorith keyword should be rejected") 45 | (is (thrown? ExceptionInfo 46 | (multihash/create 0x2F "0beec7b8")) 47 | "Unknown numeric code should be rejected") 48 | (is (thrown? ExceptionInfo 49 | (multihash/create :sha1 nil)) 50 | "Nil digest should be rejected") 51 | (is (thrown? ExceptionInfo 52 | (multihash/create :sha1 "")) 53 | "Empty digest should be rejected") 54 | (is (thrown? ExceptionInfo 55 | (multihash/create :sha1 "018zk80q")) 56 | "Malformed digest should be rejected") 57 | (is (thrown? ExceptionInfo 58 | (multihash/create :sha1 (bytes/byte-array 128))) 59 | "Digest length should be limited to 127") 60 | (is (thrown? ExceptionInfo 61 | (multihash/create :sha1 "012")) 62 | "Odd digest length should be rejected")) 63 | 64 | 65 | (deftest value-semantics 66 | (let [a (multihash/create 0x11 "0beec7b8") 67 | b (multihash/create 0x11 "94a1be0c") 68 | c (multihash/create 0x12 "00a8b94e") 69 | c' (multihash/create 0x12 "00a8b94e")] 70 | (is (= a a) "Identical values are equal") 71 | (is (= c c') "Values with same code and digest are equal") 72 | (is (integer? (hash b)) "Hash code returns an integer") 73 | (is (= (hash c) (hash c')) "Equivalent objects return same hashcode") 74 | (is (= [a b c] (sort [c b a])) "Multihashes sort in code/digest order"))) 75 | 76 | 77 | (deftest multihash-rendering 78 | (is (= "hash:sha1:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" 79 | (str (multihash/create :sha1 "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")))) 80 | (is (= "hash:sha2-256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 81 | (str (multihash/create :sha2-256 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")))) 82 | (is (= "hash:sha1:ea347f3c5b8f0fd07b5bc95d0beecdda3c275da3" 83 | (str (multihash/create :sha1 "ea347f3c5b8f0fd07b5bc95d0beecdda3c275da3"))))) 84 | 85 | 86 | (deftest exercise-metadata 87 | (let [a (multihash/create :sha1 "dbc95275da8a3d0d0beeea3f0fd47f3cc7b55bc3") 88 | a' (vary-meta a assoc :foo :bar/baz)] 89 | (is (empty? (meta a)) "values start with empty metadata") 90 | (is (= :bar/baz (:foo (meta a'))) "metadata can be associated with value") 91 | (is (= a a') "metadata does not affect equality"))) 92 | 93 | 94 | (def examples 95 | "Test case examples." 96 | {"11140beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" 97 | [0x11 :sha1 20 "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"] 98 | 99 | "11040beec7b8" 100 | [0x11 :sha1 4 "0beec7b8"] 101 | 102 | "12202c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 103 | [0x12 :sha2-256 32 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"] 104 | 105 | "12042c26b46b" 106 | [0x12 :sha2-256 4 "2c26b46b"] 107 | 108 | "40040006b46b" 109 | [0x40 :blake2b 4 "0006b46b"]}) 110 | 111 | 112 | (deftest array-decoding-failures 113 | (is (thrown? ExceptionInfo 114 | (multihash/decode-array (bytes/byte-array 2))) 115 | "byte array must have at least three bytes") 116 | (is (thrown? ExceptionInfo 117 | (multihash/decode-array 118 | (bytes/init-bytes [0x11 0x00 0x00 0x00]))) 119 | "Encoded length must be positive") 120 | (is (thrown? ExceptionInfo 121 | (multihash/decode-array 122 | (bytes/init-bytes [0x11 0x08 0x00 0x00]))) 123 | "Encoded length must be within byte content")) 124 | 125 | 126 | #?(:clj 127 | (defn stream-fixture 128 | "Constructs a stream with certain leading bytes for testing." 129 | ([code length] 130 | (stream-fixture code length length)) 131 | ([code length actual] 132 | (let [buffer (bytes/byte-array actual)] 133 | (bytes/set-byte buffer 0 code) 134 | (bytes/set-byte buffer 1 length) 135 | (ByteArrayInputStream. buffer))))) 136 | 137 | 138 | #?(:clj 139 | (deftest stream-decoding-failures 140 | (is (thrown? ExceptionInfo 141 | (multihash/decode (stream-fixture 0x11 0 4))) 142 | "Stream with non-positive length is illegal.") 143 | (is (thrown? Exception 144 | (multihash/decode (stream-fixture 0x11 20 5))) 145 | "Stream without enough data throws exception.."))) 146 | 147 | 148 | (deftest example-coding 149 | (testing "Encoding is reflexive" 150 | (let [mhash (multihash/create 0x02 "0beec7b8")] 151 | (is (= mhash (multihash/decode (multihash/encode mhash)))))) 152 | (doseq [[hex [code algorithm length digest]] examples] 153 | (let [mhash (multihash/create algorithm digest)] 154 | (is (= code (:code mhash))) 155 | (is (= algorithm (:algorithm mhash))) 156 | (is (= length (:length mhash))) 157 | (is (= digest (:hex-digest mhash))) 158 | (is (= hex (multihash/hex mhash)) 159 | "Encoded multihashes match expected hex") 160 | (is (= mhash (multihash/decode hex)) 161 | "Hex decodes into expected multihash") 162 | (let [b58 (multihash/base58 mhash)] 163 | (is (string? b58) "Multihash encodes to a base-58 string") 164 | (is (= mhash (multihash/decode b58)) 165 | "Multihash round-trips through Base58 encoding")) 166 | #?(:clj 167 | (let [stream (ByteArrayInputStream. (multihash/encode mhash))] 168 | (is (= mhash (multihash/decode stream)) 169 | "Multihash round-trips through InputStream")))))) 170 | -------------------------------------------------------------------------------- /test/multihash/digest_test.clj: -------------------------------------------------------------------------------- 1 | (ns multihash.digest-test 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer :all] 5 | [multihash.core :as multihash] 6 | [multihash.digest :as digest]) 7 | (:import 8 | java.io.ByteArrayInputStream 9 | java.nio.ByteBuffer)) 10 | 11 | 12 | (deftest hashing-constructors 13 | (doseq [algorithm (keys digest/functions)] 14 | (testing (str (name algorithm) " hashing") 15 | (let [hash-fn (digest/functions algorithm) 16 | content "foo bar baz" 17 | mh1 (hash-fn content) 18 | mh2 (hash-fn (.getBytes content)) 19 | mh3 (hash-fn (ByteBuffer/wrap (.getBytes content))) 20 | mh4 (hash-fn (ByteArrayInputStream. (.getBytes content)))] 21 | (is (= algorithm 22 | (:algorithm mh1) 23 | (:algorithm mh2) 24 | (:algorithm mh3) 25 | (:algorithm mh4)) 26 | "Constructed multihash algorithms match") 27 | (is (= (:hex-digest mh1) 28 | (:hex-digest mh2) 29 | (:hex-digest mh3) 30 | (:hex-digest mh4)) 31 | "Constructed multihash digests match") 32 | (is (thrown? RuntimeException 33 | (hash-fn 123))))))) 34 | 35 | 36 | (deftest content-validation 37 | (let [content "baz bar foo" 38 | mhash (digest/sha1 content)] 39 | (is (nil? (digest/test nil nil))) 40 | (is (nil? (digest/test nil content))) 41 | (is (nil? (digest/test mhash nil))) 42 | (is (true? (digest/test mhash content)) 43 | "Correct multihash returns true") 44 | (is (false? (digest/test 45 | (multihash/create :sha1 "68a9f54521a5501230e9dc73") 46 | content)) 47 | "Incorrect multihash returns false") 48 | (is (thrown-with-msg? RuntimeException #"^No supported hashing function" 49 | (digest/test 50 | (multihash/create :blake2b "68a9f54521a5501230e9dc73") 51 | content)) 52 | "Unsupported hash function cannot be validated"))) 53 | -------------------------------------------------------------------------------- /test/multihash/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns multihash.test-runner 2 | (:require-macros 3 | [doo.runner :refer [doo-tests]]) 4 | (:require 5 | doo.runner 6 | multihash.core-test 7 | #_ multihash.digest-test)) 8 | 9 | 10 | (doo-tests 11 | 'multihash.core-test 12 | #_ 'multihash.digest-test) 13 | --------------------------------------------------------------------------------