defmodule CID do @moduledoc """ CID - self-describing content-addressed identifiers for distributed systems. ## Overview Elixir version of [CID](https://github.com/ipld/cid). CID is currently being used as part of [IPFS](https://ipfs.io/) for identifying distributed content. > Self-describing content-addressed identifiers for distributed systems This module represents a CID as a CID struct, consisting of the following fields: * `multihash` - An encoded multihash, using `Multihash.encode/3` for example. * `version` - The CID version * `codec` - The Multicodec to use as part of CID The following are building blocks of this CID implementation (see Protocol Description): * `Multihash` - hashing content with a tagged identifier according to popular hashing algorithms, ex: `:sha2_256` * `Multicodec` - tagging content using a given `codec` * `Multibase` - tagged content encoding using a given `encoding_id` A CID struct is created by: * Multihashing some data according to a hashing algorithm such as `:sha2_256` * Selecting a `codec` name from `Multicodec` to describe the content * Selecting a `CID` version, usually the default (CID v1), but other versions may be selected less than the current version (ex: CID v0). ## Features The following functionality is provided in this module: * Encode/Decode CIDs * Encode a CID to a string * Encode a CID buffer to later Multibase encode * Decode a CID string to a CID and optionally, the Multibase encoding used * Create human readable CIDs for debugging and checking * Error handling and exception versions of most major API functions * Current support for all Multibase and Multicodec encodings * Elixir struct for CID data * Easy comparisons, construction, validation, etc. * Send over the wire * Consistent API * Support for CID v0 and CID v1 ## How does it work? - Protocol Description CID is a self-describing content-addressed identifier. It uses cryptographic hashes to achieve content addressing. It uses several [multiformats](https://github.com/multiformats/multiformats) to achieve flexible self-description, namely [multihash](https://github.com/multiformats/multihash) for hashes, [multicodec](https://github.com/multiformats/multicodec) for data content types, and [multibase](https://github.com/multiformats/multibase) to encode the CID itself into strings. Current version: CIDv1 A CIDv1 has four parts: ```sh ::= # or, expanded: ::= ``` Where - `` is a [multibase](https://github.com/multiformats/multibase) code (1 or 2 bytes), to ease encoding CIDs into various bases. - `` is a [varint](https://github.com/multiformats/unsigned-varint) representing the version of CID, here for upgradability purposes. - `` is a [multicodec](https://github.com/multiformats/multicodec) code representing the content type or format of the data being addressed. - `` is a [multihash](https://github.com/multiformats/multihash) value, representing the cryptographic hash of the content being addressed. Multihash enables CIDs to use many different cryptographic hash function, for upgradability and protocol agility purposes. ## Human Readable CIDs It is advantageous to have a human readable description of a CID, solely for the purposes of debugging and explanation. We can easily transform a CID to a "Human Readable CID" as follows: ``` ::= "-" "-" "-" ``` Where each sub-component is represented with its own human-readable form: - `` is a human-readable multibase code (eg `base58btc`) - `` is the string `cidv#` (eg `cidv1` or `cidv2`) - `` is a human-readable multicodec code (eg `cbor`) - `` is a human-readable multihash (eg `sha2-256-256-abcdef0123456789...`) This module provides a human readable CID function via 'humanize/2'. ## Versions There are currently 2 active versions of CID, v0 and v1, each with different encoding and decoding algorithms. A CID v0 and CID v1 are not equal. This module intentionally avoids assumptions about future versions as a version change may necessitate dramatic encoding and decoding changes. As such, unknown versions will return an exception or error. ## General Usage Given a CID struct, it can be encoded by selecting an `encoding_id` from `Multibase`. For CID v0, only `:base58_btc` may be used. For CID v1, any base supported by Multibase is valid, for example `:base32_z`. Encoding a struct will produce a binary - a CID string representation. If you need to see the bytes, the string is just an Elixir binary and can be handled accordingly. Decoding a CID string is straight forward using `decode/1`, `decode!/1`, `decode_cid/1` or `decode_cid!/1`. Decoding only requires a CID string because all decode information is embedded. Inspecting a CID string for debugging or other purposes can be done by using `humanize/2`. It takes a custom separator to allow you to format the pieces to your liking. Decoding a CID back to a struct also is still very legible. `decode/1` offers additional information - the `Multibase` `encoding_id` that was used to encode the CID, while `decode_cid!/1` is just the bare CID struct. """ alias __MODULE__, as: CID @enforce_keys [:version, :codec, :multihash] defstruct [:version, :codec, :multihash] @typedoc """ An encoded CID string. For CID v1: `` For CID v0: `` """ @type cid_string() :: String.t() @typedoc """ A Multihash encoded binary. """ @type multihash_binary() :: binary() @type cid_version() :: 1 | 0 @type t :: %__MODULE__{ version: cid_version(), codec: Multicodec.multi_codec(), multihash: multihash_binary(), } @current_version 1 @v0_codec "dag-pb" @v0_encoding_id :base58_btc # bugs encountering Multihash + with @dialyzer {:no_match, [humanize: 2, do_cid: 3]} @doc """ Creates a new CID. An error is returned if the provided parameters are invalid. * `codec` must correspond to a valid `Multicodec` codec name. * version 0 CIDs are checked for a valid `multihash` to avoid usage issues. * version 0 CIDs can only be created by specifying `dag-pb` (default) as the `codec`. ## Examples iex> CID.cid(<<17, 20, 86, 176, 173, 81, 245, 2, 26, 0, 52, 203, 228, 68, 33, 54, 171, 86, 201, 151, 184, 254>>, "dag-pb", 1) {:ok, %CID{ codec: "dag-pb", multihash: <<17, 20, 86, 176, 173, 81, 245, 2, 26, 0, 52, 203, 228, 68, 33, 54, 171, 86, 201, 151, 184, 254>>, version: 1 }} iex> CID.cid(<<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, "dag-pb", 0) {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, version: 0 }} iex> CID.cid("charcoal grills best", "dag-pb", 0) {:error, "Invalid hash code"} iex> CID.cid(<<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, "dag-json", 0) {:error, :invalid_multicodec} iex> CID.cid(<<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, "dag-json", 1) {:ok, %CID{ codec: "dag-json", multihash: <<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, version: 1 }} """ @spec cid(multihash_binary(), Multicodec.multi_codec(), cid_version()) :: {:ok, t()} | {:error, term()} def cid(multihash, codec \\ @v0_codec, version \\ @current_version) def cid(multihash, codec, version) when is_binary(multihash) and is_binary(codec) and is_integer(version) and version <= @current_version do if Multicodec.codec?(codec) do do_cid(multihash, codec, version) else {:error, :invalid_multicodec} end end def cid(_multihash, _codec, _version) do {:error, :unsupported_version} end @doc """ Creates a new CID. An exception is raised if the provided parameters are invalid. * `codec` must correspond to a valid `Multicodec` codec name. * version 0 CIDs are checked for a valid `multihash` to avoid usage issues. * version 0 CIDs can only be created by specifying `dag-pb` (default) as the `codec`. ## Examples iex> CID.cid!(<<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, "raw") %CID{ codec: "raw", multihash: <<18, 32, 149, 184, 131, 27, 7, 230, 246, 113, 26, 45, 235, 92, 130, 235, 240, 88, 99, 208, 173, 179, 49, 200, 107, 43, 173, 200, 167, 153, 175, 31, 65, 149>>, version: 1 } iex> CID.cid!(<<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, "git-raw", 1) %CID{ codec: "git-raw", multihash: <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, version: 1 } iex> CID.cid!(<<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, "dag-pb", 0) %CID{ codec: "dag-pb", multihash: <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, version: 0 } """ @spec cid!(multihash_binary(), Multicodec.multi_codec(), cid_version()) :: t() def cid!(multihash, codec \\ @v0_codec, version \\ @current_version) do case cid(multihash, codec, version) do {:ok, cid} -> cid {:error, reason} -> raise ArgumentError, "invalid cid data - #{inspect reason}" end end @spec do_cid(multihash_binary(), Multicodec.multi_codec(), cid_version()) :: {:ok, t()} | {:error, term()} defp do_cid(multihash, codec, version) defp do_cid(multihash, @v0_codec, 0) do case Multihash.decode(multihash) do # ensure this hash is valid for CID v0 to avoid later complications when decoding {:ok, %Multihash{length: length}} when length == 32 -> {:ok, %CID{version: 0, codec: @v0_codec, multihash: multihash}} {:ok, %Multihash{}} -> {:error, :invalid_multihash} {:error, _reason} = err -> err end end defp do_cid(multihash, codec, 1) do {:ok, %CID{version: 1, codec: codec, multihash: multihash}} end defp do_cid(_multihash, _codec, 0) do {:error, :invalid_multicodec} end defp do_cid(_multihash, _codec, _version) do {:error, :unsupported_version} end @doc """ Encodes a CID as a Multibase encoded string. Returns an error if the CID is invalid and cannot be encoded. ## Examples iex> CID.encode(%CID{codec: "dag-pb", multihash: <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, version: 0}) {:ok, "Qme2Gc15TFi3XEbU87WT9zXDfAYPJQku8vpUkjFaoZsttQ"} iex> CID.encode(%CID{codec: "dag-json", multihash: <<17, 20, 196, 25, 117, 209, 218, 225, 204, 105, 177, 106, 216, 137, 43, 140, 119, 22, 78, 132, 202, 57>>, version: 1}, :base64_url) {:ok, "uAakCERTEGXXR2uHMabFq2IkrjHcWToTKOQ"} iex> CID.encode(%CID{codec: "dag-json", multihash: <<17, 20, 196, 25, 117, 209, 218, 225, 204, 105, 177, 106, 216, 137, 43, 140, 119, 22, 78, 132, 202, 57>>, version: 1}, :does_not_exist_codec) {:error, :unsupported_encoding} """ @spec encode(t(), Multibase.encoding_id()) :: {:ok, cid_string()} | {:error, term()} def encode(cid, encoding_id \\ @v0_encoding_id) when is_map(cid) and is_atom(encoding_id) do do_encode(cid, encoding_id) end @doc """ Encodes a CID as a Multibase encoded string. Raises an exception if the CID is invalid and cannot be encoded. ## Examples iex> CID.encode!(%CID{codec: "dag-pb", multihash: <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, version: 0}) "Qme2Gc15TFi3XEbU87WT9zXDfAYPJQku8vpUkjFaoZsttQ" iex> CID.encode!( %CID{codec: "dag-json", multihash: <<17, 20, 196, 25, 117, 209, 218, 225, 204, 105, 177, 106, 216, 137, 43, 140, 119, 22, 78, 132, 202, 57>>, version: 1}, :base64_url) "uAakCERTEGXXR2uHMabFq2IkrjHcWToTKOQ" iex> CID.cid!( <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, "dag-pb", 0) |> CID.encode!(:base58_btc) "Qme2Gc15TFi3XEbU87WT9zXDfAYPJQku8vpUkjFaoZsttQ" """ @spec encode!(t(), Multibase.encoding_id()) :: cid_string() def encode!(cid, encoding_id \\ @v0_encoding_id) do case encode(cid, encoding_id) do {:ok, cid_string} -> cid_string {:error, reason} -> raise ArgumentError, "unable to encode cid - #{inspect reason}" end end defp do_encode(%{version: 0} = cid, @v0_encoding_id) do do_encode_buffer(cid) end defp do_encode(%{version: 0}, _encoding_id) do #"CID version 0 may only be encoded with :base58_btc. It does not take a multibase prefix." {:error, :invalid_encoding} end defp do_encode(cid, encoding_id) do with {:ok, encoded_buffer} <- do_encode_buffer(cid) do Multibase.encode(encoded_buffer, encoding_id) end end @doc """ Encodes a CID as a raw buffer to be encoded with Multibase. Raises an error if the CID is invalid and cannot be encoded. ## Examples iex> CID.cid!( <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, "dag-pb", 1) |> CID.encode_buffer() {:ok, <<1, 112, 18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>} iex> CID.cid!( <<18, 32, 233, 5, 138, 177, 152, 246, 144, 143, 112, 33, 17, 176, 192, 251, 91, 54, 249, 157, 0, 85, 69, 33, 136, 108, 64, 226, 137, 27, 52, 157, 199, 161>>, "dag-pb", 0) |> CID.encode_buffer() {:ok, "Qme2Gc15TFi3XEbU87WT9zXDfAYPJQku8vpUkjFaoZsttQ"} """ @spec encode_buffer(t()) :: {:ok, binary()} | {:error, term()} def encode_buffer(cid) when is_map(cid) do do_encode_buffer(cid) end @doc """ Encodes a CID as a raw buffer to be encoded with Multibase. Raises an error if the CID is invalid and cannot be encoded. ## Examples iex> CID.cid!( <<18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>>, "cbor", 1) |> CID.encode_buffer!() <<1, 81, 18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>> iex> CID.cid!( <<18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>>, "dag-pb", 1) |> CID.encode_buffer!() <<1, 112, 18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>> """ def encode_buffer!(cid) do case encode_buffer(cid) do {:ok, encoded_buffer} -> encoded_buffer {:error, reason} -> raise ArgumentError, "unable to encode the given cid - #{inspect reason}" end end defp do_encode_buffer(%{version: 0, codec: @v0_codec, multihash: multihash}) do with encoded_buffer when is_binary(encoded_buffer) <- B58.encode58(multihash) do {:ok, encoded_buffer} else _ -> {:error, "unable to encode v0 buffer."} end end defp do_encode_buffer(%{version: 0}) do {:error, :invalid_multicodec} end defp do_encode_buffer(%{version: version, codec: codec, multihash: multihash}) when version <= @current_version do with {:ok, encoded_multihash} <- Multicodec.encode(multihash, codec), encoded_version = Varint.LEB128.encode(version) do {:ok, <>} else {:error, _reason} = err -> err _ -> {:error, "unable to encode buffer."} end end defp do_encode_buffer(_cid) do {:error, :unsupported_version} end @doc """ Creates a CID by decoding a CID encoded as a string. Returns an error if the encoded CID string is invalid. ## Examples iex> CID.decode_cid("zdj7WZUkfydsNtKVZrSMzSuK6oVUj4CqpBN69q8ZRbUBdQUnC") {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>>, version: 1 }} iex> CID.decode_cid("QmSPWJYa1uQicrYWdFVHSixvrWrm8GmEDqRbtdCoNBqacG") {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 60, 41, 252, 52, 100, 55, 122, 40, 255, 226, 167, 113, 63, 26, 8, 28, 235, 246, 23, 248, 225, 29, 205, 144, 243, 180, 109, 246, 208, 70, 54, 225>>, version: 0 }} iex> CID.decode_cid("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR") {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, 57, 26>>, version: 0 }} """ @spec decode_cid(cid_string()) :: {:ok, t()} | {:error, term()} def decode_cid(cid_string) when is_binary(cid_string) do case do_decode(cid_string) do {:ok, {cid, _encoding_id}} -> {:ok, cid} {:error, _reason} = err -> err end end @doc """ Creates a CID by decoding a CID encoded as a string. Raises an exception if the encoded CID string is invalid. ## Examples iex> CID.decode_cid!("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") %CID{ codec: "dag-pb", multihash: <<18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, 57, 26>>, version: 1 } iex> CID.decode_cid!("zb2rhk6GMPQF3hfzwXTaNYFLKomMeC6UXdUt6jZKPpeVirLtV") %CID{ codec: "raw", multihash: <<18, 32, 199, 208, 20, 137, 8, 8, 88, 197, 0, 6, 88, 54, 198, 88, 248, 71, 166, 202, 103, 196, 134, 70, 25, 33, 43, 228, 248, 32, 14, 75, 186, 206>>, version: 1 } iex> CID.decode_cid!("bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy") %CID{ codec: "raw", multihash: <<18, 32, 199, 208, 20, 137, 8, 8, 88, 197, 0, 6, 88, 54, 198, 88, 248, 71, 166, 202, 103, 196, 134, 70, 25, 33, 43, 228, 248, 32, 14, 75, 186, 206>>, version: 1 } """ @spec decode_cid!(cid_string()) :: t() def decode_cid!(cid_string) do case decode_cid(cid_string) do {:ok, cid} -> cid {:error, reason} -> raise ArgumentError, "invalid CID string - #{inspect reason}" end end @doc """ Creates a CID by decoding a CID encoded as a string, and returns a tuple of the CID and the `encoding_id` used to encode the string with Multibase. Returns an error if the encoded CID string is invalid. ## Examples iex> CID.decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") {:ok, {%CID{ codec: "dag-pb", multihash: <<18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, 57, 26>>, version: 1 }, :base32_lower}} iex> CID.decode("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR") {:ok, {%CID{ codec: "dag-pb", multihash: <<18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, 57, 26>>, version: 0 }, :base58_btc}} iex> CID.decode("FREEPRETEZELSbafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") {:error, "unable to decode CID string"} """ @spec decode(cid_string()) :: {:ok, {t(), Multibase.encoding_id()}} | {:error, term()} def decode(cid_string) when is_binary(cid_string) do do_decode(cid_string) end @doc """ Creates a CID by decoding a CID encoded as a string, and returns a tuple of the CID and the `encoding_id` used to encode the string with Multibase. Raises an exception if the encoded CID string is invalid. ## Examples iex> CID.decode!("zb2rhk6GMPQF3hfzwXTaNYFLKomMeC6UXdUt6jZKPpeVirLtV") {%CID{ codec: "raw", multihash: <<18, 32, 199, 208, 20, 137, 8, 8, 88, 197, 0, 6, 88, 54, 198, 88, 248, 71, 166, 202, 103, 196, 134, 70, 25, 33, 43, 228, 248, 32, 14, 75, 186, 206>>, version: 1 }, :base58_btc} iex> CID.decode!("f015512209d8453505bdc6f269678e16b3e56c2a2948a41f2c792617cc9611ed363c95b63") {%CID{ codec: "raw", multihash: <<18, 32, 157, 132, 83, 80, 91, 220, 111, 38, 150, 120, 225, 107, 62, 86, 194, 162, 148, 138, 65, 242, 199, 146, 97, 124, 201, 97, 30, 211, 99, 201, 91, 99>>, version: 1 }, :base16_lower} iex> CID.decode!("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n") {%CID{ codec: "dag-pb", multihash: <<18, 32, 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85>>, version: 0 }, :base58_btc} """ @spec decode!(cid_string()) :: {t(), Multibase.encoding_id()} def decode!(cid_string) do case decode(cid_string) do {:ok, cid_encoding} -> cid_encoding {:error, reason} -> raise ArgumentError, "invalid CID string - #{inspect reason}" end end @spec do_decode(cid_string()) :: {:ok, {t(), Multibase.encoding_id()}} | {:error, term()} defp do_decode(data) # v0 decoding defp do_decode(<<"Qm", _rest::binary>> = data) when byte_size(data) == 46 do with {:ok, cid_bin} <- B58.decode58(data), {:ok, cid} <- decode_cid_binary(cid_bin) do {:ok, {cid, @v0_encoding_id}} end end # possible v1 decoding defp do_decode(data) do with {:ok, {decoded_bin, multibase_codec}} <- Multibase.codec_decode(data), #TODO: refactor - If the first decoded byte is 0x12, return an error. CIDv0 CIDs may not be multibase encoded and there will be no CIDv18 (0x12 = 18) to prevent ambiguity with decoded CIDv0s false <- match?(<<0x12, _::binary>>, decoded_bin), {:ok, cid} <- decode_cid_binary(decoded_bin) do {:ok, {cid, multibase_codec}} else {:error, _reason} = err -> err _ -> {:error, "unable to decode CID string"} end end @spec decode_cid_binary(binary()) :: {:ok, t()} | {:error, term()} defp decode_cid_binary(cid_bin) defp decode_cid_binary(cid_bin) when byte_size(cid_bin) == 34 do decode_cid_version(cid_bin, 0) end defp decode_cid_binary(cid_bin) do {version, payload} = Varint.LEB128.decode(cid_bin) decode_cid_version(payload, version) end @spec decode_cid_version(binary(), non_neg_integer()) :: {:ok, t()} | {:error, term()} defp decode_cid_version(cid_payload, version) defp decode_cid_version(cid_payload, 0) do {:ok, %CID{ multihash: cid_payload, codec: @v0_codec, version: 0,}} end defp decode_cid_version(cid_payload, 1) do with {:ok, {multihash, codec}} <- Multicodec.codec_decode(cid_payload) do {:ok, %CID{multihash: multihash, codec: codec, version: 1,}} end end defp decode_cid_version(_cid_payload, version) when version > @current_version do #raise ArgumentError, "CID version #{inspect version} is reserved and not yet supported." {:error, :unsupported_version} end defp decode_cid_version(_cid_payload, _version) do #raise ArgumentError, "CID is malformed" {:error, :invalid_cid} end @doc """ Converts a CID to a given destination version. Returns an error if the conversion is not possible. Conversion to a v0 CID is only possible if the codec is `dag-pb`. ## Examples iex> CID.to_version(%CID{codec: "dag-pb", multihash: <<18, 32, 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85>>, version: 0}, 1) {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85>>, version: 1 }} iex> CID.decode_cid!("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") |> CID.to_version(1) {:ok, %CID{ codec: "dag-pb", multihash: <<18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, 57, 26>>, version: 1 }} iex> CID.to_version(%CID{codec: "raw", multihash: <<18, 32, 157, 132, 83, 80, 91, 220, 111, 38, 150, 120, 225, 107, 62, 86, 194, 162, 148, 138, 65, 242, 199, 146, 97, 124, 201, 97, 30, 211, 99, 201, 91, 99>>, version: 1}, 0) {:error, :unsupported_conversion} """ @spec to_version(t(), cid_version()) :: t() def to_version(cid, destination_version) when is_map(cid) and is_integer(destination_version) and destination_version <= @current_version do convert_version(cid, destination_version) end defp convert_version(%{version: version} = cid, destination_version) when version == destination_version do {:ok, cid} end defp convert_version(%{codec: codec}, 0) when codec != @v0_codec do {:error, :unsupported_conversion} end defp convert_version(cid, destination_version) do {:ok, %{cid | version: destination_version}} end @doc ~S""" Returns a human readable CID ## Overview It is advantageous to have a human readable description of a CID, solely for the purposes of debugging and explanation. We can easily transform a CID to a "Human Readable CID" as follows: ``` ::= "-" "-" "-" ``` Where each sub-component is represented with its own human-readable form: - `` is a human-readable multibase code (eg `base58btc`) - `` is the string `cidv#` (eg `cidv1` or `cidv2`) - `` is a human-readable multicodec code (eg `cbor`) - `` is a human-readable multihash (eg `sha2-256-256-abcdef0123456789...`) ## Examples iex> CID.humanize("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") {:ok, "base32_lower - CIDv1 - dag-pb - sha2_256 - c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a"} iex> CID.humanize("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", "-") {:ok, "base32_lower-CIDv1-dag-pb-sha2_256-c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a"} iex> CID.humanize("f015512209d8453505bdc6f269678e16b3e56c2a2948a41f2c792617cc9611ed363c95b63", "::") {:ok, "base16_lower::CIDv1::raw::sha2_256::9d8453505bdc6f269678e16b3e56c2a2948a41f2c792617cc9611ed363c95b63"} iex> CID.humanize("super delicious rolls") {:error, "unable to decode CID string"} """ @spec humanize(cid_string(), String.t()) :: {:ok, String.t()} | {:error, term()} def humanize(cid_string, separator \\ " - ") when is_binary(cid_string) and is_binary(separator) do with true <- String.printable?(separator), {:ok, {%{multihash: multihash, codec: codec, version: version}, encoding_id}} <- do_decode(cid_string), {:ok, %{digest: digest, name: hash_name}} <- Multihash.decode(multihash) do hex_digest = Base.encode16(digest, case: :lower) {:ok, [encoding_id, Enum.join(["CIDv", version]), codec, hash_name, hex_digest] |> Enum.join(separator)} else {:error, _reason} = err -> err false -> {:error, "invalid separator."} _ -> {:error, "unable to decode CID string"} end end @doc """ Checks if a given CID string is a valid encoded CID. ## Examples iex> CID.cid?("uAakCERTEGXXR2uHMabFq2IkrjHcWToTKOQ") true iex> CID.cid?("sweep the leg") false iex> CID.cid?("f015512209d8453505bdc6f269678e16b3e56c2a2948a41f2c792617cc9611ed363c95b63") true iex> CID.cid?("$f015512209d8453505bdc6f269678e16b3e56c2a2948a41f2c792617cc9611ed363c95b63") false """ @spec cid?(cid_string()) :: boolean() def cid?(cid_string) do case decode_cid(cid_string) do {:ok, _cid} -> true _ -> false end end end