├── .circleci └── config.yml ├── .clj-kondo └── config.edn ├── .cljstyle ├── .gitignore ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── bb.edn ├── bin ├── build ├── repl └── test ├── build.clj ├── deps.edn ├── dev └── multiformats │ └── repl.clj ├── src └── multiformats │ ├── address.cljc │ ├── base.cljc │ ├── cid.cljc │ ├── codec.cljc │ ├── hash.cljc │ └── varint.cljc ├── test └── multiformats │ ├── address_test.cljc │ ├── base_test.cljc │ ├── cid_test.cljc │ ├── codec_test.cljc │ ├── hash_test.cljc │ └── varint_test.cljc └── tests.edn /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # Common executor configuration 4 | executors: 5 | clojure: 6 | docker: 7 | - image: cimg/clojure:1.11-openjdk-11.0 8 | working_directory: ~/repo 9 | 10 | 11 | # Job definitions 12 | jobs: 13 | style: 14 | executor: clojure 15 | steps: 16 | - checkout 17 | - run: 18 | name: Install cljstyle 19 | environment: 20 | CLJSTYLE_VERSION: 0.17.642 21 | CLJSTYLE_PLATFORM: linux_amd64 22 | command: | 23 | wget https://github.com/greglook/cljstyle/releases/download/${CLJSTYLE_VERSION}/cljstyle_${CLJSTYLE_VERSION}_${CLJSTYLE_PLATFORM}.zip 24 | unzip cljstyle_${CLJSTYLE_VERSION}_${CLJSTYLE_PLATFORM}.zip 25 | - run: 26 | name: Check source formatting 27 | command: "./cljstyle check --report" 28 | 29 | lint: 30 | executor: clojure 31 | steps: 32 | - checkout 33 | - run: 34 | name: Install clj-kondo 35 | environment: 36 | CLJ_KONDO_VERSION: 2024.11.14 37 | command: | 38 | wget https://github.com/borkdude/clj-kondo/releases/download/v${CLJ_KONDO_VERSION}/clj-kondo-${CLJ_KONDO_VERSION}-linux-amd64.zip 39 | unzip clj-kondo-${CLJ_KONDO_VERSION}-linux-amd64.zip 40 | - run: 41 | name: clj-kondo 42 | command: "./clj-kondo --lint src:test" 43 | 44 | test-clj: 45 | executor: clojure 46 | steps: 47 | - checkout 48 | - restore_cache: 49 | keys: 50 | - v1-test-clj-{{ checksum "deps.edn" }} 51 | - v1-test-clj- 52 | - run: bin/test check 53 | - run: bin/test unit 54 | - save_cache: 55 | key: v1-test-clj-{{ checksum "deps.edn" }} 56 | paths: 57 | - ~/.m2 58 | 59 | test-cljs: 60 | executor: clojure 61 | environment: 62 | QT_QPA_PLATFORM: offscreen 63 | steps: 64 | - run: 65 | name: Install NodeJS 66 | command: | 67 | curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash - 68 | sudo apt-get install -y nodejs 69 | - checkout 70 | - restore_cache: 71 | keys: 72 | - v1-test-cljs-{{ checksum "deps.edn" }} 73 | - v1-test-cljs- 74 | - run: bin/test unit-cljs 75 | - save_cache: 76 | key: v1-test-cljs-{{ checksum "deps.edn" }} 77 | paths: 78 | - ~/.m2 79 | - node_modules 80 | 81 | test-bb: 82 | executor: clojure 83 | steps: 84 | - run: 85 | name: Install babashka 86 | command: | 87 | sudo bash < <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install) 88 | - checkout 89 | - restore_cache: 90 | keys: 91 | - v1-test-bb-{{ checksum "deps.edn" }} 92 | - v1-test-bb- 93 | - run: bin/test bb 94 | - save_cache: 95 | key: v1-test-bb-{{ checksum "deps.edn" }} 96 | paths: 97 | - ~/.m2 98 | 99 | coverage: 100 | executor: clojure 101 | steps: 102 | - checkout 103 | - restore_cache: 104 | keys: 105 | - v1-coverage-{{ checksum "deps.edn" }} 106 | - v1-coverage- 107 | - v1-test-clj- 108 | - run: 109 | name: Generate test coverage 110 | command: bin/test coverage --codecov 111 | - save_cache: 112 | key: v1-coverage-{{ checksum "deps.edn" }} 113 | paths: 114 | - ~/.m2 115 | - store_artifacts: 116 | path: target/coverage 117 | destination: coverage 118 | - run: 119 | name: Install codecov 120 | command: | 121 | sudo apt-get update && sudo apt-get install gpg 122 | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import 123 | curl -Os https://uploader.codecov.io/latest/linux/codecov 124 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 125 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 126 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 127 | shasum -a 256 -c codecov.SHA256SUM 128 | chmod +x codecov 129 | - run: 130 | name: Publish coverage report 131 | command: './codecov -f target/coverage/codecov.json' 132 | 133 | 134 | # Workflow definitions 135 | workflows: 136 | version: 2 137 | test: 138 | jobs: 139 | - style 140 | - lint 141 | - test-clj 142 | - test-cljs 143 | - test-bb 144 | - coverage: 145 | requires: 146 | - test-clj 147 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:consistent-alias 2 | {:level :warning 3 | :aliases {alphabase.bytes b 4 | alphabase.core abc 5 | clojure.string str}}}} 6 | -------------------------------------------------------------------------------- /.cljstyle: -------------------------------------------------------------------------------- 1 | ;; Clojure formatting rules 2 | ;; vim: filetype=clojure 3 | {:files 4 | {:ignore #{".git" ".cljs_node_repl" "target"}}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build tooling 2 | .nrepl-port 3 | .cljs_node_repl 4 | 5 | # Dependencies and caches 6 | /node_modules 7 | /.clj-kondo/.cache 8 | /.cpcache 9 | 10 | # Build outputs 11 | /target 12 | *.pom.asc 13 | *.class 14 | *.jar 15 | -------------------------------------------------------------------------------- /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 | 9 | ## [Unreleased] 10 | 11 | ... 12 | 13 | 14 | ## [1.0.125] - 2025-03-17 15 | 16 | This is a major release with a couple of breaking changes - the high-level 17 | functionality is the same, but several things have been simplified. 18 | 19 | ### Changed 20 | - Drop dependency on Apache `commons-codec`. 21 | - Upgrade to `alphabase` to the 3.0 major version. 22 | - Rewrite `multiformats.address` and merge `multiformats.address.codec` into it. 23 | - All `multiformats.base.*` namespaces dropped in favor of new `alphabase` implementations. 24 | 25 | ### Added 26 | - Multiaddr values are now `Counted`, `Sequential`, `Indexed`, and support `peek` and `pop`. 27 | - New predicate functions `address?`, `multihash?`, and `cid?`. 28 | 29 | ### Fixed 30 | - Additional test coverage of all namespaces. 31 | 32 | 33 | ## [0.3.107] - 2023-03-06 34 | 35 | ### Fixed 36 | - Fix babashka compatibility issue with multihashes. 37 | [#5](https://github.com/greglook/clj-multiformats/issues/5) 38 | [PR#6](https://github.com/greglook/clj-multiformats/pull/6) 39 | 40 | 41 | ## [0.3.103] - 2023-02-27 42 | 43 | ### Changed 44 | - Switch from Leiningen to tools.deps and related build infrastructure. 45 | - Unroll type definitions to make them easier to understand on each platform. 46 | - Updated style and lint compliance. 47 | 48 | ### Fixed 49 | - Fix Clojurescript IpAddress refrence in address code. 50 | [#3](https://github.com/greglook/clj-multiformats/issues/3) 51 | [PR#4](https://github.com/greglook/clj-multiformats/pull/4) 52 | 53 | ### Added 54 | - Multihashes are usable in Babashka. 55 | [PR#2](https://github.com/greglook/clj-multiformats/pull/2) 56 | 57 | 58 | ## [0.2.1] - 2020-01-05 59 | 60 | ### Changed 61 | - Updated dependencies for better JDK11 compatibility. 62 | - Style and linter compliance. 63 | 64 | 65 | ## [0.2.0] - 2019-03-10 66 | 67 | ### Added 68 | - Added hex-parsing utility method for multihashes. 69 | - Implemented [multiaddr](https://github.com/multiformats/multiaddr) support. 70 | [PR#1](https://github.com/greglook/clj-multiformats/pull/1) 71 | 72 | 73 | ## [0.1.1] - 2019-01-20 74 | 75 | ### Changed 76 | - A few dependency updates to fix reflection issues. 77 | 78 | 79 | ## 0.1.0 - 2018-12-12 80 | 81 | Initial project release. 82 | 83 | 84 | [Unreleased]: https://github.com/greglook/clj-multiformats/compare/1.0.125...HEAD 85 | [1.0.125]: https://github.com/greglook/clj-multiformats/compare/0.3.107...1.0.125 86 | [0.3.107]: https://github.com/greglook/clj-multiformats/compare/0.3.103...0.3.107 87 | [0.3.103]: https://github.com/greglook/clj-multiformats/compare/0.2.1...0.3.103 88 | [0.2.1]: https://github.com/greglook/clj-multiformats/compare/0.2.0...0.2.1 89 | [0.2.0]: https://github.com/greglook/clj-multiformats/compare/0.1.1...0.2.0 90 | [0.1.1]: https://github.com/greglook/clj-multiformats/compare/0.1.0...0.1.1 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Clojure(Script) Multiformats 2 | ============================ 3 | 4 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/greglook/clj-multiformats/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/greglook/clj-multiformats/tree/main) 5 | [![codecov](https://codecov.io/gh/greglook/clj-multiformats/branch/main/graph/badge.svg)](https://codecov.io/gh/greglook/clj-multiformats) 6 | [![cljdoc](https://cljdoc.org/badge/mvxcvi/multiformats)](https://cljdoc.org/d/mvxcvi/multiformats/CURRENT) 7 | 8 | This is a cross-compiled Clojure/CLJS library implementing the 9 | [multiformats](https://github.com/multiformats/) standards which specify 10 | self-describing value formats. 11 | 12 | This library includes support for: 13 | 14 | - Unbounded [msb varint](https://github.com/multiformats/unsigned-varint) 15 | encoding. 16 | - Flexible [multibase](https://github.com/multiformats/multibase) string 17 | encoding. 18 | - Portable [cryptographic hashes](https://github.com/multiformats/multihash). 19 | - Concise packed [codec identifiers](https://github.com/multiformats/multicodec). 20 | - IPLD [CID](https://github.com/ipld/cid) content identifiers. 21 | - Composable [multiaddress](https://github.com/multiformats/multiaddr) paths. 22 | - **TODO:** multistream 23 | 24 | 25 | ## Installation 26 | 27 | Library releases are published on Clojars. To use the latest version with 28 | Leiningen, add the following dependency to your project definition: 29 | 30 | [![Clojars Project](https://clojars.org/mvxcvi/multiformats/latest-version.svg)](https://clojars.org/mvxcvi/multiformats) 31 | 32 | 33 | ## Usage 34 | 35 | Each format may be used separately, though some build on others. 36 | 37 | ### Multibase 38 | 39 | [Multibase](https://github.com/multiformats/multibase) is a protocol for 40 | distinguishing base encodings and other simple string encodings, and for 41 | ensuring full compatibility with program interfaces. Binary data encoded as a 42 | string is first prefixed with a character which signals the encoding used for 43 | the remainder of the text. 44 | 45 | ```clojure 46 | => (require '[alphabase.bytes :as b]) 47 | => (require '[multiformats.base :as mbase]) 48 | 49 | ;; lookup supported base encodings 50 | => (take 8 (keys mbase/bases)) 51 | (:base16 :base32hex :base64pad :base64urlpad :base2 :base32 :BASE16 :base64) 52 | 53 | => (def data (b/random-bytes 16)) 54 | 55 | ;; use format to convert byte data into an encoded string 56 | => (mbase/format :base16 data) 57 | "f86f0a02d004cf7e5ff8746a6307919f4" 58 | 59 | => (mbase/format :BASE32HEX data) 60 | "VGROA0B809JRUBVS78QJ30U8PUG" 61 | 62 | ;; use parse to convert encoded strings back into bytes 63 | => (b/bytes= data (mbase/parse *1)) 64 | true 65 | 66 | => (mbase/inspect *2) 67 | {:base :BASE32HEX, :prefix "V"} 68 | ``` 69 | 70 | ### Multihash 71 | 72 | [Multihash](https://github.com/multiformats/multihash) is a protocol for 73 | differentiating outputs from various well-established cryptographic hash 74 | functions, addressing size and encoding considerations. 75 | 76 | ```clojure 77 | => (require '[multiformats.hash :as mhash]) 78 | 79 | ;; manual hash construction 80 | => (def mh (mhash/create :sha1 "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed")) 81 | => mh 82 | # 83 | 84 | ;; hash properties 85 | => (:length mh) 86 | 22 87 | => (:code mh) 88 | 17 89 | => (:algorithm mh) 90 | :sha1 91 | => (:bits mh) 92 | 160 93 | => (:digest mh) 94 | "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" 95 | 96 | ;; encode to/from bytes 97 | => (mhash/encode mh) 98 | #bin "ERQqrmw1yU/PtBXb6V9Ai5zpHuhG7Q==" 99 | => (mhash/decode *1) 100 | # 101 | => (= mh *1) 102 | true 103 | 104 | ;; render as fully encoded hex 105 | => (mhash/hex mh) 106 | "11142aae6c35c94fcfb415dbe95f408b9ce91ee846ed" 107 | ``` 108 | 109 | For convenience, several hashing functions are provided for direct digest 110 | construction. These produce multihashes and may be used to test validity. 111 | 112 | ```clojure 113 | ;; available functions 114 | => mhash/functions 115 | {:md5 #, 116 | :sha1 #, 117 | :sha2-256 #, 118 | :sha2-512 #} 119 | 120 | ;; produce a new hash 121 | => (mhash/sha2-256 "hello world") 122 | # 123 | 124 | ;; test for correctness 125 | => (mhash/test *1 "hello world") 126 | true 127 | => (mhash/test *2 "foo bar baz") 128 | false 129 | ``` 130 | 131 | ### Multicodec 132 | 133 | [Multicodec](https://github.com/multiformats/multicodec) is a multiformat which 134 | wraps other formats with a tiny bit of self-description. A multicodec identifier 135 | may either be a varint (in a byte string) or a character (in a text string). 136 | 137 | ```clojure 138 | => (require '[multiformats.codec :as mcodec]) 139 | 140 | ;; resolve a code or key to a keyword name 141 | => (mcodec/resolve-key :git-raw) 142 | :git-raw 143 | => (mcodec/resolve-key 0x51) 144 | :cbor 145 | 146 | ;; resolve a code or key to a numeric code 147 | => (mcodec/resolve-code :cbor) 148 | 81 149 | => (mcodec/resolve-code 0xb0) 150 | 176 151 | 152 | ;; register a new code 153 | => (mcodec/register! :foo-format 0x0abc) 154 | nil 155 | ``` 156 | 157 | ### Content Identifiers 158 | 159 | [CID](https://github.com/ipld/cid) is a self-describing content-addressed 160 | identifier. It uses cryptographic hashing to identify content, multicodec packed 161 | codes to label the content type, and multibase to encode the final identifier 162 | into a string. 163 | 164 | ```clojure 165 | => (require '[multiformats.cid :as cid]) 166 | 167 | ;; manual cid construction 168 | => (def cid (cid/create :cbor (mhash/create :sha1 "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"))) 169 | => cid 170 | # 171 | 172 | ;; cid properties 173 | => (:length cid) 174 | 24 175 | => (:version cid) 176 | 1 177 | => (:code cid) 178 | 81 179 | => (:codec cid) 180 | :cbor 181 | => (:bits cid) 182 | 160 183 | => (:hash cid) 184 | # 185 | 186 | ;; encode to/from bytes 187 | => (cid/encode cid) 188 | #bin "AVERFCqubDXJT8+0FdvpX0CLnOke6Ebt" 189 | => (cid/decode *1) 190 | # 191 | => (= cid *1) 192 | true 193 | 194 | ;; render as strings 195 | => (cid/format cid) 196 | "bafircfbkvzwdlskpz62blw7jl5aixhhjd3uen3i" 197 | => (cid/format :base58btc cid) 198 | "z7xnojvcv7i2mALpTu2f9tVWEG2593rNx" 199 | => (= cid (cid/parse *1)) 200 | true 201 | 202 | ;; inspection 203 | => (cid/inspect *2) 204 | {:base :base64, 205 | :prefix "m", 206 | :length 24, 207 | :version 1, 208 | :code 81, 209 | :codec :cbor, 210 | :hash #} 211 | ``` 212 | 213 | ### Multiaddress 214 | 215 | [Multiaddr](https://github.com/multiformats/multiaddr) is a multiformat which 216 | specifies network addresses in a protocol-agnostic, composable way. 217 | 218 | ```clojure 219 | => (require '[multiformats.address :as addr]) 220 | 221 | ;; There's no place like it... 222 | => (addr/create [:ip4 "127.0.0.1"]) 223 | # 224 | 225 | => (str *1) 226 | "/ip4/127.0.0.1" 227 | 228 | ;; Addresses can be treated as sequences: 229 | => (conj *2 [:tcp "80"]) 230 | # 231 | 232 | => (seq *1) 233 | ([:ip4 "127.0.0.1"] [:tcp "80"]) 234 | ``` 235 | 236 | 237 | ## License 238 | 239 | This is free and unencumbered software released into the public domain. 240 | See the [UNLICENSE](UNLICENSE) file for more information. 241 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {mvxcvi/multiformats {:local/root "."}} 3 | 4 | :tasks 5 | {test 6 | {:extra-paths ["test"] 7 | :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} 8 | :requires ([cognitect.test-runner]) 9 | :task (apply cognitect.test-runner/-main 10 | "--namespace" "multiformats.base-test" 11 | "--namespace" "multiformats.hash-test" 12 | "--namespace" "multiformats.varint-test" 13 | *command-line-args*)}}} 14 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: ft=bash 3 | 4 | set -e 5 | 6 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 7 | 8 | if [[ $1 = clean ]]; then 9 | rm -rf target 10 | 11 | elif [[ $1 = hiera ]]; then 12 | shift 13 | exec clojure -X:hiera "$@" 14 | 15 | elif [[ $1 = deploy ]]; then 16 | shift 17 | if [[ -z $CLOJARS_USERNAME && -z $CLOJARS_PASSWORD && -f $HOME/.clojure/clojars.env ]]; then 18 | source $HOME/.clojure/clojars.env 19 | fi 20 | if [[ -z $CLOJARS_USERNAME ]]; then 21 | read -p "Clojars username: " CLOJARS_USERNAME 22 | if [[ -z $CLOJARS_USERNAME ]]; then 23 | echo "No username available, aborting" >&2 24 | exit 1 25 | fi 26 | fi 27 | if [[ -z $CLOJARS_PASSWORD ]]; then 28 | read -p "Clojars deploy token: " CLOJARS_PASSWORD 29 | if [[ -z $CLOJARS_PASSWORD ]]; then 30 | echo "No deploy token available, aborting" >&2 31 | exit 1 32 | fi 33 | fi 34 | if [[ -z $CLOJARS_SIGNING_KEY ]]; then 35 | read -rp "Clojars signing key: " CLOJARS_SIGNING_KEY 36 | if [[ -z $CLOJARS_SIGNING_KEY ]]; then 37 | echo "No signing key specified, aborting" >&2 38 | exit 1 39 | fi 40 | fi 41 | export CLOJARS_USERNAME CLOJARS_PASSWORD CLOJARS_SIGNING_KEY 42 | exec clojure -T:build deploy "$@" 43 | 44 | else 45 | exec clojure -T:build "$@" 46 | fi 47 | -------------------------------------------------------------------------------- /bin/repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: ft=bash 3 | 4 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 5 | 6 | if [[ -z $1 || $1 = clj ]]; then 7 | exec clj -M:dev:repl 8 | 9 | elif [[ $1 = cljs ]]; then 10 | echo "NYI: get this working" >&2 11 | exit 2 12 | 13 | else 14 | echo "Unknown REPL type: $1" >2 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: ft=bash 3 | 4 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 5 | 6 | if [[ $1 = check ]]; then 7 | exec clojure -M:check 8 | 9 | elif [[ $1 = coverage ]]; then 10 | shift 11 | exec clojure -M:coverage "$@" 12 | 13 | elif [[ $1 = bb ]]; then 14 | shift 15 | exec bb test "$@" 16 | 17 | else 18 | [[ -d node_modules/ws ]] || npm install --no-save ws 19 | exec clojure -M:test "$@" 20 | fi 21 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | "Build instructions for clj-multiformats. 3 | 4 | Different tasks accept different arguments, but some common ones are: 5 | 6 | - `:force` 7 | Perform actions without prompting for user input. 8 | - `:qualifier` 9 | Apply a qualifier to the release, such as 'rc1'. 10 | - `:snapshot` 11 | If true, prepare a SNAPSHOT release." 12 | (:require 13 | [clojure.java.io :as io] 14 | [clojure.string :as str] 15 | [clojure.tools.build.api :as b] 16 | [deps-deploy.deps-deploy :as d]) 17 | (:import 18 | java.time.LocalDate)) 19 | 20 | 21 | (def basis (b/create-basis {:project "deps.edn"})) 22 | 23 | (def lib-name 'mvxcvi/multiformats) 24 | (def major-version "1.0") 25 | 26 | (def src-dir "src") 27 | (def class-dir "target/classes") 28 | 29 | 30 | ;; ## Utilities 31 | 32 | (defn clean 33 | "Remove compiled artifacts." 34 | [opts] 35 | (b/delete {:path "target"}) 36 | opts) 37 | 38 | 39 | ;; ## Version and Releases 40 | 41 | (defn- version-info 42 | "Compute the current version information." 43 | ([opts] 44 | (version-info opts false)) 45 | ([opts next?] 46 | {:tag (str major-version 47 | "." 48 | (cond-> (parse-long (b/git-count-revs nil)) 49 | next? inc) 50 | (when-let [qualifier (:qualifier opts)] 51 | (str "-" qualifier)) 52 | (when (:snapshot opts) 53 | "-SNAPSHOT")) 54 | :commit (b/git-process {:git-args "rev-parse HEAD"}) 55 | :date (str (LocalDate/now))})) 56 | 57 | 58 | (defn- format-version 59 | "Format the version string from the `version-info` map." 60 | [version] 61 | (let [{:keys [tag commit date]} version] 62 | (format "%s %s (built from %s on %s)" lib-name tag commit date))) 63 | 64 | 65 | (defn print-version 66 | "Print the current version information." 67 | [opts] 68 | (let [version (version-info opts)] 69 | (println (format-version version)) 70 | (assoc opts :version version))) 71 | 72 | 73 | (defn- update-changelog 74 | "Stamp the CHANGELOG file with the new version." 75 | [version] 76 | (let [{:keys [tag date]} version 77 | file (io/file "CHANGELOG.md") 78 | changelog (slurp file)] 79 | (when (str/includes? changelog "## [Unreleased]\n\n...\n") 80 | (binding [*out* *err*] 81 | (println "Changelog does not appear to have been updated with changes, aborting") 82 | (System/exit 3))) 83 | (-> changelog 84 | (str/replace #"## \[Unreleased\]" 85 | (str "## [Unreleased]\n\n...\n\n\n" 86 | "## [" tag "] - " date)) 87 | (str/replace #"\[Unreleased\]: (\S+/compare)/(\S+)\.\.\.HEAD" 88 | (str "[Unreleased]: $1/" tag "...HEAD\n" 89 | "[" tag "]: $1/$2..." tag)) 90 | (->> (spit file))))) 91 | 92 | 93 | (defn- fail-if-dirty! 94 | "Exit with an error if the local repository has uncommitted changes." 95 | [] 96 | (let [status (b/git-process {:git-args "status --porcelain --untracked-files=no"})] 97 | (when-not (str/blank? status) 98 | (binding [*out* *err*] 99 | (println "Uncommitted changes in local repository, aborting") 100 | (System/exit 2))))) 101 | 102 | 103 | (defn prep-release 104 | "Prepare the repository for release." 105 | [opts] 106 | (fail-if-dirty!) 107 | (let [version (version-info opts true) 108 | tag (:tag version)] 109 | (update-changelog version) 110 | (b/git-process {:git-args ["commit" "-am" (str "Prepare release " tag)]}) 111 | (b/git-process {:git-args ["tag" tag "-s" "-m" (str "Release " tag)]}) 112 | (println "Prepared release for" tag) 113 | (assoc opts :version version))) 114 | 115 | 116 | ;; ## Library Installation 117 | 118 | (defn- pom-template 119 | "Generate template data for the Maven pom.xml file." 120 | [version-tag] 121 | [[:description "Clojure(Script) implementations of the self-describing multiformat specs."] 122 | [:url "https://github.com/greglook/clj-multiformats"] 123 | [:licenses 124 | [:license 125 | [:name "Public Domain"] 126 | [:url "https://unlicense.org/"]]] 127 | [:scm 128 | [:url "https://github.com/greglook/clj-multiformats"] 129 | [:connection "scm:git:https://github.com/greglook/clj-multiformats.git"] 130 | [:developerConnection "scm:git:ssh://git@github.com/greglook/clj-multiformats.git"] 131 | [:tag version-tag]]]) 132 | 133 | 134 | (defn pom 135 | "Write out a pom.xml file for the project." 136 | [opts] 137 | (let [version (version-info opts) 138 | pom-file (b/pom-path 139 | {:class-dir class-dir 140 | :lib lib-name})] 141 | (b/write-pom 142 | {:basis basis 143 | :lib lib-name 144 | :version (:tag version) 145 | :src-dirs [src-dir] 146 | :class-dir class-dir 147 | :pom-data (pom-template 148 | (if (or (:snapshot opts) (:qualifier opts)) 149 | (:commit version) 150 | (:tag version)))}) 151 | (assoc opts 152 | :version version 153 | :pom-file pom-file))) 154 | 155 | 156 | (defn jar 157 | "Build a JAR file for distribution." 158 | [opts] 159 | (let [opts (pom opts) 160 | version (:version opts) 161 | jar-file (format "target/%s-%s.jar" 162 | (name lib-name) 163 | (:tag version))] 164 | (b/copy-dir 165 | {:src-dirs [src-dir] 166 | :target-dir class-dir}) 167 | (b/jar 168 | {:class-dir class-dir 169 | :jar-file jar-file}) 170 | (assoc opts :jar-file jar-file))) 171 | 172 | 173 | (defn install 174 | "Install a JAR into the local Maven repository." 175 | [opts] 176 | (let [opts (-> opts clean jar) 177 | version (:version opts)] 178 | (b/install 179 | {:basis basis 180 | :lib lib-name 181 | :version (:tag version) 182 | :jar-file (:jar-file opts) 183 | :class-dir class-dir}) 184 | (println "Installed version" (:tag version) "to local repository") 185 | opts)) 186 | 187 | 188 | ;; ## Clojars Deployment 189 | 190 | (defn deploy 191 | "Publish the library to Clojars." 192 | [opts] 193 | (fail-if-dirty!) 194 | (let [opts (-> opts clean jar) 195 | version (:version opts) 196 | signing-key-id (System/getenv "CLOJARS_SIGNING_KEY") 197 | proceed? (or (:force opts) 198 | (and 199 | (or signing-key-id 200 | (do 201 | (print "No signing key specified - proceed without signature? [yN] ") 202 | (flush) 203 | (= "y" (str/lower-case (read-line))))) 204 | (do 205 | (printf "About to deploy version %s to Clojars - proceed? [yN] " 206 | (:tag version)) 207 | (flush) 208 | (= "y" (str/lower-case (read-line))))))] 209 | (if proceed? 210 | (d/deploy 211 | (-> opts 212 | (assoc :installer :remote 213 | :pom-file (:pom-file opts) 214 | :artifact (:jar-file opts)) 215 | (cond-> 216 | signing-key-id 217 | (assoc :sign-releases? true 218 | :sign-key-id signing-key-id)))) 219 | (binding [*out* *err*] 220 | (println "Aborting deploy") 221 | (System/exit 1))) 222 | opts)) 223 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps 4 | {mvxcvi/alphabase {:mvn/version "3.0.185"}} 5 | 6 | :aliases 7 | {:build 8 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 9 | io.github.clojure/tools.build {:mvn/version "0.10.7"} 10 | io.github.slipset/deps-deploy {:git/sha "07022b92d768590ab25b9ceb619ef17d2922da9a"} 11 | ;; Override deps-deploy RELEASE dependency to avoid https://github.com/qos-ch/slf4j/issues/422 12 | org.slf4j/slf4j-nop {:mvn/version "2.0.16"}} 13 | :ns-default build} 14 | 15 | :dev 16 | {:extra-paths ["dev" "test"] 17 | :extra-deps {org.clojure/tools.namespace {:mvn/version "1.5.0"} 18 | com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.6.1"} 19 | criterium/criterium {:mvn/version "0.4.6"}} 20 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow" 21 | "-XX:+UnlockDiagnosticVMOptions" 22 | "-XX:+DebugNonSafepoints" 23 | "-Djdk.attach.allowAttachSelf"]} 24 | 25 | :repl 26 | {:extra-deps {mvxcvi/puget {:mvn/version "1.3.4"}} 27 | :main-opts ["-e" "(require,'puget.printer)" 28 | "-e" "(require,'clojure.stacktrace)" 29 | "-e" "(clojure.main/repl,:init,#(do,(require,'multiformats.repl),(in-ns,'multiformats.repl)),:print,puget.printer/cprint,:caught,clojure.stacktrace/print-cause-trace)"]} 30 | 31 | :check 32 | {:extra-deps {io.github.athos/clj-check {:git/sha "d997df866b2a04b7ce7b17533093ee0a2e2cb729"}} 33 | :main-opts ["-m" "clj-check.check"]} 34 | 35 | :test 36 | {:extra-paths ["test"] 37 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} 38 | com.lambdaisland/kaocha-cljs {:mvn/version "1.5.154"}} 39 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 40 | :main-opts ["-m" "kaocha.runner"]} 41 | 42 | :coverage 43 | {:extra-paths ["test"] 44 | :extra-deps {cloverage/cloverage {:mvn/version "1.2.4"}} 45 | :main-opts ["-m" "cloverage.coverage" 46 | "--src-ns-path" "src" 47 | "--test-ns-path" "test"]} 48 | 49 | :hiera 50 | {:deps {io.github.greglook/clj-hiera {:git/tag "2.0.0", :git/sha "b14e514"}} 51 | :exec-fn hiera.main/graph 52 | :exec-args {:cluster-depth 2}}}} 53 | -------------------------------------------------------------------------------- /dev/multiformats/repl.clj: -------------------------------------------------------------------------------- 1 | (ns multiformats.repl 2 | (:require 3 | [alphabase.bytes :as b] 4 | [clj-async-profiler.core :as prof] 5 | [clojure.java.io :as io] 6 | [clojure.repl :refer :all] 7 | [clojure.stacktrace :refer [print-cause-trace]] 8 | [clojure.string :as str] 9 | [clojure.tools.namespace.repl :refer [refresh]] 10 | [criterium.core :as crit] 11 | [multiformats.address :as maddr] 12 | [multiformats.base :as mbase] 13 | [multiformats.cid :as cid] 14 | [multiformats.codec :as mcodec] 15 | [multiformats.hash :as mhash] 16 | [multiformats.varint :as varint])) 17 | -------------------------------------------------------------------------------- /src/multiformats/address.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.address 2 | "Multiaddr aims to make network addresses future-proof, composable, and 3 | efficient. 4 | 5 | An `Address` behaves like a sequence of `[protocol-key value]` pairs which 6 | you can `seq` through. This isn't an associative structure on protocol keys 7 | since you can have duplicate pairs and the order of the (key, val) pairs 8 | matters (e.g, for cases like tunneling). 9 | 10 | The binary form of an address entry consists of one to three values: 11 | - A varint encoding the numeric protocol code 12 | - An optional varint with the number of bytes in a variable-length value (e.g. a UTF-8 string) 13 | - The bytes encoding the protocol value, if any 14 | 15 | See: https://github.com/multiformats/multiaddr" 16 | (:require 17 | [alphabase.bytes :as b] 18 | [clojure.string :as str] 19 | #?(:cljs [goog.net.ipaddress :as ipaddress]) 20 | [multiformats.varint :as varint]) 21 | #?(:clj 22 | (:import 23 | (clojure.lang 24 | Counted 25 | IMeta 26 | IObj 27 | IPersistentCollection 28 | IPersistentStack 29 | Indexed 30 | Seqable 31 | Sequential) 32 | java.io.Serializable 33 | (java.net 34 | Inet4Address 35 | Inet6Address 36 | InetAddress)))) 37 | 38 | 39 | ;; ## Protocol Definitions 40 | 41 | (def protocols 42 | "Map from address protocol keywords to a map of information about it. 43 | 44 | https://github.com/multiformats/multiaddr/blob/master/protocols.csv" 45 | (into {} 46 | (map (fn prep-protocol 47 | [proto] 48 | [(first proto) (zipmap [:key :code :type :desc] proto)])) 49 | [[:ip4 4 :ip4] 50 | [:tcp 6 :ushort] 51 | [:udp 273 :ushort] 52 | [:dccp 33 :ushort] 53 | [:ip6 41 :ip6] 54 | [:ip6zone 42 :utf8 "rfc4007 IPv6 zone"] 55 | [:dns 53 :utf8 "domain name resolvable to both IPv6 and IPv4 addresses"] 56 | [:dns4 54 :utf8 "domain name resolvable only to IPv4 addresses"] 57 | [:dns6 55 :utf8 "domain name resolvable only to IPv6 addresses"] 58 | [:dnsaddr 56 :utf8] 59 | [:sctp 132 :ushort] 60 | [:udt 301 :null] 61 | [:utp 302 :null] 62 | [:unix 400 :utf8] 63 | [:p2p 421 :utf8 "preferred over /ipfs"] 64 | [:ipfs 421 :utf8 "backwards compatibility; equivalent to /p2p"] 65 | [:garlic64 446 :utf8] 66 | [:garlic32 447 :utf8] 67 | [:tls 448 :null "Transport Layer Security"] 68 | [:sni 449 :utf8 "Server Name Indication RFC 6066 § 3"] 69 | [:noise 454 :null] 70 | [:quic 460 :null] 71 | [:quic-v1 461 :null] 72 | [:webtransport 465 :null] 73 | [:certhash 466 :utf8] 74 | [:http 480 :null "HyperText Transfer Protocol"] 75 | [:http-path 481 :utf8 "Percent-encoded path to an HTTP resource"] 76 | [:https 443 :null "Deprecated alias for /tls/http"] 77 | [:ws 477 :null "WebSockets"] 78 | [:wss 478 :null "Deprecated alias for /tls/ws"] 79 | [:webrtc-direct 280 :null "ICE-lite webrtc transport with SDP munging during connection establishment and without use of a STUN server"] 80 | [:webrtc 281 :null "webrtc transport where connection establishment is according to w3c spec"] 81 | [:p2p-circuit 290 :null] 82 | [:memory 777 :utf8 "in memory transport for self-dialing and testing; arbitrary"]])) 83 | 84 | 85 | (def ^:private code->protocol 86 | "Mapping from code point to protocol keyword." 87 | (into {} 88 | (map (juxt :code :key)) 89 | (vals protocols))) 90 | 91 | 92 | ;; ## String Functions 93 | 94 | (defn- format-entry 95 | "Format an address protocol entry into a segment in a human-readable string." 96 | [[proto-key value]] 97 | (if (some? value) 98 | (str "/" (name proto-key) "/" value) 99 | (str "/" (name proto-key)))) 100 | 101 | 102 | (defn- format-entries 103 | "Format an entire address string from a sequence of protocol entries." 104 | [entries] 105 | (str/join (map format-entry entries))) 106 | 107 | 108 | (defn- parse-entries 109 | "Parse a formatted string into a sequence of protocol+value entries." 110 | [string] 111 | (when-not (str/starts-with? string "/") 112 | (throw (ex-info "Expected address string to begin with a slash" 113 | {:address string}))) 114 | (loop [parts (rest (str/split string #"/")) 115 | entries []] 116 | (if (seq parts) 117 | ;; Parse next protocol entry 118 | (let [proto-str (first parts)] 119 | (when-not (re-matches #"[a-z][a-z0-9-]+" proto-str) 120 | (throw (ex-info (str "Invalid protocol type string: " proto-str) 121 | {:address string 122 | :protocol proto-str}))) 123 | (let [protocol (get protocols (keyword proto-str))] 124 | (when-not protocol 125 | (throw (ex-info (str "Unknown protocol type: " proto-str) 126 | {:address string 127 | :protocol proto-str}))) 128 | (if (= :null (:type protocol)) 129 | (recur (next parts) 130 | (conj entries (:key protocol))) 131 | (let [value (second parts)] 132 | (when-not value 133 | (throw (ex-info (str "Missing value for " proto-str " protocol") 134 | {:address string 135 | :protocol (:key protocol)}))) 136 | (recur (nnext parts) 137 | (conj entries [(:key protocol) value])))))) 138 | ;; No more parts to parse 139 | entries))) 140 | 141 | 142 | ;; ## Binary Encoding 143 | 144 | (defn- encode-utf8-entry 145 | "Encode a protocol entry for a UTF-8 string value to a sequence of byte 146 | arrays." 147 | [protocol value] 148 | (let [proto-key (:key protocol) 149 | str-bytes (if (string? value) 150 | (b/from-string value) 151 | (throw (ex-info (str "Protocol " (name proto-key) 152 | " requires a UTF-8 string value, got: " 153 | (pr-str value)) 154 | {:protocol proto-key 155 | :value value})))] 156 | [(varint/encode (:code protocol)) 157 | (varint/encode (alength ^bytes str-bytes)) 158 | str-bytes])) 159 | 160 | 161 | (defn- encode-short-entry 162 | "Encode a protocol entry for an unsigned short value to a sequence of byte 163 | arrays." 164 | [protocol value] 165 | (let [proto-key (:key protocol) 166 | n (cond 167 | (integer? value) 168 | value 169 | 170 | (string? value) 171 | (or (parse-long value) 172 | (throw (ex-info (str "Protocol " (name proto-key) 173 | " has invalid string value: " value) 174 | {:protocol proto-key 175 | :value value}))) 176 | 177 | :else 178 | (throw (ex-info (str "Protocol " (name proto-key) 179 | " requires an unsigned short value or numeric string, got: " 180 | (pr-str value)) 181 | {:protocol proto-key 182 | :value value})))] 183 | (when (< 65535 n) 184 | (throw (ex-info (str "Protocol " (name proto-key) 185 | " has value too big for unsigned short: " n) 186 | {:protocol proto-key 187 | :value n}))) 188 | [(varint/encode (:code protocol)) 189 | (b/init-bytes 190 | [(bit-and (bit-shift-right n 8) 0xFF) 191 | (bit-and n 0xFF)])])) 192 | 193 | 194 | (defn- encode-ip4-entry 195 | "Encode a protocol entry for an IPv4 address value to a sequence of byte 196 | arrays." 197 | [protocol value] 198 | (let [proto-key (:key protocol) 199 | addr-bytes (cond 200 | (string? value) 201 | #?(:clj 202 | (try 203 | (let [addr (InetAddress/getByName value)] 204 | (when-not (instance? Inet4Address addr) 205 | (throw (ex-info (str "Invalid IPv4 address string: " value) 206 | {:protocol proto-key 207 | :value value}))) 208 | (.getAddress addr)) 209 | (catch Exception ex 210 | (throw (ex-info (str "Invalid IPv4 address string: " value) 211 | {:protocol proto-key 212 | :value value} 213 | ex)))) 214 | 215 | :cljs 216 | (let [addr (ipaddress/IpAddress.fromString value)] 217 | (when (or (nil? addr) (not= 4 (.getVersion addr))) 218 | (throw (ex-info (str "Invalid IPv4 address string: " value) 219 | {:protocol proto-key 220 | :value value}))) 221 | (let [bs (b/byte-array 4) 222 | addr-ints (.toInteger addr) 223 | n (.getBitsUnsigned addr-ints 0)] 224 | ;; goog.math.Integer int32 pieces are little endian 225 | ;; so need to work backwards for network order. 226 | (b/set-byte bs 0 (bit-and (bit-shift-right n 24) 0xFF)) 227 | (b/set-byte bs 1 (bit-and (bit-shift-right n 16) 0xFF)) 228 | (b/set-byte bs 2 (bit-and (bit-shift-right n 8) 0xFF)) 229 | (b/set-byte bs 3 (bit-and n 0xFF)) 230 | bs))) 231 | 232 | ;; TODO: could accept "real" IP value types here 233 | 234 | :else 235 | (throw (ex-info (str "Protocol " (name proto-key) 236 | " requires an IP address string, got: " 237 | (pr-str value)) 238 | {:protocol proto-key 239 | :value value})))] 240 | [(varint/encode (:code protocol)) 241 | addr-bytes])) 242 | 243 | 244 | (defn- encode-ip6-entry 245 | "Encode a protocol entry for an IPv6 address value to a sequence of byte 246 | arrays." 247 | [protocol value] 248 | (let [proto-key (:key protocol) 249 | addr-bytes (cond 250 | (string? value) 251 | #?(:clj 252 | (try 253 | (let [addr (InetAddress/getByName value)] 254 | (when-not (instance? Inet6Address addr) 255 | (throw (ex-info (str "Invalid IPv6 address string: " value) 256 | {:protocol proto-key 257 | :value value}))) 258 | (.getAddress addr)) 259 | (catch Exception ex 260 | (throw (ex-info (str "Invalid IPv6 address string: " value) 261 | {:protocol proto-key 262 | :value value} 263 | ex)))) 264 | 265 | :cljs 266 | (let [addr (ipaddress/IpAddress.fromString value)] 267 | (when (or (nil? addr) (not= 6 (.getVersion addr))) 268 | (throw (ex-info (str "Invalid IPv6 address string: " value) 269 | {:protocol proto-key 270 | :value value}))) 271 | (let [bs (b/byte-array 16) 272 | addr-ints (.toInteger addr)] 273 | ;; goog.math.Integer int32 pieces are little endian 274 | ;; so need to work backwards for network order. 275 | (dotimes [idx 4] 276 | (let [n (.getBitsUnsigned addr-ints (- 3 idx)) 277 | offset (* 4 idx)] 278 | (b/set-byte bs (+ offset 0) (bit-and (bit-shift-right n 24) 0xFF)) 279 | (b/set-byte bs (+ offset 1) (bit-and (bit-shift-right n 16) 0xFF)) 280 | (b/set-byte bs (+ offset 2) (bit-and (bit-shift-right n 8) 0xFF)) 281 | (b/set-byte bs (+ offset 3) (bit-and n 0xFF)))) 282 | bs))) 283 | 284 | ;; TODO: could accept "real" IP value types here 285 | 286 | :else 287 | (throw (ex-info (str "Protocol " (name proto-key) 288 | " requires an IP address string, got: " 289 | (pr-str value)) 290 | {:protocol proto-key 291 | :value value})))] 292 | [(varint/encode (:code protocol)) 293 | addr-bytes])) 294 | 295 | 296 | (defn- encode-entry 297 | "Encode a protocol entry to a sequence of byte arrays that represent the 298 | entry when concatenated together." 299 | [entry] 300 | (when-not (or (keyword? entry) 301 | (and (vector? entry) 302 | (keyword? (first entry)) 303 | (or (= 1 (count entry)) 304 | (= 2 (count entry))))) 305 | (throw (ex-info "Address entry must be a protocol keyword or vector pair" 306 | {:entry entry}))) 307 | (let [[proto-key value] (if (keyword? entry) 308 | [entry nil] 309 | entry) 310 | protocol (get protocols proto-key)] 311 | (when-not protocol 312 | (throw (ex-info (str "Unsupported protocol type: " (name proto-key)) 313 | {:protocol proto-key}))) 314 | (case (:type protocol) 315 | :null 316 | (if (nil? value) 317 | [(varint/encode (:code protocol))] 318 | (throw (ex-info (str "Protocol " (name proto-key) " does not support values, got: " (pr-str value)) 319 | {:protocol proto-key 320 | :value value}))) 321 | 322 | :utf8 323 | (encode-utf8-entry protocol value) 324 | 325 | :ushort 326 | (encode-short-entry protocol value) 327 | 328 | :ip4 329 | (encode-ip4-entry protocol value) 330 | 331 | :ip6 332 | (encode-ip6-entry protocol value)))) 333 | 334 | 335 | ;; ## Binary Decoding 336 | 337 | (defn- decode-utf8-entry 338 | "Decode a protocol entry for a UTF-8 string value from the offset into the 339 | byte array. Returns a vector with the value and the number of bytes read." 340 | [^bytes data offset] 341 | (let [[str-len len-len] (varint/read-bytes data offset) 342 | string #?(:clj 343 | (String. data (int (+ offset len-len)) (int str-len) "UTF-8") 344 | :cljs 345 | (b/to-string (b/copy-slice data (+ offset len-len) str-len)))] 346 | [string (+ len-len str-len)])) 347 | 348 | 349 | (defn- decode-short-entry 350 | "Decode a protocol entry for an unsigned short value from the offset into the 351 | byte array. Returns a vector with the value and the number of bytes read." 352 | [^bytes data offset] 353 | (let [n (bit-or (bit-shift-left (b/get-byte data offset) 8) 354 | (b/get-byte data (inc offset)))] 355 | [n 2])) 356 | 357 | 358 | (defn- decode-ip4-entry 359 | "Decode a protocol entry for an IPv4 address value from the offset into the 360 | byte array. Returns a vector with the value and the number of bytes read." 361 | [^bytes data offset] 362 | (let [addr #?(:clj 363 | (-> 364 | (b/copy-slice data offset 4) 365 | (InetAddress/getByAddress) 366 | (.getHostAddress)) 367 | ;; goog.net.IpAddress doesn't support an easy way 368 | ;; to build IP from bytes, so manually construct string 369 | :cljs 370 | (str (b/get-byte data offset) "." 371 | (b/get-byte data (+ offset 1)) "." 372 | (b/get-byte data (+ offset 2)) "." 373 | (b/get-byte data (+ offset 3))))] 374 | [addr 4])) 375 | 376 | 377 | (defn- decode-ip6-entry 378 | "Decode a protocol entry for an IPv6 address value from the offset into the 379 | byte array. Returns a vector with the value and the number of bytes read." 380 | [^bytes data offset] 381 | (let [addr #?(:clj 382 | (-> 383 | (b/copy-slice data offset 16) 384 | (InetAddress/getByAddress) 385 | (.getHostAddress)) 386 | ;; goog.net.IpAddress doesn't support an easy way 387 | ;; to build IP from bytes, so manually construct string 388 | :cljs 389 | (->> 390 | (range 8) 391 | (map (fn extract-hextet 392 | [i] 393 | (let [hex-off (+ offset (* 2 i)) 394 | n (bit-or 395 | (bit-shift-left (b/get-byte data hex-off) 8) 396 | (b/get-byte data (inc hex-off)))] 397 | (.toString n 16)))) 398 | (str/join ":")))] 399 | [addr 16])) 400 | 401 | 402 | (defn- decode-entry 403 | "Decode a protocol entry from the offset into the byte array. Returns a 404 | vector with the protocol entry and its length in bytes." 405 | [^bytes data offset] 406 | (let [[code code-len] (varint/read-bytes data offset) 407 | proto-key (code->protocol code) 408 | protocol (get protocols proto-key)] 409 | (when-not protocol 410 | (throw (ex-info (str "Unsupported protocol code: " code) 411 | {:code code 412 | :offset offset}))) 413 | (case (:type protocol) 414 | :null 415 | [[proto-key] code-len] 416 | 417 | :utf8 418 | (let [[string val-len] (decode-utf8-entry data (+ offset code-len))] 419 | [[proto-key string] (+ code-len val-len)]) 420 | 421 | :ushort 422 | (let [[n val-len] (decode-short-entry data (+ offset code-len))] 423 | [[proto-key n] (+ code-len val-len)]) 424 | 425 | :ip4 426 | (let [[addr val-len] (decode-ip4-entry data (+ offset code-len))] 427 | [[proto-key addr] (+ code-len val-len)]) 428 | 429 | :ip6 430 | (let [[addr val-len] (decode-ip6-entry data (+ offset code-len))] 431 | [[proto-key addr] (+ code-len val-len)])))) 432 | 433 | 434 | (defn- decode-entries 435 | "Decode a sequence of entries from the byte array." 436 | [^bytes data] 437 | (loop [offset 0 438 | entries []] 439 | (if (< offset (alength data)) 440 | (let [[entry entry-len] (decode-entry data offset)] 441 | (recur (long (+ offset entry-len)) (conj entries entry))) 442 | entries))) 443 | 444 | 445 | ;; ## Address Type 446 | 447 | #?(:clj 448 | (deftype Address 449 | [^bytes _bytes 450 | _points 451 | _meta 452 | ^:unsynchronized-mutable _hash] 453 | 454 | Serializable 455 | 456 | 457 | Object 458 | 459 | (toString 460 | [_] 461 | (format-entries (decode-entries _bytes))) 462 | 463 | 464 | (equals 465 | [this that] 466 | (cond 467 | (identical? this that) 468 | true 469 | 470 | (instance? Address that) 471 | (b/bytes= _bytes (._bytes ^Address that)) 472 | 473 | :else 474 | false)) 475 | 476 | 477 | (hashCode 478 | [_] 479 | (if (zero? _hash) 480 | (let [hc (hash (b/byte-seq _bytes))] 481 | (set! _hash hc) 482 | hc) 483 | _hash)) 484 | 485 | 486 | Comparable 487 | 488 | (compareTo 489 | [this that] 490 | (cond 491 | (identical? this that) 492 | 0 493 | 494 | (instance? Address that) 495 | (b/compare _bytes (.-_bytes ^Address that)) 496 | 497 | :else 498 | (throw (ex-info 499 | (str "Cannot compare multiaddress value to " (type that)) 500 | {:this this 501 | :that that})))) 502 | 503 | 504 | IMeta 505 | 506 | (meta 507 | [_] 508 | _meta) 509 | 510 | 511 | IObj 512 | 513 | (withMeta 514 | [_ new-meta] 515 | (Address. _bytes _points new-meta _hash)) 516 | 517 | 518 | Counted 519 | 520 | (count 521 | [_] 522 | (count _points)) 523 | 524 | 525 | Sequential 526 | 527 | 528 | Seqable 529 | 530 | (seq 531 | [_] 532 | (seq (decode-entries _bytes))) 533 | 534 | 535 | Indexed 536 | 537 | (nth 538 | [_ i] 539 | (if (<= 0 i (dec (count _points))) 540 | (let [offset (nth _points i)] 541 | (first (decode-entry _bytes offset))) 542 | (throw (IndexOutOfBoundsException. 543 | (str "Index " i " is outside the " 544 | (count _points) " elements in the address"))))) 545 | 546 | 547 | (nth 548 | [_ i not-found] 549 | (if (<= 0 i (dec (count _points))) 550 | (let [offset (nth _points i)] 551 | (first (decode-entry _bytes offset))) 552 | not-found)) 553 | 554 | 555 | IPersistentCollection 556 | 557 | (empty 558 | [_] 559 | (Address. (b/byte-array 0) [] _meta 0)) 560 | 561 | 562 | (equiv 563 | [this that] 564 | (.equals this that)) 565 | 566 | 567 | (cons 568 | [_ entry] 569 | (let [entry-arrs (encode-entry entry) 570 | new-bytes (apply b/concat (cons _bytes entry-arrs)) 571 | new-points (conj _points (alength _bytes))] 572 | (Address. new-bytes new-points _meta 0))) 573 | 574 | 575 | IPersistentStack 576 | 577 | (peek 578 | [_] 579 | (when (seq _points) 580 | (let [offset (peek _points)] 581 | (first (decode-entry _bytes offset))))) 582 | 583 | 584 | (pop 585 | [_] 586 | (when (empty? _points) 587 | (throw (ex-info "Can't pop empty address" {}))) 588 | (let [offset (peek _points) 589 | new-bytes (b/copy-slice _bytes 0 offset) 590 | new-points (pop _points)] 591 | (Address. new-bytes new-points _meta 0)))) 592 | 593 | :cljs 594 | (deftype Address 595 | [_bytes 596 | _points 597 | _meta 598 | ^:unsynchronized-mutable _hash] 599 | 600 | Object 601 | 602 | (toString 603 | [_] 604 | (format-entries (decode-entries _bytes))) 605 | 606 | 607 | IEquiv 608 | 609 | (-equiv 610 | [this that] 611 | (cond 612 | (identical? this that) 613 | true 614 | 615 | (instance? Address that) 616 | (b/bytes= _bytes (.-_bytes ^Address that)) 617 | 618 | :else 619 | false)) 620 | 621 | 622 | IHash 623 | 624 | (-hash 625 | [_] 626 | (if (zero? _hash) 627 | (let [hc (hash (b/byte-seq _bytes))] 628 | (set! _hash hc) 629 | hc) 630 | _hash)) 631 | 632 | 633 | IComparable 634 | 635 | (-compare 636 | [this that] 637 | (cond 638 | (identical? this that) 639 | 0 640 | 641 | (instance? Address that) 642 | (b/compare _bytes (.-_bytes ^Address that)) 643 | 644 | :else 645 | (throw (ex-info 646 | (str "Cannot compare multiaddress value to " (type that)) 647 | {:this this 648 | :that that})))) 649 | 650 | 651 | IMeta 652 | 653 | (-meta 654 | [_] 655 | _meta) 656 | 657 | 658 | IWithMeta 659 | 660 | (-with-meta 661 | [_ new-meta] 662 | (Address. _bytes _points new-meta _hash)) 663 | 664 | 665 | ICounted 666 | 667 | (-count 668 | [_] 669 | (count _points)) 670 | 671 | 672 | ISequential 673 | 674 | 675 | ISeqable 676 | 677 | (-seq 678 | [_] 679 | (seq (decode-entries _bytes))) 680 | 681 | 682 | IIndexed 683 | 684 | (-nth 685 | [_ i] 686 | (if (<= 0 i (dec (count _points))) 687 | (let [offset (nth _points i)] 688 | (first (decode-entry _bytes offset))) 689 | (throw (ex-info 690 | (str "Index " i " is outside the " 691 | (count _points) " elements in the address") 692 | {:i i})))) 693 | 694 | 695 | (-nth 696 | [_ i not-found] 697 | (if (<= 0 i (dec (count _points))) 698 | (let [offset (nth _points i)] 699 | (first (decode-entry _bytes offset))) 700 | not-found)) 701 | 702 | 703 | IEmptyableCollection 704 | 705 | (-empty 706 | [_] 707 | (Address. (b/byte-array 0) [] _meta 0)) 708 | 709 | 710 | ICollection 711 | 712 | (-conj 713 | [_ entry] 714 | (let [entry-arrs (encode-entry entry) 715 | new-bytes (apply b/concat (cons _bytes entry-arrs)) 716 | new-points (conj _points (alength _bytes))] 717 | (Address. new-bytes new-points _meta 0))) 718 | 719 | 720 | IStack 721 | 722 | (-peek 723 | [_] 724 | (when (seq _points) 725 | (let [offset (peek _points)] 726 | (first (decode-entry _bytes offset))))) 727 | 728 | 729 | (-pop 730 | [_] 731 | (when (empty? _points) 732 | (throw (ex-info "Can't pop empty address" {}))) 733 | (let [offset (peek _points) 734 | new-bytes (b/copy-slice _bytes 0 offset) 735 | new-points (pop _points)] 736 | (Address. new-bytes new-points _meta 0))))) 737 | 738 | 739 | (alter-meta! #'->Address assoc :private true) 740 | 741 | 742 | ;; ## Constructors 743 | 744 | (defn create 745 | "Constructs a new multiaddr from a sequence of protocol/value pairs. Each 746 | entry should be a vector with a protocol keyword and a value; alternatively, 747 | protocols with no value may be specified as a simple keyword. The values 748 | should be either strings or appropriate types, such as numbers or IP 749 | addresses. 750 | 751 | (create [[:ip4 \"127.0.0.1\"] [:tcp 80] :tls])" 752 | ([] 753 | (create nil)) 754 | ([entries] 755 | (let [entry-bytes (mapv encode-entry entries) 756 | points (loop [offset 0 757 | points [] 758 | entry-bytes entry-bytes] 759 | (if (seq entry-bytes) 760 | (let [next-len (apply + (map alength (first entry-bytes)))] 761 | (recur (long (+ offset next-len)) 762 | (conj points offset) 763 | (next entry-bytes))) 764 | points)) 765 | addr-bytes (apply b/concat (apply concat entry-bytes))] 766 | (->Address addr-bytes points nil 0)))) 767 | 768 | 769 | (defn address? 770 | "True if the value is a multiaddress object." 771 | [x] 772 | (instance? Address x)) 773 | 774 | 775 | ;; ## Serialization 776 | 777 | (defn encode 778 | "Encode a multiaddr into a binary representation. Returns the byte array." 779 | ^bytes 780 | [^Address addr] 781 | (b/copy (.-_bytes addr))) 782 | 783 | 784 | (defn decode 785 | "Decode a multiaddr by reading data from a byte array." 786 | ^Address 787 | [^bytes data] 788 | (when (seq data) 789 | (loop [offset 0 790 | points []] 791 | (if (< offset (alength data)) 792 | (let [[_ entry-len] (decode-entry data offset)] 793 | (recur (long (+ offset entry-len)) 794 | (conj points offset))) 795 | (->Address (b/copy data) points nil 0))))) 796 | 797 | 798 | (defn parse 799 | "Parse a string representation into a multiaddr. 800 | 801 | (parse \"/ip4/127.0.0.1/tcp/80/tls\")" 802 | ^Address 803 | [string] 804 | (create (parse-entries string))) 805 | -------------------------------------------------------------------------------- /src/multiformats/base.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.base 2 | "Multibase is a protocol for distinguishing base encodings and other simple 3 | string encodings, and for ensuring full compatibility with program 4 | interfaces. 5 | 6 | https://github.com/multiformats/multibase" 7 | (:refer-clojure :exclude [bases format]) 8 | (:require 9 | [alphabase.base16 :as b16] 10 | [alphabase.base2 :as b2] 11 | [alphabase.base32 :as b32] 12 | [alphabase.base58 :as b58] 13 | [alphabase.base64 :as b64] 14 | [alphabase.base8 :as b8] 15 | [clojure.string :as str])) 16 | 17 | 18 | (def codes 19 | "Map of base keys to multicodec packed symbols from the standard table." 20 | {;; Numeric 21 | :base1 \1 22 | :base2 \0 23 | :base8 \7 24 | :base10 \9 25 | ;; Hexadecimal (RFC 4648) 26 | :base16 \f 27 | :BASE16 \F 28 | ;; Base32 (RFC 4648) 29 | :base32 \b 30 | :BASE32 \B 31 | :base32pad \c 32 | :BASE32PAD \C 33 | :base32hex \v 34 | :BASE32HEX \V 35 | :base32hexpad \t 36 | :BASE32HEXPAD \T 37 | ;; Base58 38 | :base58btc \z 39 | :base58flickr \Z 40 | ;; Base64 (RFC 4648) 41 | :base64 \m 42 | :base64pad \M 43 | :base64url \u 44 | :base64urlpad \U}) 45 | 46 | 47 | ;; ## Base Encodings 48 | 49 | (def ^:private binary-encodings 50 | "Binary (b2) and Octal (b8) encodings." 51 | [{:key :base2 52 | :formatter b2/encode 53 | :parser b2/decode} 54 | {:key :base8 55 | :formatter b8/encode 56 | :parser b8/decode}]) 57 | 58 | 59 | (def ^:private hex-encodings 60 | "Hexadecimal (b16) encodings." 61 | [{:key :base16 62 | :formatter (comp str/lower-case b16/encode) 63 | :parser b16/decode} 64 | {:key :BASE16 65 | :formatter b16/encode 66 | :parser b16/decode}]) 67 | 68 | 69 | (def ^:private b32-encodings 70 | "Base32 encodings." 71 | (letfn [(parse-base32 72 | [string] 73 | (b32/decode string false)) 74 | 75 | (parse-base32hex 76 | [string] 77 | (b32/decode string true))] 78 | [{:key :base32 79 | :formatter (fn format-base32 80 | [data] 81 | (str/lower-case (b32/encode data false false))) 82 | :parser parse-base32} 83 | {:key :BASE32 84 | :formatter (fn format-BASE32 85 | [data] 86 | (b32/encode data false false)) 87 | :parser parse-base32} 88 | {:key :base32pad 89 | :formatter (fn format-base32pad 90 | [data] 91 | (str/lower-case (b32/encode data false true))) 92 | :parser parse-base32} 93 | {:key :BASE32PAD 94 | :formatter (fn format-BASE32PAD 95 | [data] 96 | (b32/encode data false true)) 97 | :parser parse-base32} 98 | {:key :base32hex 99 | :formatter (fn format-base32hex 100 | [data] 101 | (str/lower-case (b32/encode data true false))) 102 | :parser parse-base32hex} 103 | {:key :BASE32HEX 104 | :formatter (fn format-BASE32HEX 105 | [data] 106 | (b32/encode data true false)) 107 | :parser parse-base32hex} 108 | {:key :base32hexpad 109 | :formatter (fn format-base32hexpad 110 | [data] 111 | (str/lower-case (b32/encode data true true))) 112 | :parser parse-base32hex} 113 | {:key :BASE32HEXPAD 114 | :formatter (fn format-BASE32HEXPAD 115 | [data] 116 | (b32/encode data true true)) 117 | :parser parse-base32hex}])) 118 | 119 | 120 | (def ^:private b58-encodings 121 | "Base58 encodings." 122 | [{:key :base58btc 123 | :formatter b58/encode 124 | :parser b58/decode}]) 125 | 126 | 127 | (def ^:private b64-encodings 128 | "Base64 encodings." 129 | [{:key :base64 130 | :formatter (fn format-base64 131 | [data] 132 | (b64/encode data false false)) 133 | :parser b64/decode} 134 | {:key :base64pad 135 | :formatter (fn format-base64pad 136 | [data] 137 | (b64/encode data false true)) 138 | :parser b64/decode} 139 | {:key :base64url 140 | :formatter (fn format-base64url 141 | [data] 142 | (b64/encode data true false)) 143 | :parser b64/decode} 144 | {:key :base64urlpad 145 | :formatter (fn format-base64urlpad 146 | [data] 147 | (b64/encode data true true)) 148 | :parser b64/decode}]) 149 | 150 | 151 | (def bases 152 | "Map of base keys to definition maps." 153 | (into {} 154 | (comp 155 | cat 156 | (map (fn prep-base 157 | [params] 158 | (let [base-key (:key params) 159 | prefix (some-> (get codes base-key) str)] 160 | [base-key (assoc params :prefix prefix)])))) 161 | [binary-encodings 162 | hex-encodings 163 | b32-encodings 164 | b58-encodings 165 | b64-encodings])) 166 | 167 | 168 | (def ^:private prefix->base 169 | "Cached map of prefix characters to base keys." 170 | (into {} (map (juxt (comp :prefix val) key)) bases)) 171 | 172 | 173 | ;; ## Encoding 174 | 175 | (defn format* 176 | "Formats binary data into a string with the given base. Returns the formatted 177 | string without a prefix." 178 | ^String 179 | [base-key ^bytes data] 180 | (when-not (keyword? base-key) 181 | (throw (ex-info "base-key must be a keyword" {:base base-key}))) 182 | (when (zero? (alength data)) 183 | (throw (ex-info "Cannot format empty data as a multibase string" 184 | {:base base-key}))) 185 | (let [formatter (get-in bases [base-key :formatter])] 186 | (when-not formatter 187 | (throw (ex-info (str (name base-key) " does not have a supported multibase formatter") 188 | {:base base-key}))) 189 | (formatter data))) 190 | 191 | 192 | (defn format 193 | "Formats binary data into a string with the given base. Returns the formatted 194 | string, prefixed with the base constant." 195 | ^String 196 | [base-key ^bytes data] 197 | (let [prefix (get codes base-key)] 198 | (str prefix (format* base-key data)))) 199 | 200 | 201 | ;; ## Decoding 202 | 203 | (defn- get-prefix 204 | "Get the multibase prefix character from the given string. Throws an 205 | exception if the string is too short." 206 | [^String string] 207 | (when (< (count string) 2) 208 | (throw (ex-info (str "The string " (pr-str string) 209 | " is too short to be multibase-formatted data") 210 | {:data string}))) 211 | (str (first string))) 212 | 213 | 214 | (defn parse* 215 | "Parse a non-prefixed string into binary data using the given base. Returns 216 | the decoded byte array." 217 | ^bytes 218 | [base-key string] 219 | (when-not (keyword? base-key) 220 | (throw (ex-info "base-key must be a keyword" {:base base-key}))) 221 | (let [parser (get-in bases [base-key :parser])] 222 | (when-not parser 223 | (throw (ex-info (str (name base-key) " does not have a supported multibase parser") 224 | {:base base-key}))) 225 | (parser string))) 226 | 227 | 228 | (defn parse 229 | "Parses a multibase-prefixed string into binary data. Returns an array with 230 | the parsed bytes, or throws an error if there is no known base." 231 | ^bytes 232 | [^String string] 233 | (let [prefix (get-prefix string) 234 | base-key (prefix->base prefix)] 235 | (when-not base-key 236 | (throw (ex-info (str "The prefix " (pr-str prefix) 237 | " does not map to a supported multibase encoding") 238 | {:data string 239 | :prefix prefix}))) 240 | (parse* base-key (subs string 1)))) 241 | 242 | 243 | (defn inspect 244 | "Inspect a string and return a map of information including the prefix 245 | character, base key, and encoded data length." 246 | [^String string] 247 | (let [prefix (get-prefix string) 248 | base (prefix->base prefix)] 249 | {:base base 250 | :prefix prefix 251 | :length (dec (count string))})) 252 | -------------------------------------------------------------------------------- /src/multiformats/cid.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.cid 2 | "CID is a self-describing content-addressed identifier. It uses cryptographic 3 | hashing to identify content, multicodec packed codes to label the content 4 | type, and multibase to encode the final identifier into a string. 5 | 6 | https://github.com/ipld/cid" 7 | (:refer-clojure :exclude [format]) 8 | (:require 9 | [alphabase.base58 :as b58] 10 | [alphabase.bytes :as b] 11 | [clojure.string :as str] 12 | [multiformats.base :as mbase] 13 | [multiformats.codec :as mcodec] 14 | [multiformats.hash :as mhash] 15 | [multiformats.varint :as varint]) 16 | #?(:clj 17 | (:import 18 | (clojure.lang 19 | ILookup 20 | IMeta 21 | IObj) 22 | java.io.Serializable))) 23 | 24 | 25 | ;; ## Coding Functions 26 | 27 | (defn- read-header 28 | "Read the cid version and codec code from the encoded bytes. Returns a tuple of 29 | the numeric version, code, and number of bytes read." 30 | [^bytes data] 31 | (let [[version vsize] (varint/read-bytes data 0) 32 | [codec csize] (varint/read-bytes data vsize)] 33 | [version codec (+ vsize csize)])) 34 | 35 | 36 | (defn- decode-parameters 37 | "Read the header and multihash from the encoded bytes to build a map of cid 38 | parameters. Resolves the codec to a keyword if possible." 39 | [^bytes data] 40 | (let [[version codec offset] (read-header data) 41 | [mhash _] (mhash/read-bytes data offset)] 42 | {:version version 43 | :codec (mcodec/resolve-key codec) 44 | :hash mhash})) 45 | 46 | 47 | (defn- encode-bytes 48 | "Encode a cid version, codec, and multihash into a byte array." 49 | [version codec mhash] 50 | (let [header (b/byte-array 8) 51 | vlength (varint/write-bytes version header 0) 52 | clength (varint/write-bytes codec header vlength) 53 | hlength (+ vlength clength) 54 | buffer (b/byte-array (+ hlength (:length mhash)))] 55 | (b/copy header 0 buffer 0 hlength) 56 | (mhash/write-bytes mhash buffer hlength) 57 | buffer)) 58 | 59 | 60 | (defn- cid-str 61 | "Createa string representation of the CID parameters." 62 | [params] 63 | (let [codec (:codec params) 64 | codec-name (if (keyword? codec) 65 | (name codec) 66 | (or codec "?")) 67 | algorithm (get-in params [:hash :algorithm]) 68 | algo-name (if (keyword? algorithm) 69 | (name algorithm) 70 | (or algorithm "?"))] 71 | (str "cidv" (:version params) 72 | ":" codec-name ":" algo-name ":" 73 | (get-in params [:hash :digest] "?")))) 74 | 75 | 76 | ;; ## ContentID Type 77 | 78 | #?(:clj 79 | (deftype ContentID 80 | [^bytes _bytes 81 | _meta 82 | ^:unsynchronized-mutable _hash] 83 | 84 | Serializable 85 | 86 | 87 | Object 88 | 89 | (toString 90 | [_] 91 | (cid-str (decode-parameters _bytes))) 92 | 93 | 94 | (equals 95 | [this that] 96 | (cond 97 | (identical? this that) true 98 | 99 | (instance? ContentID that) 100 | (b/bytes= _bytes (._bytes ^ContentID that)) 101 | 102 | :else false)) 103 | 104 | 105 | (hashCode 106 | [_] 107 | (let [hc _hash] 108 | (if (zero? hc) 109 | (let [params (decode-parameters _bytes) 110 | hc (hash [::cid (:version params) (:codec params) (:hash params)])] 111 | (set! _hash hc) 112 | hc) 113 | hc))) 114 | 115 | 116 | Comparable 117 | 118 | (compareTo 119 | [this that] 120 | (cond 121 | (identical? this that) 0 122 | 123 | (instance? ContentID that) 124 | (b/compare _bytes (._bytes ^ContentID that)) 125 | 126 | :else 127 | (throw (ex-info 128 | (str "Cannot compare CID value to " (type that)) 129 | {:this this 130 | :that that})))) 131 | 132 | 133 | ILookup 134 | 135 | (valAt 136 | [this k] 137 | (.valAt this k nil)) 138 | 139 | 140 | (valAt 141 | [_ k not-found] 142 | (let [[version codec hlength] (read-header _bytes)] 143 | (case k 144 | :length (if (zero? version) 145 | (- (alength _bytes) hlength) 146 | (alength _bytes)) 147 | :version version 148 | :codec (mcodec/resolve-key codec) 149 | :code codec 150 | :hash (first (mhash/read-bytes _bytes hlength)) 151 | not-found))) 152 | 153 | 154 | IMeta 155 | 156 | (meta 157 | [_] 158 | _meta) 159 | 160 | 161 | IObj 162 | 163 | (withMeta 164 | [_ meta-map] 165 | (ContentID. _bytes meta-map _hash))) 166 | 167 | :cljs 168 | (deftype ContentID 169 | [_bytes 170 | _meta 171 | ^:unsynchronized-mutable _hash] 172 | 173 | Object 174 | 175 | (toString 176 | [_] 177 | (cid-str (decode-parameters _bytes))) 178 | 179 | 180 | IEquiv 181 | 182 | (-equiv 183 | [this that] 184 | (cond 185 | (identical? this that) true 186 | 187 | (instance? ContentID that) 188 | (b/bytes= _bytes (.-_bytes ^ContentID that)) 189 | 190 | :else false)) 191 | 192 | 193 | IHash 194 | 195 | (-hash 196 | [_] 197 | (let [hc _hash] 198 | (if (zero? hc) 199 | (let [params (decode-parameters _bytes) 200 | hc (hash [::cid (:version params) (:codec params) (:hash params)])] 201 | (set! _hash hc) 202 | hc) 203 | hc))) 204 | 205 | 206 | IComparable 207 | 208 | (-compare 209 | [this that] 210 | (cond 211 | (identical? this that) 0 212 | 213 | (instance? ContentID that) 214 | (b/compare _bytes (.-_bytes ^ContentID that)) 215 | 216 | :else 217 | (throw (ex-info 218 | (str "Cannot compare CID value to " (type that)) 219 | {:this this 220 | :that that})))) 221 | 222 | 223 | ILookup 224 | 225 | (-lookup 226 | [this k] 227 | (-lookup this k nil)) 228 | 229 | 230 | (-lookup 231 | [_ k not-found] 232 | (let [[version codec hlength] (read-header _bytes)] 233 | (case k 234 | :length (if (zero? version) 235 | (- (alength _bytes) hlength) 236 | (alength _bytes)) 237 | :version version 238 | :codec (mcodec/resolve-key codec) 239 | :code codec 240 | :hash (first (mhash/read-bytes _bytes hlength)) 241 | not-found))) 242 | 243 | 244 | IMeta 245 | 246 | (-meta 247 | [_] 248 | _meta) 249 | 250 | 251 | IWithMeta 252 | 253 | (-with-meta 254 | [_ meta-map] 255 | (ContentID. _bytes meta-map _hash)))) 256 | 257 | 258 | (alter-meta! #'->ContentID assoc :private true) 259 | 260 | 261 | ;; ## Construction 262 | 263 | (defn create 264 | "Construct a new v1 content identifier for a known codec type and multihash." 265 | [codec mhash] 266 | (when-not (instance? multiformats.hash.Multihash mhash) 267 | (throw (ex-info (str "Cannot construct CID with non-multihash value: " 268 | (pr-str mhash)) 269 | {:codec codec 270 | :hash mhash}))) 271 | (let [version 1 272 | code (mcodec/resolve-code codec) 273 | encoded (encode-bytes version code mhash)] 274 | (->ContentID encoded nil 0))) 275 | 276 | 277 | (defn cid? 278 | "True if the value is a ContentID object." 279 | [x] 280 | (instance? ContentID x)) 281 | 282 | 283 | ;; ## Binary Serialization 284 | 285 | (defn- inner-bytes 286 | "Retrieve the inner encoded bytes from a CID value." 287 | ^bytes 288 | [^ContentID cid] 289 | (#?(:clj ._bytes :cljs .-_bytes) cid)) 290 | 291 | 292 | (defn read-bytes 293 | "Read a content identifier from a byte array. Returns a tuple containing the 294 | CID and the number of bytes read." 295 | [^bytes data offset] 296 | (let [[version vlength] (varint/read-bytes data offset)] 297 | (when (not= 1 version) 298 | (throw (ex-info 299 | (str "Unable to decode CID version " (pr-str version)) 300 | {:version version}))) 301 | (let [[_ clength] (varint/read-bytes data (+ offset vlength)) 302 | [_ hlength] (mhash/read-bytes data (+ offset vlength clength)) 303 | length (+ vlength clength hlength) 304 | buffer (b/byte-array length)] 305 | (b/copy data offset buffer 0 length) 306 | [(->ContentID buffer nil 0) length]))) 307 | 308 | 309 | (defn write-bytes 310 | "Write an encoded content identifier to a byte array at the given offset. 311 | Returns the number of bytes written." 312 | [^ContentID cid ^bytes buffer offset] 313 | (let [encoded (inner-bytes cid) 314 | [version _ hsize] (read-header encoded)] 315 | (if (zero? version) 316 | (b/copy encoded hsize buffer offset (- (alength encoded) hsize)) 317 | (b/copy encoded buffer offset)))) 318 | 319 | 320 | (defn encode 321 | "Encode a content identifier into a binary representation. Returns the byte 322 | array." 323 | ^bytes 324 | [^ContentID cid] 325 | (if (zero? (:version cid)) 326 | (mhash/encode (:hash cid)) 327 | (b/copy (inner-bytes cid)))) 328 | 329 | 330 | (defn decode 331 | "Decode a content identifier from a byte array." 332 | [^bytes data] 333 | (if (and (= 34 (alength data)) 334 | (= 0x12 (b/get-byte data 0)) 335 | (= 0x20 (b/get-byte data 1))) 336 | ;; v0 CID (bare multihash) 337 | (let [mhash (mhash/decode data) 338 | ;; This is a bit of a departure from the IPLD/CID spec, but using the 339 | ;; raw codec for 'unknown' seems generally more sensible. 340 | code (mcodec/resolve-code :raw) 341 | encoded (encode-bytes 0 code mhash)] 342 | (->ContentID encoded nil 0)) 343 | ;; v1+ CID 344 | (first (read-bytes data 0)))) 345 | 346 | 347 | ;; ## String Serialization 348 | 349 | (defn format 350 | "Format a CID into a string representation. Returns a multibase-prefixed 351 | string representing the encoded value. Uses base32 if not otherwise 352 | specified." 353 | ([cid] 354 | (if (zero? (:version cid)) 355 | (mbase/format* :base58btc (mhash/encode (:hash cid))) 356 | (format :base32 cid))) 357 | ([base cid] 358 | (if (zero? (:version cid)) 359 | (throw (ex-info "v0 CID values cannot be formatted in alternative bases" 360 | {:cid cid})) 361 | (mbase/format base (inner-bytes cid))))) 362 | 363 | 364 | (defn parse 365 | "Parse a content identifier from a base-encoded string." 366 | [string] 367 | (if (and (= 46 (count string)) (str/starts-with? string "Qm")) 368 | (decode (b58/decode string)) 369 | (decode (mbase/parse string)))) 370 | 371 | 372 | (defn inspect 373 | "Inspect a CID, encoded byte array, or string and return a map of information 374 | about the content identifier." 375 | [x] 376 | (when-let [cid (cond 377 | (instance? ContentID x) x 378 | (b/bytes? x) (decode x) 379 | (string? x) (parse x) 380 | :else nil)] 381 | (let [info {:length (:length cid) 382 | :version (:version cid) 383 | :codec (:codec cid) 384 | :code (:code cid) 385 | :hash (:hash cid)}] 386 | (if (and (string? x) (pos? (:version cid))) 387 | (merge (mbase/inspect x) info) 388 | info)))) 389 | -------------------------------------------------------------------------------- /src/multiformats/codec.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.codec 2 | "Multicodec is a multiformat which wraps other formats with a tiny bit of 3 | self-description. A multicodec identifier may either be a varint (in a byte 4 | string) or a character (in a text string). 5 | 6 | https://github.com/multiformats/multicodec" 7 | (:require 8 | [multiformats.address :as maddr] 9 | [multiformats.base :as mbase] 10 | [multiformats.hash :as mhash])) 11 | 12 | 13 | ;; ## Code Tables 14 | 15 | (def miscellaneous-codes 16 | "Miscellaneous codes." 17 | {:raw 0x55 ; Raw binary data. 18 | ,,,}) 19 | 20 | 21 | (def serialization-codes 22 | "General-purpose serialization formats." 23 | {:cbor 0x51 ; CBOR 24 | ;; :bson 0x?? ; Binary JSON 25 | ;; :ubjson 0x?? ; Universal Binary JSON 26 | :protobuf 0x50 ; Protocol Buffers 27 | ;; :capnp 0x?? ; Cap-n-Proto 28 | ;; :flatbuf 0x?? ; FlatBuffers 29 | :rlp 0x60 ; Recursive Length Prefix 30 | ;; :msgpack 0x?? ; MessagePack 31 | ;; :binc 0x?? ; Binc 32 | :bencode 0x63 ; Bencode 33 | ,,,}) 34 | 35 | 36 | (def multiformat-codes 37 | "Generic codes to indicate which multiformat a value represents." 38 | {:multicodec 0x30 39 | :multihash 0x31 40 | :multiaddr 0x32 41 | :multibase 0x33}) 42 | 43 | 44 | (def ipld-codes 45 | "Structured data formats used in IPLD and other systems." 46 | {:ipld-pb 0x70 ; MerkleDAG protobuf 47 | :ipld-cbor 0x71 ; MerkleDAG cbor 48 | :ipld-json 0x0129 ; MerkleDAG json 49 | :git-raw 0x78 ; Raw Git object 50 | :eth-block 0x90 ; Ethereum Block (RLP) 51 | :eth-block-list 0x91 ; Ethereum Block List (RLP) 52 | :eth-tx-trie 0x92 ; Ethereum Transaction Trie (Eth-Trie) 53 | :eth-tx 0x93 ; Ethereum Transaction (RLP) 54 | :eth-tx-receipt-trie 0x94 ; Ethereum Transaction Receipt Trie (Eth-Trie) 55 | :eth-tx-receipt 0x95 ; Ethereum Transaction Receipt (RLP) 56 | :eth-state-trie 0x96 ; Ethereum State Trie (Eth-Secure-Trie) 57 | :eth-account-snapshot 0x97 ; Ethereum Account Snapshot (RLP) 58 | :eth-storage-trie 0x98 ; Ethereum Contract Storage Trie (Eth-Secure-Trie) 59 | :bitcoin-block 0xb0 ; Bitcoin Block 60 | :bitcoin-tx 0xb1 ; Bitcoin Tx 61 | :zcash-block 0xc0 ; Zcash Block 62 | :zcash-tx 0xc1 ; Zcash Tx 63 | :stellar-block 0xd0 ; Stellar Block 64 | :stellar-tx 0xd1 ; Stellar Tx 65 | :decred-block 0xe0 ; Decred Block 66 | :decred-tx 0xe1 ; Decred Tx 67 | :dash-block 0xf0 ; Dash Block 68 | :dash-tx 0xf1 ; Dash Tx 69 | :torrent-info 0x7b ; Torrent file info field (bencoded) 70 | :torrent-file 0x7c ; Torrent file (bencoded) 71 | :ed25519-pub 0xed ; Ed25519 public key 72 | ,,,}) 73 | 74 | 75 | ;; ## Lookup Maps 76 | 77 | (def key->code 78 | "Map of codec keys to compact code values." 79 | ;; TODO: check for conflicts 80 | (merge 81 | miscellaneous-codes 82 | multiformat-codes 83 | (into {} 84 | (map (juxt :key :code)) 85 | (vals maddr/protocols)) 86 | mbase/codes 87 | mhash/codes 88 | serialization-codes 89 | ipld-codes)) 90 | 91 | 92 | (def code->key 93 | "Map of compact code values to codec keys." 94 | (into {} (map (juxt val key)) key->code)) 95 | 96 | 97 | (defn register! 98 | "Register a new codec keyword and numeric code pair." 99 | [codec-key code] 100 | (when-not (and (keyword? codec-key) 101 | (integer? code) 102 | (not (neg? code))) 103 | (throw (ex-info "Invalid arguments given to register-codec!" 104 | {:key codec-key 105 | :code code}))) 106 | (when-let [extant-code (key->code codec-key)] 107 | (when (not= code extant-code) 108 | (throw (ex-info 109 | (str "Codec " (name codec-key) 110 | " is already registered with code " extant-code) 111 | {:key codec-key 112 | :code code 113 | :extant extant-code})))) 114 | (when-let [extant-key (code->key code)] 115 | (when (not= codec-key extant-key) 116 | (throw (ex-info 117 | (str "Code " code " is already registered by codec " 118 | (name extant-key)) 119 | {:key codec-key 120 | :code code 121 | :extant extant-key})))) 122 | #?(:clj (alter-var-root #'key->code assoc codec-key code) 123 | :cljs (set! key->code (assoc key->code codec-key code))) 124 | #?(:clj (alter-var-root #'code->key assoc code codec-key) 125 | :cljs (set! code->key (assoc code->key code codec-key))) 126 | nil) 127 | 128 | 129 | (defn unregister! 130 | "Remove the registration for an existing codec key. Does not throw an error 131 | if the key is not registered already." 132 | [codec-key] 133 | (when-let [code (key->code codec-key)] 134 | #?(:clj (alter-var-root #'key->code dissoc codec-key) 135 | :cljs (set! key->code (dissoc key->code codec-key))) 136 | #?(:clj (alter-var-root #'code->key dissoc code) 137 | :cljs (set! code->key (dissoc code->key code)))) 138 | nil) 139 | 140 | 141 | (defn resolve-key 142 | "Resolve a codec to a keyword name or falls back to a numeric code, or throws 143 | an exception on invalid input." 144 | [codec] 145 | (cond 146 | (integer? codec) 147 | (if (nat-int? codec) 148 | (code->key codec codec) 149 | (throw (ex-info (str "Multicodec codes cannot be negative: " codec) 150 | {:codec codec}))) 151 | 152 | (keyword? codec) 153 | (if (key->code codec) 154 | codec 155 | (throw (ex-info 156 | (str codec " does not map to a known multicodec code.") 157 | {:codec codec}))) 158 | 159 | :else 160 | (throw (ex-info 161 | (str (pr-str codec) 162 | " is not a valid codec keyword or numeric code.") 163 | {:codec codec})))) 164 | 165 | 166 | (defn resolve-code 167 | "Resolve a keyword to a numeric code, or throws an exception on invalid 168 | input." 169 | [codec] 170 | (cond 171 | (integer? codec) 172 | (if (nat-int? codec) 173 | codec 174 | (throw (ex-info (str "Multicodec codes cannot be negative: " codec) 175 | {:codec codec}))) 176 | 177 | (keyword? codec) 178 | (or (key->code codec) 179 | (throw (ex-info 180 | (str codec " does not map to a known multicodec code.") 181 | {:codec codec}))) 182 | 183 | :else 184 | (throw (ex-info 185 | (str (pr-str codec) 186 | " is not a valid codec keyword or numeric code.") 187 | {:codec codec})))) 188 | -------------------------------------------------------------------------------- /src/multiformats/hash.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.hash 2 | "Multihash is a protocol for differentiating outputs from various 3 | well-established cryptographic hash functions, addressing size and encoding 4 | considerations. 5 | 6 | https://github.com/multiformats/multihash" 7 | (:refer-clojure :exclude [test]) 8 | (:require 9 | [alphabase.base16 :as hex] 10 | [alphabase.bytes :as b] 11 | [clojure.string :as str] 12 | #?@(:cljs 13 | [[goog.crypt.Md5] 14 | [goog.crypt.Sha1] 15 | [goog.crypt.Sha256] 16 | [goog.crypt.Sha512]]) 17 | [multiformats.varint :as varint]) 18 | #?(:clj 19 | (:import 20 | (clojure.lang 21 | ILookup 22 | IMeta 23 | IObj) 24 | java.io.InputStream 25 | java.nio.ByteBuffer 26 | java.security.MessageDigest))) 27 | 28 | 29 | (def codes 30 | "Hash algorithm identifiers for use in multihashes." 31 | {:identity 0x00 32 | :md4 0xd4 33 | :md5 0xd5 34 | :sha1 0x11 35 | :sha2-256 0x12 36 | :sha2-512 0x13 37 | :dbl-sha2-256 0x56 38 | :sha3-224 0x17 39 | :sha3-256 0x16 40 | :sha3-384 0x15 41 | :sha3-512 0x14 42 | :shake-128 0x18 43 | :shake-256 0x19 44 | :keccak-224 0x1A 45 | :keccak-256 0x1B 46 | :keccak-384 0x1C 47 | :keccak-512 0x1D 48 | :murmur3 0x22 49 | :x11 0x1100}) 50 | 51 | 52 | (def ^:private code->algo 53 | "Mapping of numeric code to algorithm keyword." 54 | (into {} 55 | (map (juxt val key)) 56 | codes)) 57 | 58 | 59 | ;; ## Coding Functions 60 | 61 | (defn- read-header 62 | "Read the algorithm code and digest bit size from the encoded bytes. Returns 63 | a tuple of the numeric code, byte length, and number of bytes read." 64 | [^bytes data] 65 | (let [[code csize] (varint/read-bytes data 0) 66 | [length lsize] (varint/read-bytes data csize)] 67 | [code length (+ csize lsize)])) 68 | 69 | 70 | (defn- decode-parameters 71 | "Read the header and digest from the encoded bytes." 72 | [^bytes data] 73 | (let [[code length offset] (read-header data) 74 | digest (str/lower-case (subs (hex/encode data) (* 2 offset)))] 75 | {:code code 76 | :algorithm (code->algo code) 77 | :length length 78 | :digest digest})) 79 | 80 | 81 | (defn- encode-bytes 82 | "Encode a multihash algorithm, digest length, and digest bytes into a single 83 | byte array." 84 | [code ^bytes digest] 85 | (when (zero? (alength digest)) 86 | (throw (ex-info "Cannot encode a multihash with an empty digest" 87 | {:code code}))) 88 | (let [header (b/byte-array 8) 89 | clength (varint/write-bytes code header 0) 90 | llength (varint/write-bytes (alength digest) header clength) 91 | hlength (+ clength llength) 92 | buffer (b/byte-array (+ hlength (alength digest)))] 93 | (b/copy header 0 buffer 0 hlength) 94 | (b/copy digest 0 buffer hlength (alength digest)) 95 | buffer)) 96 | 97 | 98 | (defn- mhash-str 99 | "Create a string representation of a multihash from a map of parameters." 100 | [params] 101 | (let [algo (if-let [algorithm (:algorithm params)] 102 | (name algorithm) 103 | (:code params))] 104 | (str "hash:" algo \: (:digest params)))) 105 | 106 | 107 | ;; ## Multihash Type 108 | 109 | #?(:bb 110 | (defrecord Multihash 111 | [_bytes] 112 | 113 | Object 114 | 115 | (toString 116 | [this] 117 | (mhash-str this))) 118 | 119 | :clj 120 | (deftype Multihash 121 | [^bytes _bytes 122 | _meta 123 | ^:unsynchronized-mutable _hash] 124 | 125 | java.io.Serializable 126 | 127 | 128 | Object 129 | 130 | (toString 131 | [_] 132 | (mhash-str (decode-parameters _bytes))) 133 | 134 | 135 | (equals 136 | [this that] 137 | (cond 138 | (identical? this that) true 139 | 140 | (instance? Multihash that) 141 | (b/bytes= _bytes (._bytes ^Multihash that)) 142 | 143 | :else false)) 144 | 145 | 146 | (hashCode 147 | [_] 148 | (let [hc _hash] 149 | (if (zero? hc) 150 | (let [params (decode-parameters _bytes) 151 | hc (hash [::multihash (:code params) (:digest params)])] 152 | (set! _hash hc) 153 | hc) 154 | hc))) 155 | 156 | 157 | Comparable 158 | 159 | (compareTo 160 | [this that] 161 | (cond 162 | (identical? this that) 0 163 | 164 | (instance? Multihash that) 165 | (b/compare _bytes (._bytes ^Multihash that)) 166 | 167 | :else 168 | (throw (ex-info 169 | (str "Cannot compare multihash value to " (type that)) 170 | {:this this 171 | :that that})))) 172 | 173 | 174 | ILookup 175 | 176 | (valAt 177 | [this k] 178 | (.valAt this k nil)) 179 | 180 | 181 | (valAt 182 | [_ k not-found] 183 | (case k 184 | :length (alength _bytes) 185 | :code (first (read-header _bytes)) 186 | :algorithm (let [[code] (read-header _bytes)] 187 | (code->algo code)) 188 | :bits (let [[_ length] (read-header _bytes)] 189 | (* length 8)) 190 | :digest (:digest (decode-parameters _bytes)) 191 | not-found)) 192 | 193 | 194 | IMeta 195 | 196 | (meta 197 | [_] 198 | _meta) 199 | 200 | 201 | IObj 202 | 203 | (withMeta 204 | [_ meta-map] 205 | (Multihash. _bytes meta-map _hash))) 206 | 207 | :cljs 208 | (deftype Multihash 209 | [_bytes 210 | _meta 211 | ^:unsynchronized-mutable _hash] 212 | 213 | Object 214 | 215 | (toString 216 | [_] 217 | (mhash-str (decode-parameters _bytes))) 218 | 219 | 220 | IEquiv 221 | 222 | (-equiv 223 | [this that] 224 | (cond 225 | (identical? this that) true 226 | 227 | (instance? Multihash that) 228 | (b/bytes= _bytes (.-_bytes ^Multihash that)) 229 | 230 | :else false)) 231 | 232 | 233 | IHash 234 | 235 | (-hash 236 | [_] 237 | (let [hc _hash] 238 | (if (zero? hc) 239 | (let [params (decode-parameters _bytes) 240 | hc (hash [::multihash (:code params) (:digest params)])] 241 | (set! _hash hc) 242 | hc) 243 | hc))) 244 | 245 | 246 | IComparable 247 | 248 | (-compare 249 | [this that] 250 | (cond 251 | (identical? this that) 0 252 | 253 | (instance? Multihash that) 254 | (b/compare _bytes (.-_bytes ^Multihash that)) 255 | 256 | :else 257 | (throw (ex-info 258 | (str "Cannot compare multihash value to " (type that)) 259 | {:this this 260 | :that that})))) 261 | 262 | 263 | ILookup 264 | 265 | (-lookup 266 | [this k] 267 | (-lookup this k nil)) 268 | 269 | 270 | (-lookup 271 | [_ k not-found] 272 | (case k 273 | :length (alength _bytes) 274 | :code (first (read-header _bytes)) 275 | :algorithm (let [[code] (read-header _bytes)] 276 | (code->algo code)) 277 | :bits (let [[_ length] (read-header _bytes)] 278 | (* length 8)) 279 | :digest (:digest (decode-parameters _bytes)) 280 | not-found)) 281 | 282 | 283 | IMeta 284 | 285 | (-meta 286 | [_] 287 | _meta) 288 | 289 | 290 | IWithMeta 291 | 292 | (-with-meta 293 | [_ meta-map] 294 | (Multihash. _bytes meta-map _hash)))) 295 | 296 | 297 | (alter-meta! #'->Multihash assoc :private true) 298 | 299 | 300 | ;; ## Constructors 301 | 302 | (defn- resolve-code 303 | "Resolve an algorithm to a numeric code, or throws an exception on invalid input." 304 | [algorithm] 305 | (cond 306 | (integer? algorithm) 307 | (if (nat-int? algorithm) 308 | algorithm 309 | (throw (ex-info (str "Hash algorithm codes cannot be negative: " algorithm) 310 | {:algorithm algorithm}))) 311 | 312 | (keyword? algorithm) 313 | (or (get codes algorithm) 314 | (throw (ex-info 315 | (str algorithm " does not map to a known hash algorithm code.") 316 | {:algorithm algorithm}))) 317 | 318 | :else 319 | (throw (ex-info 320 | (str (pr-str algorithm) 321 | " is not a valid algorithm keyword or numeric code.") 322 | {:algorithm algorithm})))) 323 | 324 | 325 | (defn- resolve-digest 326 | "Resolve a digest to a byte array, or throws an exception on invalid input." 327 | [digest] 328 | (cond 329 | (b/bytes? digest) 330 | digest 331 | 332 | (string? digest) 333 | (hex/decode digest) 334 | 335 | :else 336 | (throw (ex-info 337 | (str (pr-str digest) " is not a byte array or hex string.") 338 | {:digest digest})))) 339 | 340 | 341 | (defn create 342 | "Constructs a new Multihash identifier from the given algorithm key (or 343 | numeric code) and digest byte array (or hex string)." 344 | [algorithm digest] 345 | (let [code (resolve-code algorithm) 346 | digest-bytes (resolve-digest digest) 347 | encoded (encode-bytes code digest-bytes)] 348 | #?(:bb (assoc (->Multihash encoded) 349 | :length (count encoded) 350 | :digest (str/lower-case (hex/encode digest-bytes)) 351 | :code code 352 | :algorithm (code->algo code) 353 | :bits (* 8 (count digest-bytes))) 354 | :default (->Multihash encoded nil 0)))) 355 | 356 | 357 | (defn multihash? 358 | "True if the value is a multihash object." 359 | [x] 360 | (instance? Multihash x)) 361 | 362 | 363 | ;; ## Serialization 364 | 365 | (defn- inner-bytes 366 | "Retrieve the inner encoded bytes from a multihash value." 367 | ^bytes 368 | [^Multihash mhash] 369 | (#?(:cljs .-_bytes, :default ._bytes) mhash)) 370 | 371 | 372 | (defn read-bytes 373 | "Read a multihash from a byte array. Returns a tuple containing the multihash 374 | and the number of bytes read." 375 | [^bytes data offset] 376 | (let [[_ csize] (varint/read-bytes data offset) 377 | [length lsize] (varint/read-bytes data (+ offset csize)) 378 | total-size (+ csize lsize length) 379 | buffer (b/byte-array total-size)] 380 | (b/copy data offset buffer 0 total-size) 381 | [(Multihash. buffer nil 0) total-size])) 382 | 383 | 384 | (defn write-bytes 385 | "Write an encoded multihash to a byte array at the given offset. Returns the 386 | number of bytes written." 387 | [^Multihash mhash ^bytes buffer offset] 388 | (b/copy (inner-bytes mhash) buffer offset)) 389 | 390 | 391 | (defn encode 392 | "Encode a multihash into a binary representation. Returns the byte array." 393 | ^bytes 394 | [^Multihash mhash] 395 | (b/copy (inner-bytes mhash))) 396 | 397 | 398 | (defn decode 399 | "Decode a multihash by reading data from a byte array." 400 | [^bytes data] 401 | (first (read-bytes data 0))) 402 | 403 | 404 | (defn hex 405 | "Format a multihash as a hex string." 406 | [mhash] 407 | (str/lower-case (hex/encode (inner-bytes mhash)))) 408 | 409 | 410 | (defn parse 411 | "Parse a hex string into a multihash." 412 | [string] 413 | (decode (hex/decode string))) 414 | 415 | 416 | ;; ## Digest Constructors 417 | 418 | (defn- digest-content 419 | "Constructs a cryptographic digest for a given hasher and content. Content 420 | may be in the form of a raw byte array, a `ByteBuffer`, an `InputStream`, or 421 | a string. Returns a byte array with the digest." 422 | ^bytes 423 | [^MessageDigest hasher content] 424 | (cond 425 | (string? content) 426 | (.update hasher (b/from-string content)) 427 | 428 | (b/bytes? content) 429 | (.update hasher ^bytes content) 430 | 431 | #?@(:clj 432 | [(instance? ByteBuffer content) 433 | (.update hasher ^ByteBuffer content) 434 | 435 | (instance? InputStream content) 436 | (let [buffer (byte-array 4096)] 437 | (loop [] 438 | (let [n (.read ^InputStream content buffer 0 (count buffer))] 439 | (when (pos? n) 440 | (.update hasher buffer 0 n) 441 | (recur)))))]) 442 | 443 | :else 444 | (throw (ex-info (str "Don't know how to compute digest from " 445 | (type content)) 446 | {:content content}))) 447 | (.digest hasher)) 448 | 449 | 450 | (defn md5 451 | "Calculate a multihash of the content using MD5." 452 | [content] 453 | (let [hasher #?(:clj (MessageDigest/getInstance "MD5") 454 | :cljs (goog.crypt.Md5.)) 455 | digest (digest-content hasher content)] 456 | (create :md5 digest))) 457 | 458 | 459 | (defn sha1 460 | "Calculate a multihash of the content using SHA-1." 461 | [content] 462 | (let [hasher #?(:clj (MessageDigest/getInstance "SHA-1") 463 | :cljs (goog.crypt.Sha1.)) 464 | digest (digest-content hasher content)] 465 | (create :sha1 digest))) 466 | 467 | 468 | (defn sha2-256 469 | "Calculate a multihash of the content using SHA-256." 470 | [content] 471 | (let [hasher #?(:clj (MessageDigest/getInstance "SHA-256") 472 | :cljs (goog.crypt.Sha256.)) 473 | digest (digest-content hasher content)] 474 | (create :sha2-256 digest))) 475 | 476 | 477 | (defn sha2-512 478 | "Calculate a multihash of the content using SHA-512." 479 | [content] 480 | (let [hasher #?(:clj (MessageDigest/getInstance "SHA-512") 481 | :cljs (goog.crypt.Sha512.)) 482 | digest (digest-content hasher content)] 483 | (create :sha2-512 digest))) 484 | 485 | 486 | (def functions 487 | "Map of hash digest functions available." 488 | {:md5 md5 489 | :sha1 sha1 490 | :sha2-256 sha2-256 491 | :sha2-512 sha2-512}) 492 | 493 | 494 | (defn test 495 | "Determines whether a multihash is a correct identifier for some content by 496 | recomputing the digest for the algorithm specified in the multihash. Returns 497 | nil if either argument is nil, true if the digest matches, or false if not. 498 | Throws an exception if the multihash specifies an unsupported algorithm." 499 | [mhash content] 500 | (when (and mhash content) 501 | (if-let [hash-fn (get functions (:algorithm mhash))] 502 | (let [other (hash-fn content)] 503 | #?(:bb (= (:digest mhash) 504 | (:digest other)) 505 | :default (= mhash other))) 506 | (throw (ex-info 507 | (str "No supported hashing function for algorithm " 508 | (or (:algorithm mhash) (:code mhash)) 509 | " to validate " mhash) 510 | {:code (:code mhash) 511 | :algorithm (:algorithm mhash)}))))) 512 | -------------------------------------------------------------------------------- /src/multiformats/varint.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.varint 2 | "Implementation of an MSB unsigned variable-size integer. 3 | 4 | Unsigned integers are serialized 7 bits at a time, starting with the 5 | least-significant bits. The highest bit (msb) in each output byte indicates 6 | if there is a continuation byte. 7 | 8 | https://github.com/multiformats/unsigned-varint" 9 | (:require 10 | [alphabase.bytes :as b])) 11 | 12 | 13 | ;; ## Encoding 14 | 15 | (defn write-bytes 16 | "Write a value as a varint to a byte array at the given offset. Returns the 17 | number of bytes written." 18 | [value ^bytes buffer offset] 19 | (when (neg? value) 20 | (throw (ex-info "Varints are unsigned and cannot be negative" 21 | {:value value}))) 22 | (loop [v value 23 | i 0] 24 | ;; Check for index out of bounds. 25 | (when (<= (alength buffer) (+ offset i)) 26 | (throw (ex-info 27 | (str "Varint write index out of bounds at position " 28 | (+ offset i) " (" i " bytes from offset " offset ")") 29 | {:offset (+ offset i)}))) 30 | ;; Check for overflow. 31 | (when (<= 9 i) 32 | (throw (ex-info 33 | "Varints larger than nine bytes are not currently supported" 34 | {:value value}))) 35 | (if (<= 0x80 v) 36 | ;; Write continuation byte and recur. 37 | (let [b (bit-or (bit-and v 0x7F) 0x80)] 38 | (b/set-byte buffer (+ offset i) b) 39 | (recur (unsigned-bit-shift-right v 7) 40 | (inc i))) 41 | ;; Final byte. 42 | (let [b (bit-and v 0x7F)] 43 | (b/set-byte buffer (+ offset i) b) 44 | (inc i))))) 45 | 46 | 47 | (defn encode 48 | "Encode a value as a sequence of varint bytes. Returns the encoded byte 49 | array." 50 | ^bytes 51 | [value] 52 | (let [buffer (b/byte-array 9) 53 | length (write-bytes value buffer 0) 54 | result (b/byte-array length)] 55 | (b/copy buffer 0 result 0 length) 56 | result)) 57 | 58 | 59 | ;; ## Decoding 60 | 61 | (defn read-bytes 62 | "Read bytes from the byte array at the given offset. Returns a tuple with the 63 | decoded varint and the number of bytes read." 64 | [^bytes data offset] 65 | (loop [i offset 66 | n 0 67 | v 0] 68 | ;; Check for index out of bounds. 69 | (when (<= (alength data) i) 70 | (throw (ex-info 71 | (str "Ran out of bytes to decode at position " i 72 | " (" n " bytes from offset " offset ")") 73 | {:offset offset 74 | :length (alength data)}))) 75 | ;; Check for overflow of soft limit. 76 | (when (<= 9 n) 77 | (throw (ex-info 78 | "Varints larger than nine bytes are not currently supported" 79 | {:offset offset}))) 80 | ;; Decode next byte. 81 | (let [b (b/get-byte data i)] 82 | (if (< b 0x80) 83 | ;; Final byte. 84 | [(bit-or (bit-shift-left b (* 7 n)) v) 85 | (inc n)] 86 | ;; Continuation byte. Add masked lower bits and recur. 87 | (recur (inc i) 88 | (inc n) 89 | (bit-or (bit-shift-left (bit-and b 0x7F) (* 7 n)) v)))))) 90 | 91 | 92 | (defn decode 93 | "Decode a byte array as a varint value. Returns the decoded value. 94 | 95 | This is a shorthand for reading the bytes at the beginning of the array and 96 | ignoring any extra data." 97 | [buffer] 98 | (first (read-bytes buffer 0))) 99 | -------------------------------------------------------------------------------- /test/multiformats/address_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.address-test 2 | (:require 3 | [alphabase.bytes :as b] 4 | [clojure.test :refer [deftest testing is]] 5 | [multiformats.address :as address])) 6 | 7 | 8 | (deftest creation 9 | (testing "of empty address" 10 | (let [addr (address/create)] 11 | (is (address/address? addr) 12 | "should create a new address") 13 | (is (empty? addr)) 14 | (is (zero? (count addr))) 15 | (is (= "" (str addr)) 16 | "should represent as an empty string"))) 17 | (testing "with basic protocols" 18 | (let [addr (address/create [[:dns "mvxcvi.com"] [:tcp 80]])] 19 | (is (address/address? addr)) 20 | (is (= "/dns/mvxcvi.com/tcp/80" (str addr)))))) 21 | 22 | 23 | (deftest predicate 24 | (is (address/address? (address/create [[:tcp 80]]))) 25 | (is (not (address/address? nil))) 26 | (is (not (address/address? "foo")))) 27 | 28 | 29 | (deftest protocol-encoding 30 | (testing "with bad entry" 31 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Address entry must be a protocol keyword or vector pair" 32 | (address/create [123])) 33 | "should throw when given an invalid entry type") 34 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Address entry must be a protocol keyword or vector pair" 35 | (address/create [["foo" 123]])) 36 | "should throw when given an invalid protocol type") 37 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Address entry must be a protocol keyword or vector pair" 38 | (address/create [[:dns "example.com" 123]])) 39 | "should throw when given an entry which is too long") 40 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Unsupported protocol type" 41 | (address/create [[:not-valid 123]])) 42 | "should throw on unsupported protocol keyword")) 43 | (testing "for null values" 44 | (is (= [[:tls]] 45 | (seq (address/create [:tls])))) 46 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol tls does not support values" 47 | (address/create [[:tls "abc"]])) 48 | "should throw when null protocols given a value")) 49 | (testing "for utf8 values" 50 | (is (= [[:dns "example.com"]] 51 | (seq (address/create [[:dns "example.com"]])))) 52 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol dns requires a UTF-8 string value" 53 | (address/create [[:dns 123]])) 54 | "should throw when given a non-string value")) 55 | (testing "for ushort values" 56 | (is (= [[:tcp 80]] 57 | (seq (address/create [[:tcp 80]])))) 58 | (is (= [[:tcp 80]] 59 | (seq (address/create [[:tcp "80"]]))) 60 | "should parse string inputs") 61 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol tcp has invalid string value" 62 | (address/create [[:tcp "xyz"]])) 63 | "should throw when given a non-numeric string value") 64 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol tcp requires an unsigned short value or numeric string" 65 | (address/create [[:tcp :foo]])) 66 | "should throw when given an invalid value type") 67 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol tcp has value too big for unsigned short" 68 | (address/create [[:tcp 66000]])) 69 | "should throw when given an out-of-range number")) 70 | (testing "for ip4 values" 71 | (is (= [[:ip4 "127.0.0.1"]] 72 | (seq (address/create [[:ip4 "127.0.0.1"]])))) 73 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Invalid IPv4 address string" 74 | (address/create [[:ip4 "1.2.3.256"]])) 75 | "should throw on invalid ip4 address") 76 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Invalid IPv4 address string" 77 | (address/create [[:ip4 "::1"]])) 78 | "should throw on invalid ip4 address") 79 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol ip4 requires an IP address string" 80 | (address/create [[:ip4 123]])) 81 | "should throw on invalid value types")) 82 | (testing "for ip6 values" 83 | (is (= [[:ip6 "0:0:0:0:0:0:0:1"]] 84 | (seq (address/create [[:ip6 "::1"]]))) 85 | "should expand :: placeholder") 86 | (is (= [[:ip6 "2001:db8:85a3:0:0:8a2e:370:7334"]] 87 | (seq (address/create [[:ip6 "2001:0db8:85a3:0000:0000:8a2e:0370:7334"]])))) 88 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Invalid IPv6 address string" 89 | (address/create [[:ip6 "1.2.3.4"]])) 90 | "should throw on invalid ip6 address") 91 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Protocol ip6 requires an IP address string" 92 | (address/create [[:ip6 123]])) 93 | "should throw on invalid value types"))) 94 | 95 | 96 | (deftest string-serialization 97 | (testing "bad inputs" 98 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Expected address string to begin with a slash" 99 | (address/parse "foobar"))) 100 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Invalid protocol type string: 123" 101 | (address/parse "/123/foo"))) 102 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Unknown protocol type: foo" 103 | (address/parse "/foo"))) 104 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Missing value for tcp protocol" 105 | (address/parse "/dns/example.com/tcp")))) 106 | (testing "roundtrips" 107 | (is (= "/ip4/127.0.0.1" (str (address/parse "/ip4/127.0.0.1")))) 108 | (is (= "/ip4/10.8.0.5/tcp/80" (str (address/parse "/ip4/10.8.0.5/tcp/80")))) 109 | (is (= "/dns/example.com/udp/9090/quic" (str (address/parse "/dns/example.com/udp/9090/quic")))) 110 | (is (= "/dns6/abc.xyz/tcp/3217/tls/ws" (str (address/parse "/dns6/abc.xyz/tcp/3217/tls/ws")))))) 111 | 112 | 113 | (deftest binary-serialization 114 | (testing "bad data" 115 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Unsupported protocol code: 1234" 116 | (address/decode (b/init-bytes [210 9 5]))))) 117 | (testing "ip4 address" 118 | (let [addr (address/create [[:ip4 "127.0.0.1"]]) 119 | data [4 127 0 0 1]] 120 | (is (= data (b/byte-seq (address/encode addr)))) 121 | (is (= addr (address/decode (b/init-bytes data))))) 122 | (let [addr (address/create [[:ip4 "10.8.0.5"] [:tcp 80]]) 123 | data [4, 10 8 0 5 124 | 6, 0 80]] 125 | (is (= data (b/byte-seq (address/encode addr)))) 126 | (is (= addr (address/decode (b/init-bytes data)))))) 127 | (testing "ip6 address" 128 | (let [addr (address/create [[:ip6 "2001:db8:85a3:0:0:8a2e:370:7334"] 129 | [:sctp 8080] 130 | [:tls]]) 131 | data [41, 0x20 0x01 0x0d 0xb8 0x85 0xa3 0x00 0x00 0x00 0x00 0x8a 0x2e 0x03 0x70 0x73 0x34 132 | 132 1, 31 144 133 | 192 3]] 134 | (is (= data (b/byte-seq (address/encode addr)))) 135 | (is (= addr (address/decode (b/init-bytes data)))))) 136 | (testing "complex address" 137 | (let [addr (address/create [[:dns "example.com"] 138 | [:udp 9090] 139 | :quic]) 140 | data [53, 11, 101 120 97 109 112 108 101 46 99 111 109 141 | 145 2, 35 130 142 | 204 3]] 143 | (is (= data (b/byte-seq (address/encode addr)))) 144 | (is (= addr (address/decode (b/init-bytes data))))))) 145 | 146 | 147 | (deftest value-equality 148 | (let [addr (address/create [[:ip4 "127.0.0.1"]])] 149 | (is (= addr addr)) 150 | (is (= addr (address/parse "/ip4/127.0.0.1"))) 151 | (is (not= addr (address/create [[:ip4 "127.0.0.2"]]))) 152 | (is (not= addr (address/create [[:ip4 "127.0.0.1"] [:tcp 80]]))) 153 | (is (not= addr nil)) 154 | (is (not= addr :abc)))) 155 | 156 | 157 | (deftest value-hashing 158 | (let [addr (address/create [[:dns "example.com"] [:tcp 443] [:tls] [:http]])] 159 | (is (= (hash addr) (hash addr))) 160 | (is (= (hash addr) (hash (address/parse "/dns/example.com/tcp/443/tls/http")))) 161 | (is (not= (hash addr) (hash (address/create [[:dns "example.com"]])))))) 162 | 163 | 164 | (deftest value-comparison 165 | (let [localhost (address/create [[:ip4 "127.0.0.1"]]) 166 | example-com (address/create [[:dns "example.com"] [:tcp 443] [:tls] [:http]])] 167 | (is (zero? (compare localhost localhost))) 168 | (is (zero? (compare localhost (address/parse "/ip4/127.0.0.1")))) 169 | (is (neg? (compare localhost example-com))) 170 | (is (pos? (compare example-com localhost))) 171 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Cannot compare multiaddress value to" 172 | (compare localhost "abc")) 173 | "should throw on invalid comparison"))) 174 | 175 | 176 | (deftest value-metadata 177 | (let [addr (address/create [[:ip4 "127.0.0.1"]])] 178 | (is (nil? (meta addr)) 179 | "should be absent on creation") 180 | (is (= {:abc true} (meta (with-meta addr {:abc true}))) 181 | "should support metadata") 182 | (is (= addr (with-meta addr {:abc true})) 183 | "should not affect equality"))) 184 | 185 | 186 | (deftest entry-sequence 187 | (let [addr (address/create [[:dns "example.com"] [:tcp 443] [:tls] [:http]])] 188 | (is (= 4 (count addr))) 189 | (is (= [[:dns "example.com"] [:tcp 443] [:tls] [:http]] (seq addr))) 190 | (is (= [:dns "example.com"] (nth addr 0))) 191 | (is (= [:tls] (nth addr 2))) 192 | (is (= [:tcp 443] (nth addr 1 :missing))) 193 | (is (= :missing (nth addr 4 :missing))) 194 | (is (thrown-with-msg? #?(:clj IndexOutOfBoundsException, :cljs js/Error) #"Index 4 is outside the 4 elements in the address" 195 | (nth addr 4)) 196 | "should throw on out-of-bounds without not-found"))) 197 | 198 | 199 | (deftest entry-manipulation 200 | (let [addr (address/create [[:dns "example.com"] [:tcp 443]])] 201 | (is (= (address/create) (empty addr)) 202 | "should be emptyable") 203 | (is (= (address/create [[:dns "example.com"] [:tcp 443] :tls]) 204 | (conj addr :tls))) 205 | (is (= (address/create [[:dns "example.com"] [:tcp 443] :tls :http]) 206 | (conj addr [:tls nil] [:http]))) 207 | (is (= [:tcp 443] (peek addr))) 208 | (is (= (address/create [[:dns "example.com"]]) 209 | (pop addr))) 210 | (is (= (address/create) (pop (pop addr)))) 211 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) #"Can't pop empty address" 212 | (pop (address/create)))))) 213 | -------------------------------------------------------------------------------- /test/multiformats/base_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.base-test 2 | "Test cases drawn from: 3 | - https://tools.ietf.org/html/rfc4648#section-10 4 | - https://github.com/eth-r/multibase/tree/master/tests" 5 | (:require 6 | [alphabase.bytes :as b :refer [bytes=]] 7 | [clojure.test :refer [deftest testing is]] 8 | [multiformats.base :as mbase])) 9 | 10 | 11 | (deftest arg-validation 12 | (testing "formatting" 13 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 14 | :cljs js/Error) 15 | #"must be a keyword" 16 | (mbase/format* 123 (b/byte-array 1)))) 17 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 18 | :cljs js/Error) 19 | #"not format empty data" 20 | (mbase/format :base32 (b/byte-array 0)))) 21 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 22 | :cljs js/Error) 23 | #"foo does not have a supported multibase formatter" 24 | (mbase/format :foo (b/byte-array 1))))) 25 | (testing "parsing" 26 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 27 | :cljs js/Error) 28 | #"must be a keyword" 29 | (mbase/parse* 123 "abc"))) 30 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 31 | :cljs js/Error) 32 | #"foo does not have a supported multibase parser" 33 | (mbase/parse* :foo "abc"))) 34 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 35 | :cljs js/Error) 36 | #"is too short to be multibase-formatted data" 37 | (mbase/parse ""))) 38 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 39 | :cljs js/Error) 40 | #"is too short to be multibase-formatted data" 41 | (mbase/parse "1"))) 42 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 43 | :cljs js/Error) 44 | #"prefix \"x\" does not map to a supported multibase encoding" 45 | (mbase/parse "xabc"))))) 46 | 47 | 48 | (deftest rfc-vectors 49 | (doseq [[base input encoded] 50 | [[:base64pad "f" "MZg=="] 51 | [:base64pad "fo" "MZm8="] 52 | [:base64 "foo" "mZm9v"] 53 | [:base64pad "foob" "MZm9vYg=="] 54 | [:base64pad "fooba" "MZm9vYmE="] 55 | [:base64 "foobar" "mZm9vYmFy"] 56 | [:base32pad "f" "cmy======"] 57 | [:BASE32PAD "fo" "CMZXQ===="] 58 | [:base32pad "foo" "cmzxw6==="] 59 | [:BASE32PAD "foob" "CMZXW6YQ="] 60 | [:base32 "fooba" "bmzxw6ytb"] 61 | [:BASE32 "foobar" "BMZXW6YTBOI"] 62 | [:base32hexpad "f" "tco======"] 63 | [:BASE32HEXPAD "fo" "TCPNG===="] 64 | [:BASE32HEXPAD "foo" "TCPNMU==="] 65 | [:base32hex "foob" "vcpnmuog"] 66 | [:BASE32HEX "fooba" "VCPNMUOJ1"] 67 | [:BASE32HEXPAD "foobar" "TCPNMUOJ1E8======"] 68 | [:BASE16 "f" "F66"] 69 | [:BASE16 "fo" "F666F"] 70 | [:BASE16 "foo" "F666F6F"] 71 | [:BASE16 "foob" "F666F6F62"] 72 | [:BASE16 "fooba" "F666F6F6261"] 73 | [:BASE16 "foobar" "F666F6F626172"]]] 74 | (testing (name base) 75 | (let [data (b/from-string input)] 76 | (is (= encoded (mbase/format base data))) 77 | (is (bytes= data (mbase/parse encoded))))))) 78 | 79 | 80 | (deftest example-1 81 | (let [data (b/from-string "Decentralize everything!!") 82 | cases {:base2 "001000100011001010110001101100101011011100111010001110010011000010110110001101001011110100110010100100000011001010111011001100101011100100111100101110100011010000110100101101110011001110010000100100001" 83 | :base8 "71043126154533472162302661513646244031273145344745643206455631620441" 84 | ;; :base10 "9429328951066508984658627669258025763026247056774804621697313" 85 | :base16 "f446563656e7472616c697a652065766572797468696e672121" 86 | :BASE16 "F446563656E7472616C697A652065766572797468696E672121" 87 | :base32 "birswgzloorzgc3djpjssazlwmvzhs5dinfxgoijb" 88 | :BASE32 "BIRSWGZLOORZGC3DJPJSSAZLWMVZHS5DINFXGOIJB" 89 | :base32pad "cirswgzloorzgc3djpjssazlwmvzhs5dinfxgoijb" 90 | :BASE32PAD "CIRSWGZLOORZGC3DJPJSSAZLWMVZHS5DINFXGOIJB" 91 | :base32hex "v8him6pbeehp62r39f9ii0pbmclp7it38d5n6e891" 92 | :BASE32HEX "V8HIM6PBEEHP62R39F9II0PBMCLP7IT38D5N6E891" 93 | :base32hexpad "t8him6pbeehp62r39f9ii0pbmclp7it38d5n6e891" 94 | :BASE32HEXPAD "T8HIM6PBEEHP62R39F9II0PBMCLP7IT38D5N6E891" 95 | ;; :base32z "het1sg3mqqt3gn5djxj11y3msci3817depfzgqejb" 96 | ;; :base58flickr "Ztwe7gVTeK8wswS1gf8hrgAua9fcw9reboD" 97 | :base58btc "zUXE7GvtEk8XTXs1GF8HSGbVA9FCX9SEBPe" 98 | :base64 "mRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ" 99 | :base64pad "MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ==" 100 | :base64url "uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ" 101 | :base64urlpad "URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchIQ=="}] 102 | (doseq [[base encoded] cases] 103 | (testing (name base) 104 | (is (= encoded (mbase/format base data))) 105 | (is (bytes= data (mbase/parse encoded))))))) 106 | 107 | 108 | (deftest example-2 109 | (let [data (b/from-string "yes mani !") 110 | cases {:base2 "001111001011001010111001100100000011011010110000101101110011010010010000000100001" 111 | :base8 "7171312714403326055632220041" 112 | ;; :base10 "9573277761329450583662625" 113 | :base16 "f796573206d616e692021" 114 | :BASE16 "F796573206D616E692021" 115 | :base32 "bpfsxgidnmfxgsibb" 116 | :BASE32 "BPFSXGIDNMFXGSIBB" 117 | :base32pad "cpfsxgidnmfxgsibb" 118 | :BASE32PAD "CPFSXGIDNMFXGSIBB" 119 | :base32hex "vf5in683dc5n6i811" 120 | :BASE32HEX "VF5IN683DC5N6I811" 121 | :base32hexpad "tf5in683dc5n6i811" 122 | :BASE32HEXPAD "TF5IN683DC5N6I811" 123 | ;; :base32z "hxf1zgedpcfzg1ebb" 124 | ;; :base58flickr "Z7Pznk19XTTzBtx" 125 | :base58btc "z7paNL19xttacUY" 126 | :base64 "meWVzIG1hbmkgIQ" 127 | :base64pad "MeWVzIG1hbmkgIQ==" 128 | :base64url "ueWVzIG1hbmkgIQ" 129 | :base64urlpad "UeWVzIG1hbmkgIQ=="}] 130 | (doseq [[base encoded] cases] 131 | (testing (name base) 132 | (is (= encoded (mbase/format base data))) 133 | (is (bytes= data (mbase/parse encoded))))))) 134 | 135 | 136 | (deftest example-3 137 | (let [data (b/from-string "hello world") 138 | cases {:base2 "00110100001100101011011000110110001101111001000000111011101101111011100100110110001100100" 139 | :base8 "764145330661571007355734466144" ; originally had leading zero? 140 | ;; :base10 "9126207244316550804821666916" 141 | :base16 "f68656c6c6f20776f726c64" 142 | :BASE16 "F68656C6C6F20776F726C64" 143 | :base32 "bnbswy3dpeb3w64tmmq" 144 | :BASE32 "BNBSWY3DPEB3W64TMMQ" 145 | :base32pad "cnbswy3dpeb3w64tmmq======" 146 | :BASE32PAD "CNBSWY3DPEB3W64TMMQ======" 147 | :base32hex "vd1imor3f41rmusjccg" 148 | :BASE32HEX "VD1IMOR3F41RMUSJCCG" 149 | :base32hexpad "td1imor3f41rmusjccg======" 150 | :BASE32HEXPAD "TD1IMOR3F41RMUSJCCG======" 151 | ;; :base32z "hpb1sa5dxrb5s6hucco" 152 | ;; :base58flickr "ZrTu1dk6cWsRYjYu" 153 | :base58btc "zStV1DL6CwTryKyV" 154 | :base64 "maGVsbG8gd29ybGQ" 155 | :base64pad "MaGVsbG8gd29ybGQ=" 156 | :base64url "uaGVsbG8gd29ybGQ" 157 | :base64urlpad "UaGVsbG8gd29ybGQ="}] 158 | (doseq [[base encoded] cases] 159 | (testing (name base) 160 | (is (= encoded (mbase/format base data))) 161 | (is (bytes= data (mbase/parse encoded))))))) 162 | 163 | 164 | (deftest mixed-case 165 | (let [data (b/from-string "hello world")] 166 | (testing "base16 vs BASE16" 167 | (is (bytes= data (mbase/parse "f68656c6c6f20776F726C64"))) 168 | (is (bytes= data (mbase/parse "F68656c6c6f20776F726C64")))) 169 | (testing "base32 vs BASE32" 170 | (is (bytes= data (mbase/parse "bnbswy3dpeB3W64TMMQ"))) 171 | (is (bytes= data (mbase/parse "Bnbswy3dpeB3W64TMMQ")))) 172 | (testing "base32hex vs BASE32HEX" 173 | (is (bytes= data (mbase/parse "vd1imor3f41RMUSJCCG"))) 174 | (is (bytes= data (mbase/parse "Vd1imor3f41RMUSJCCG")))) 175 | (testing "base32pad vs BASE32PAD" 176 | (is (bytes= data (mbase/parse "cnbswy3dpeB3W64TMMQ======"))) 177 | (is (bytes= data (mbase/parse "Cnbswy3dpeB3W64TMMQ======")))) 178 | (testing "base32hexpad vs BASE32HEXPAD" 179 | (is (bytes= data (mbase/parse "td1imor3f41RMUSJCCG======"))) 180 | (is (bytes= data (mbase/parse "Td1imor3f41RMUSJCCG======")))))) 181 | 182 | 183 | (deftest miscellaneous 184 | (testing "binary padding" 185 | (is (bytes= (b/init-bytes [0x01 0x23 0x45]) 186 | (mbase/parse "00010010001101000101")))) 187 | (testing "inspect" 188 | (let [info (mbase/inspect "zUXE7GvtEk8XTXs1GF8HSGbVA9FCX9SEBPe")] 189 | (is (map? info)) 190 | (is (= "z" (:prefix info))) 191 | (is (= :base58btc (:base info)))))) 192 | -------------------------------------------------------------------------------- /test/multiformats/cid_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.cid-test 2 | (:require 3 | [alphabase.bytes :as b :refer [bytes=]] 4 | [clojure.test :refer [deftest testing is]] 5 | [multiformats.cid :as cid] 6 | [multiformats.hash :as mhash]) 7 | #?(:clj 8 | (:import 9 | clojure.lang.ExceptionInfo))) 10 | 11 | 12 | (deftest constructor-validation 13 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 14 | (cid/create true (mhash/sha1 "hello world"))) 15 | "Unknown codec type should be rejected") 16 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 17 | (cid/create :no-such-codec (mhash/create :sha1 "0beec7b8"))) 18 | "Unknown algorithm keyword should be rejected") 19 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 20 | (cid/create -1 (mhash/sha1 "hello world"))) 21 | "Negative codec code should be rejected") 22 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 23 | (cid/create :cbor nil)) 24 | "Nil hash should be rejected")) 25 | 26 | 27 | (deftest predicate 28 | (is (cid/cid? (cid/create :raw (mhash/sha1 "hello")))) 29 | (is (not (cid/cid? nil))) 30 | (is (not (cid/cid? "foo")))) 31 | 32 | 33 | (deftest value-semantics 34 | (let [mh (mhash/sha1 "hello world") 35 | a (cid/create :raw mh) 36 | b (cid/create 0x51 mh) 37 | b' (cid/create :cbor mh)] 38 | (testing "equality" 39 | (is (= a a) "identical values are equal") 40 | (is (= b b') "values with same code and digest are equal") 41 | (is (not= a b)) 42 | (is (not= a nil)) 43 | (is (not= a "foo"))) 44 | (testing "hashing" 45 | (is (integer? (hash a)) "hash code returns an integer") 46 | (is (= (hash a) (hash a)) "hash code is reflexive") 47 | (is (= (hash b) (hash b')) "equivalent objects return same hashcode")) 48 | (testing "comparison" 49 | (is (zero? (compare a a))) 50 | (is (zero? (compare b b'))) 51 | (is (pos? (compare a b))) 52 | (is (neg? (compare b a))) 53 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) #"Cannot compare CID value to" 54 | (compare a "foo")))))) 55 | 56 | 57 | (deftest cid-properties 58 | (let [mh (mhash/sha2-256 "test input") 59 | cid (cid/create :cbor mh)] 60 | (is (= 36 (:length cid))) 61 | (is (= 1 (:version cid))) 62 | (is (= :cbor (:codec cid))) 63 | (is (= 0x51 (:code cid))) 64 | (is (= :sha2-256 (get-in cid [:hash :algorithm]))) 65 | (is (nil? (:foo cid))) 66 | (is (= {:length 36 67 | :version 1 68 | :codec :cbor 69 | :code 0x51 70 | :hash mh} 71 | (cid/inspect cid))) 72 | (is (nil? (cid/inspect nil))))) 73 | 74 | 75 | (deftest cid-rendering 76 | (is (= "cidv1:raw:sha1:0beec7b5ea3f0fdb" 77 | (str (cid/create :raw (mhash/create :sha1 "0beec7b5ea3f0fdb"))))) 78 | (is (= "cidv1:cbor:sha2-256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 79 | (str (cid/create :cbor (mhash/create :sha2-256 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")))))) 80 | 81 | 82 | (deftest exercise-metadata 83 | (let [a (cid/create :raw (mhash/sha2-256 "abc123")) 84 | a' (vary-meta a assoc :foo :bar/baz)] 85 | (is (empty? (meta a)) "values start with empty metadata") 86 | (is (= :bar/baz (:foo (meta a'))) "metadata can be associated with value") 87 | (is (= a a') "metadata does not affect equality"))) 88 | 89 | 90 | (deftest binary-serialization 91 | (testing "v0" 92 | (let [mh (mhash/sha2-256 "hello world") 93 | encoded (mhash/encode mh) 94 | cid (cid/decode encoded)] 95 | (is (= 0 (:version cid))) 96 | (is (= :raw (:codec cid))) 97 | (is (= mh (:hash cid))) 98 | (is (bytes= encoded (cid/encode cid))) 99 | (let [buffer (b/byte-array (+ 4 (:length cid)))] 100 | (is (= (:length cid) (cid/write-bytes cid buffer 2))) 101 | (is (= [0x00 0x00 0x12 0x20] 102 | (take 4 (b/byte-seq buffer))))))) 103 | (testing "v1" 104 | (let [mh (mhash/sha2-256 "hello world") 105 | cid (cid/create :cbor mh)] 106 | (is (= 1 (:version cid))) 107 | (is (= :cbor (:codec cid))) 108 | (is (= mh (:hash cid))) 109 | (let [encoded (cid/encode cid)] 110 | (is (= (alength encoded) (:length cid))) 111 | (is (= cid (cid/decode encoded)))) 112 | (let [buffer (b/byte-array (+ 4 (:length cid)))] 113 | (is (= (:length cid) (cid/write-bytes cid buffer 1))) 114 | (is (= [0x00 0x01 0x51 0x12 0x20] 115 | (take 5 (b/byte-seq buffer))))))) 116 | (testing "bad input" 117 | (let [encoded (doto (b/byte-array 34) 118 | (b/set-byte 0 0x02))] 119 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) 120 | #"Unable to decode CID version 2" 121 | (cid/decode encoded)))))) 122 | 123 | 124 | (deftest string-serialization 125 | (testing "v0" 126 | (let [b58 "Qmd8kgzaFLGYtTS1zfF37qKGgYQd5yKcQMyBeSa8UkUz4W" 127 | mh (mhash/sha2-256 "foo bar baz") 128 | encoded (mhash/encode mh) 129 | cid (cid/decode encoded)] 130 | (is (= 34 (:length cid))) 131 | (is (= 0 (:version cid))) 132 | (is (= :raw (:codec cid))) 133 | (is (= b58 (cid/format cid))) 134 | (is (= cid (cid/parse b58))) 135 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) 136 | #"v0 CID values cannot be formatted in alternative bases" 137 | (cid/format :base32 cid))) 138 | (is (= {:length 34 139 | :version 0 140 | :codec :raw 141 | :code 0x55 142 | :hash mh} 143 | (cid/inspect cid))) 144 | (is (= {:length 34 145 | :version 0 146 | :codec :raw 147 | :code 0x55 148 | :hash mh} 149 | (cid/inspect encoded))) 150 | (is (= {:length 34 151 | :version 0 152 | :codec :raw 153 | :code 0x55 154 | :hash mh} 155 | (cid/inspect b58))))) 156 | (testing "v1" 157 | (let [b58 "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA" 158 | b32 "bafkreidon73zkcrwdb5iafqtijxildoonbwnpv7dyd6ef3qdgads2jc4su" 159 | cid (cid/parse b58) 160 | mh (:hash cid)] 161 | (is (= 36 (:length cid))) 162 | (is (= 1 (:version cid))) 163 | (is (= :raw (:codec cid))) 164 | (is (= b58 (cid/format :base58btc cid))) 165 | (is (= b32 (cid/format cid))) 166 | (is (= cid (cid/parse b32))) 167 | (is (= {:length 36 168 | :version 1 169 | :codec :raw 170 | :code 0x55 171 | :hash mh 172 | :prefix "b" 173 | :base :base32} 174 | (cid/inspect b32))) 175 | (is (= {:length 36 176 | :version 1 177 | :codec :raw 178 | :code 0x55 179 | :hash mh 180 | :prefix "z" 181 | :base :base58btc} 182 | (cid/inspect b58)))))) 183 | -------------------------------------------------------------------------------- /test/multiformats/codec_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.codec-test 2 | (:require 3 | [clojure.test :refer [deftest testing is]] 4 | [multiformats.codec :as mcodec])) 5 | 6 | 7 | (deftest codec-registration 8 | (testing "registration" 9 | (is (nil? (mcodec/unregister! :baz))) 10 | (is (nil? (mcodec/key->code :baz))) 11 | (is (nil? (mcodec/register! :baz 0x1234))) 12 | (is (= 0x1234 (mcodec/key->code :baz))) 13 | (is (= 0x1234 (mcodec/resolve-code :baz)))) 14 | (testing "checks" 15 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 16 | #"Invalid arguments" 17 | (mcodec/register! 0 :foo))) 18 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 19 | #"Codec cbor is already registered with code 81" 20 | (mcodec/register! :cbor 0x1234))) 21 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 22 | #"Code 81 is already registered by codec cbor" 23 | (mcodec/register! :foo 0x51)))) 24 | (testing "unregistration" 25 | (is (nil? (mcodec/unregister! :baz))) 26 | (is (nil? (mcodec/unregister! :baz)) 27 | "repeat unregistration should succeed") 28 | (is (nil? (mcodec/key->code :baz))) 29 | (is (nil? (mcodec/register! :baz 0x1234))))) 30 | 31 | 32 | (deftest resolution 33 | (testing "resolve-key" 34 | (is (= :raw (mcodec/resolve-key 0x55))) 35 | (is (= :cbor (mcodec/resolve-key :cbor))) 36 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 37 | #"codes cannot be negative" 38 | (mcodec/resolve-key -1))) 39 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 40 | #":foo does not map to a known multicodec" 41 | (mcodec/resolve-key :foo))) 42 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 43 | #"is not a valid codec keyword or numeric code" 44 | (mcodec/resolve-key "abc")))) 45 | (testing "resolve-code" 46 | (is (= 0x55 (mcodec/resolve-code 0x55))) 47 | (is (= 0x51 (mcodec/resolve-code :cbor))) 48 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 49 | #"codes cannot be negative" 50 | (mcodec/resolve-code -1))) 51 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 52 | #":foo does not map to a known multicodec" 53 | (mcodec/resolve-code :foo))) 54 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 55 | #"is not a valid codec keyword or numeric code" 56 | (mcodec/resolve-code "abc"))))) 57 | -------------------------------------------------------------------------------- /test/multiformats/hash_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.hash-test 2 | (:require 3 | [alphabase.bytes :as b :refer [bytes=]] 4 | [clojure.test :refer [deftest testing is]] 5 | [multiformats.hash :as mhash]) 6 | #?(:clj 7 | (:import 8 | clojure.lang.ExceptionInfo 9 | java.io.ByteArrayInputStream 10 | java.nio.ByteBuffer))) 11 | 12 | 13 | (deftest constructor-validation 14 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 15 | (mhash/create true "0beec7b8")) 16 | "Unknown algorithm type should be rejected") 17 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 18 | (mhash/create :no-such-algo "0beec7b8")) 19 | "Unknown algorithm keyword should be rejected") 20 | (is (thrown? #?(:clj ExceptionInfo, :cljs js/Error) 21 | (mhash/create -1 "0beec7b8")) 22 | "Negative algorithm code should be rejected") 23 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) #"nil is not a byte array or hex string" 24 | (mhash/create :sha1 nil)) 25 | "Nil digest should be rejected") 26 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) #"Cannot encode a multihash with an empty digest" 27 | (mhash/create 0x11 (b/byte-array 0))) 28 | "Empty digest should be rejected")) 29 | 30 | 31 | (deftest predicate 32 | (is (mhash/multihash? (mhash/create :sha1 "deadbeef"))) 33 | (is (not (mhash/multihash? nil))) 34 | (is (not (mhash/multihash? "foo")))) 35 | 36 | 37 | #?(:bb nil 38 | :default 39 | (deftest value-semantics 40 | (let [a (mhash/create 0x11 "0beec7b8") 41 | b (mhash/create 0x11 "94a1be0c") 42 | c (mhash/create 0x12 "00a8b94e") 43 | c' (mhash/create 0x12 (b/init-bytes [0x00 0xa8 0xb9 0x4e]))] 44 | (testing "equality" 45 | (is (= a a) "identical values are equal") 46 | (is (= c c') "values with same code and digest are equal") 47 | (is (not= a b)) 48 | (is (not= a nil)) 49 | (is (not= a "foo"))) 50 | (testing "hashing" 51 | (is (integer? (hash b)) "hash code returns an integer") 52 | (is (= (hash a) (hash a)) "hash code should be reflexive") 53 | (is (= (hash c) (hash c')) "equivalent objects return same hashcode")) 54 | #?(:bb nil 55 | :default 56 | (testing "comparison" 57 | (is (= [a b c] (sort [c b a])) "Multihashes sort in code/digest order") 58 | (is (zero? (compare a a))) 59 | (is (pos? (compare c a))) 60 | (is (neg? (compare a b))) 61 | (is (thrown-with-msg? #?(:clj ExceptionInfo, :cljs js/Error) #"Cannot compare multihash value to" 62 | (compare a "foo")))))))) 63 | 64 | 65 | (deftest multihash-rendering 66 | (is (= "hash:sha1:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" 67 | (str (mhash/create :sha1 "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")))) 68 | (is (= "hash:sha2-256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 69 | (str (mhash/create :sha2-256 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")))) 70 | (is (= "hash:sha1:ea347f3c5b8f0fd07b5bc95d0beecdda3c275da3" 71 | (str (mhash/create :sha1 "ea347f3c5b8f0fd07b5bc95d0beecdda3c275da3"))))) 72 | 73 | 74 | (deftest exercise-metadata 75 | (let [a (mhash/create :sha1 "dbc95275da8a3d0d0beeea3f0fd47f3cc7b55bc3") 76 | a' (vary-meta a assoc :foo :bar/baz)] 77 | (is (empty? (meta a)) "values start with empty metadata") 78 | (is (= :bar/baz (:foo (meta a'))) "metadata can be associated with value") 79 | (is (= a a') "metadata does not affect equality"))) 80 | 81 | 82 | (def examples 83 | "Test case examples." 84 | {"11140beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" 85 | [0x11 :sha1 160 "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"] 86 | 87 | "11040beec7b8" 88 | [0x11 :sha1 32 "0beec7b8"] 89 | 90 | "12202c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 91 | [0x12 :sha2-256 256 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"] 92 | 93 | "12042c26b46b" 94 | [0x12 :sha2-256 32 "2c26b46b"] 95 | 96 | "22040006b46b" 97 | [0x22 :murmur3 32 "0006b46b"]}) 98 | 99 | 100 | (deftest example-coding 101 | (testing "Encoding is reflexive" 102 | (let [mhash (mhash/create 0x02 "0beec7b8") 103 | encoded (mhash/encode mhash)] 104 | (is (= 6 (:length mhash) (alength encoded))) 105 | #?(:bb nil 106 | :default (is (= mhash (mhash/decode encoded)))))) 107 | (testing "buffer writes" 108 | (let [mhash (mhash/create :sha1 "deadbeef") 109 | buffer (b/byte-array (+ 4 (:length mhash)))] 110 | (is (= 6 (mhash/write-bytes mhash buffer 2))) 111 | (is (bytes= (b/init-bytes [0x00 0x00 0x11 0x04 0xde 0xad 0xbe 0xef 0x00 0x00]) 112 | buffer)))) 113 | (doseq [[hex [code algorithm bits digest]] examples] 114 | (let [mhash (mhash/create algorithm digest)] 115 | (is (= (/ (count hex) 2) (:length mhash))) 116 | (is (= code (:code mhash))) 117 | (is (= algorithm (:algorithm mhash))) 118 | (is (= bits (:bits mhash))) 119 | (is (= digest (:digest mhash))) 120 | (is (= hex (mhash/hex mhash)) 121 | "Encoded multihashes match expected hex") 122 | (is (= :not-found (:foo mhash :not-found))) 123 | #?(:bb nil 124 | :default 125 | (is (= mhash (mhash/parse hex)) 126 | "Hex parses back to multihash"))))) 127 | 128 | 129 | (deftest hashing-constructors 130 | (doseq [[algorithm hash-fn] mhash/functions] 131 | (testing (str (name algorithm) " hashing") 132 | (let [content "foo bar baz" 133 | cbytes (b/from-string content) 134 | mh1 (hash-fn content) 135 | mh2 (hash-fn cbytes) 136 | mh3 #?(:clj (hash-fn (ByteBuffer/wrap (.getBytes content))) 137 | :cljs mh1) 138 | mh4 #?(:clj (hash-fn (ByteArrayInputStream. (.getBytes content))) 139 | :cljs mh1)] 140 | (is (= algorithm 141 | (:algorithm mh1) 142 | (:algorithm mh2) 143 | (:algorithm mh3) 144 | (:algorithm mh4)) 145 | "Constructed multihash algorithms match") 146 | (is (= (:digest mh1) 147 | (:digest mh2) 148 | (:digest mh3) 149 | (:digest mh4)) 150 | "Constructed multihash digests match") 151 | (is (thrown? #?(:clj Exception, :cljs js/Error) 152 | (hash-fn 123))))))) 153 | 154 | 155 | (deftest content-validation 156 | (let [content "baz bar foo" 157 | mhash (mhash/sha1 content)] 158 | (is (nil? (mhash/test nil nil))) 159 | (is (nil? (mhash/test nil content))) 160 | (is (nil? (mhash/test mhash nil))) 161 | (is (true? (mhash/test mhash content)) 162 | "Correct multihash returns true") 163 | (is (false? (mhash/test 164 | (mhash/create :sha1 "68a9f54521a5501230e9dc73") 165 | content)) 166 | "Incorrect multihash returns false") 167 | (is (thrown-with-msg? #?(:clj clojure.lang.ExceptionInfo 168 | :cljs js/Error) 169 | #"No supported hashing function for algorithm :shake-128" 170 | (mhash/test 171 | (mhash/create :shake-128 "68a9f54521a5501230e9dc73") 172 | content)) 173 | "Unsupported hash function cannot be validated"))) 174 | -------------------------------------------------------------------------------- /test/multiformats/varint_test.cljc: -------------------------------------------------------------------------------- 1 | (ns multiformats.varint-test 2 | (:require 3 | [alphabase.bytes :as b] 4 | [clojure.test :refer [deftest testing is]] 5 | [multiformats.varint :as varint])) 6 | 7 | 8 | (deftest varint-coding 9 | (let [examples {0 [0x00] 10 | 1 [0x01] 11 | 127 [0x7F] 12 | 128 [0x80 0x01] 13 | 129 [0x81 0x01] 14 | 255 [0xFF 0x01] 15 | 256 [0x80 0x02] 16 | 257 [0x81 0x02] 17 | 300 [0xAC 0x02] 18 | 16384 [0x80 0x80 0x01]}] 19 | (doseq [[n bs] examples] 20 | (is (= bs (b/byte-seq (varint/encode n)))) 21 | (is (= n (varint/decode (b/init-bytes bs))))))) 22 | 23 | 24 | (deftest edge-cases 25 | (testing "encoding" 26 | (testing "negative value" 27 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 28 | #"cannot be negative" 29 | (varint/encode -1)))) 30 | #_ ; TODO: doesn't work since bit shifts on a bigint fail 31 | (testing "varint larger than nine bytes" 32 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 33 | #"larger than nine bytes" 34 | (varint/encode 9223372036854775808)))) 35 | (testing "out of buffer bounds" 36 | (let [buffer (b/byte-array 1)] 37 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 38 | #"write index out of bounds" 39 | (varint/write-bytes 255 buffer 0)))))) 40 | (testing "decoding" 41 | (testing "varint larger than nine bytes" 42 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 43 | #"larger than nine bytes" 44 | (varint/decode (b/init-bytes [0x80 0x80 0x80 0x80 0x80 45 | 0x80 0x80 0x80 0x80 0x01]))))) 46 | (testing "out of buffer bounds" 47 | (is (thrown-with-msg? #?(:clj Exception, :cljs js/Error) 48 | #"out of bytes to decode" 49 | (varint/decode (b/init-bytes [0x80 0x81]))))))) 50 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit} 3 | {:id :unit-cljs 4 | :type :kaocha.type/cljs 5 | :cljs/compiler-options {:output-dir "target/cljs/out"}}]} 6 | --------------------------------------------------------------------------------