Skip to content

Commit b211b65

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

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: }
@@ -99,6 +114,10 @@ defmodule Multihash do
99114
iex> Multihash.decode(<<17, 20, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147, 90, 93, 120, 94, 12, 197, 217, 208, 171, 240>>)
100115
{:ok, %Multihash{name: "sha1", code: 17, length: 20, digest: <<247, 255, 158, 139, 123, 178, 224, 155, 112, 147, 90, 93, 120, 94, 12, 197, 217, 208, 171, 240>>}}
101116
117+
iex> Multihash.decode(<<17, 10, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>)
118+
{:ok, %Multihash{name: "sha1", code: 17, length: 10, digest: <<247, 255, 158, 139, 123, 178, 224, 155, 112, 147>>}}
119+
120+
102121
Invalid multihash will result in errors
103122
104123
iex> Multihash.decode(<<17, 20, 247, 255, 158, 139, 123, 178, 224, 155, 112, 147, 90, 93, 120, 94, 12, 197, 217, 208, 171>>)
@@ -121,8 +140,8 @@ defmodule Multihash do
121140
|> get_hash_function
122141
|> get_hash_info
123142
|> check_length(length)
124-
|> check_digest_length(digest)
125-
|> decode_internal(digest)
143+
|> check_truncated_digest_length(digest, length)
144+
|> decode_internal(digest, length)
126145
end
127146
end
128147

@@ -173,16 +192,20 @@ defmodule Multihash do
173192
defp is_valid_hash_code({:error, _}), do: false
174193

175194
@doc """
176-
Encode the digest to multihash
195+
Encode the `digest` to multihash, truncating it to the `trunc_length` if necessary
177196
"""
178-
defp encode_internal([code: code, length: length], <<digest::binary>>) do
179-
Monad.Error.return <<code, length>> <> digest
197+
defp encode_internal([code: code, length: length], <<digest::binary>>, trunc_length) do
198+
case trunc_length do
199+
:default -> Monad.Error.return <<code, length>> <> digest
200+
l when 0 < l and l <= length -> Monad.Error.return <<code, l>> <> Kernel.binary_part(digest, 0, l)
201+
_ -> Monad.Error.fail @error_invalid_trunc_length
202+
end
180203
end
181204

182205
@doc """
183206
Decode the multihash to %Multihash{name, code, length, digest} structure
184207
"""
185-
defp decode_internal([code: code, length: length], <<digest::binary>>) do
208+
defp decode_internal([code: code, length: _default_length], <<digest::binary>>, length) do
186209
{:ok, name} = get_hash_function <<code>>
187210
Monad.Error.return %Multihash{
188211
name: to_string(name) |> String.replace("_", "-"),
@@ -199,25 +222,35 @@ defmodule Multihash do
199222
defp check_hash_code(false, _), do: Monad.Error.fail @error_invalid_hash_code
200223

201224
@doc """
202-
Checks that the `original_lenght` is same as the expected `length` of the hash function
225+
Checks if the incoming multihash has a `length` field equal or lower than the `default_length` of the hash function
203226
"""
204-
defp check_length([code: _code, length: length] = hash_info, original_length) do
227+
defp check_length([code: _code, length: default_length] = hash_info, original_length) do
205228
case original_length do
206-
^length -> Monad.Error.return hash_info
229+
l when 0 < l and l <= default_length -> Monad.Error.return hash_info
207230
_ -> Monad.Error.fail @error_invalid_length
208231
end
209232
end
210233

211234
@doc """
212-
Checks if the length of the `digest` is same as the expected `length` of the has function
235+
Checks if the incoming multihash has a `length` field fitting the actual size of the possibly truncated `digest`
213236
"""
214-
defp check_digest_length([code: _code, length: length] = hash_info, digest) when is_binary(digest) do
237+
defp check_truncated_digest_length([code: _code, length: _default_length] = hash_info, digest, length) when is_binary(digest) do
215238
case byte_size(digest) do
216239
^length -> Monad.Error.return hash_info
217240
_ -> Monad.Error.fail @error_invalid_size
218241
end
219242
end
220243

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

0 commit comments

Comments
 (0)