Skip to content

Commit 5320d4d

Browse files
committed
add truncated digest length to encoding and decoding
- jbenet's rationale: multiformats/multihash#1 (comment)
1 parent 74797ca commit 5320d4d

File tree

1 file changed

+55
-22
lines changed

1 file changed

+55
-22
lines changed

lib/multihash.ex

+55-22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ defmodule Multihash do
1919

2020
@type on_decode :: {:ok, t} | error
2121

22+
@type integer_default :: integer | :default
23+
2224
@hash_info [
2325
sha1: [code: 0x11, length: 20],
2426
sha2_256: [code: 0x12, length: 32],
@@ -41,7 +43,8 @@ defmodule Multihash do
4143
# Error strings
4244
@error_invalid_digest_hash "Invalid digest or hash"
4345
@error_invalid_multihash "Invalid multihash"
44-
@error_invalid_length "Invalid length of provided hash function"
46+
@error_invalid_length "Invalid length"
47+
@error_invalid_trunc_length "Invalid truncation length is longer than digest"
4548
@error_invalid_size "Invalid size"
4649
@error_invalid_hash_function "Invalid hash function"
4750
@error_invalid_hash_code "Invalid hash code"
@@ -65,31 +68,43 @@ defmodule Multihash do
6568
iex> Multihash.encode(0x20, :crypto.hash(:sha, "Hello"))
6669
{:error, "Invalid hash code"}
6770
71+
It's possible to [truncate a digest](https://github.com/jbenet/multihash/issues/1#issuecomment-91783612)
72+
by passing an optional `length` parameter. Passing a `length` longer than the default digest length
73+
of the hash function will return an error.
74+
75+
iex> Multihash.encode(:sha1, :crypto.hash(:sha, "Hello"), 10)
76+
{:ok, <<17, 10, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>}
77+
78+
iex> Multihash.encode(:sha1, :crypto.hash(:sha, "Hello"), 30)
79+
{:error, "Invalid truncation length is longer than digest"}
80+
6881
"""
69-
@spec encode(integer, binary) :: on_encode
70-
def encode(hash_code, digest) when is_number(hash_code) and is_binary(digest), do:
71-
Monad.Error.p({:ok, <<hash_code>>} |> encode(digest))
82+
def encode(hash_code, digest, length \\ :default)
7283

73-
@spec encode(binary, binary) :: on_encode
74-
def encode(<<_hash_code>> = hash_code, digest) when is_binary(digest) do
84+
@spec encode(integer, binary, integer_default) :: on_encode
85+
def encode(hash_code, digest, length) when is_number(hash_code) and is_binary(digest), do:
86+
Monad.Error.p({:ok, <<hash_code>>} |> encode(digest, length))
87+
88+
@spec encode(binary, binary, integer_default) :: on_encode
89+
def encode(<<_hash_code>> = hash_code, digest, length) when is_binary(digest) do
7590
Monad.Error.p do
7691
{:ok, hash_code}
7792
|> get_hash_function
78-
|> encode(digest)
93+
|> encode(digest, length)
7994
end
8095
end
8196

82-
@spec encode(hash_type, binary) :: on_encode
83-
def encode(hash_func, digest) when is_atom(hash_func) and is_binary(digest) do
97+
@spec encode(hash_type, binary, integer_default) :: on_encode
98+
def encode(hash_func, digest, length) when is_atom(hash_func) and is_binary(digest) do
8499
Monad.Error.p do
85100
{:ok, hash_func}
86101
|> get_hash_info
87102
|> check_digest_length(digest)
88-
|> encode_internal(digest)
103+
|> encode_internal(digest, length)
89104
end
90105
end
91106

92-
def encode(_digest,_hash_code), do: {:error, @error_invalid_digest_hash}
107+
def encode(_digest,_hash_code, _length), do: {:error, @error_invalid_digest_hash}
93108

94109
@doc ~S"""
95110
Decode the provided multi hash to %Multihash{code: , name: , length: , digest: }
@@ -102,6 +117,10 @@ defmodule Multihash do
102117
iex> Multihash.decode(<<17, 10, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147, 90, 93, 120, 94, 12, 197, 217, 208, 171, 240>>)
103118
{:ok, %Multihash{name: :sha1, code: 17, length: 10, digest: <<247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>}}
104119
120+
iex> Multihash.decode(<<17, 10, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>)
121+
{:ok, %Multihash{name: "sha1", code: 17, length: 10, digest: <<247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>}}
122+
123+
105124
Invalid multihash will result in errors
106125
107126
iex> Multihash.decode(<<17, 20, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147, 90, 93, 120, 94, 12, 197, 217, 208, 171>>)
@@ -124,8 +143,8 @@ defmodule Multihash do
124143
|> get_hash_function
125144
|> get_hash_info
126145
|> check_length(length)
127-
|> check_digest_length(digest)
128-
|> decode_internal(digest)
146+
|> check_truncated_digest_length(digest, length)
147+
|> decode_internal(digest, length)
129148
end
130149
end
131150

@@ -176,16 +195,20 @@ defmodule Multihash do
176195
defp is_valid_hash_code({:error, _}), do: false
177196

178197
@doc """
179-
Encode the digest to multihash
198+
Encode the `digest` to multihash, truncating it to the `trunc_length` if necessary
180199
"""
181-
defp encode_internal([code: code, length: length], <<digest::binary>>) do
182-
Monad.Error.return <<code, length>> <> digest
200+
defp encode_internal([code: code, length: length], <<digest::binary>>, trunc_length) do
201+
case trunc_length do
202+
:default -> Monad.Error.return <<code, length>> <> digest
203+
l when 0 < l and l <= length -> Monad.Error.return <<code, l>> <> Kernel.binary_part(digest, 0, l)
204+
_ -> Monad.Error.fail @error_invalid_trunc_length
205+
end
183206
end
184207

185208
@doc """
186209
Decode the multihash to %Multihash{name, code, length, digest} structure
187210
"""
188-
defp decode_internal([code: code, length: length], <<digest::binary>>) do
211+
defp decode_internal([code: code, length: _default_length], <<digest::binary>>, length) do
189212
{:ok, name} = get_hash_function <<code>>
190213
Monad.Error.return %Multihash{
191214
name: to_string(name) |> String.replace("_", "-") |> String.to_atom,
@@ -202,25 +225,35 @@ defmodule Multihash do
202225
defp check_hash_code(false, _), do: Monad.Error.fail @error_invalid_hash_code
203226

204227
@doc """
205-
Checks that the `original_lenght` is same as the expected `length` of the hash function
228+
Checks if the incoming multihash has a `length` field equal or lower than the `default_length` of the hash function
206229
"""
207-
defp check_length([code: _code, length: length] = hash_info, original_length) do
230+
defp check_length([code: _code, length: default_length] = hash_info, original_length) do
208231
case original_length do
209-
^length -> Monad.Error.return hash_info
232+
l when 0 < l and l <= default_length -> Monad.Error.return hash_info
210233
_ -> Monad.Error.fail @error_invalid_length
211234
end
212235
end
213236

214237
@doc """
215-
Checks if the length of the `digest` is same as the expected `length` of the has function
238+
Checks if the incoming multihash has a `length` field fitting the actual size of the possibly truncated `digest`
216239
"""
217-
defp check_digest_length([code: _code, length: length] = hash_info, digest) when is_binary(digest) do
240+
defp check_truncated_digest_length([code: _code, length: _default_length] = hash_info, digest, length) when is_binary(digest) do
218241
case byte_size(digest) do
219242
^length -> Monad.Error.return hash_info
220243
_ -> Monad.Error.fail @error_invalid_size
221244
end
222245
end
223246

247+
@doc """
248+
Checks if the length of the `digest` is same as the expected `default_length` of the hash function while encoding
249+
"""
250+
defp check_digest_length([code: _code, length: default_length] = hash_info, digest) when is_binary(digest) do
251+
case byte_size(digest) do
252+
^default_length -> Monad.Error.return hash_info
253+
_ -> Monad.Error.fail @error_invalid_size
254+
end
255+
end
256+
224257
@doc """
225258
Get hash info from the @hash_info keyword map based on the provided `hash_func`
226259
"""

0 commit comments

Comments
 (0)