Skip to content

Commit f8545c5

Browse files
authored
Merge pull request #113 from filip26/feat/uvarint-write
Add `uvarint.write(...):int` & `Multihash.digestLength(byte[]):long`
2 parents f589e21 + 5b23196 commit f8545c5

4 files changed

Lines changed: 111 additions & 13 deletions

File tree

pom.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>com.apicatalog</groupId>
77
<artifactId>copper-multicodec</artifactId>
8-
<version>2.2.1-SNAPSHOT</version>
8+
<version>2.3.0-SNAPSHOT</version>
99
<packaging>jar</packaging>
1010
<url>https://github.com/filip26/copper-multicodec</url>
1111
<scm>
@@ -28,7 +28,7 @@
2828
<name>Copper Multicodec &amp; Multihash</name>
2929

3030
<description>
31-
Multicodec & Multihash - encoder/decoder for self-describing data formats.
31+
Multicodec &amp; Multihash - encoder/decoder for self-describing data formats.
3232
</description>
3333

3434
<licenses>
@@ -166,7 +166,6 @@
166166
<extensions>true</extensions>
167167
<configuration>
168168
<publishingServerId>central</publishingServerId>
169-
<tokenAuth>true</tokenAuth>
170169
<autoPublish>true</autoPublish>
171170
</configuration>
172171
</plugin>

src/main/java/com/apicatalog/multihash/Multihash.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,68 @@ public byte[] decode(byte[] encoded, int index, int length) {
242242
length + index);
243243
}
244244

245+
/**
246+
* Returns the digest length (in bytes) declared by the given multihash-encoded
247+
* value, starting at offset {@code 0}.
248+
*
249+
* <p>
250+
* This is a convenience method equivalent to {@link #digestLength(byte[], int)
251+
* hashLength(encoded, 0)}.
252+
* </p>
253+
*
254+
* @param encoded the multihash-encoded byte array
255+
* @return the declared digest length, in bytes
256+
*
257+
* @throws NullPointerException if {@code encoded} is {@code null}
258+
* @throws IllegalArgumentException if the input is shorter than
259+
* {@code codeVarint.length + 2} bytes, or if
260+
* the bytes at offset {@code 0} do not start
261+
* with this multihash's code
262+
*/
263+
public long digestLength(byte[] encoded) {
264+
return digestLength(encoded, 0);
265+
}
266+
267+
/**
268+
* Returns the digest length (in bytes) declared by the given multihash-encoded
269+
* value.
270+
*
271+
* <p>
272+
* The method validates that the input begins at {@code index} with this
273+
* multihash's varint code, then decodes the varint length that immediately
274+
* follows the code.
275+
* </p>
276+
*
277+
* @param encoded the multihash-encoded byte array
278+
* @param index the starting offset within {@code encoded} at which the
279+
* multihash begins
280+
* @return the declared digest length, in bytes
281+
*
282+
* @throws NullPointerException if {@code encoded} is {@code null}
283+
* @throws IllegalArgumentException if the input is shorter than
284+
* {@code codeVarint.length + 2} bytes, or if
285+
* the bytes at {@code index} do not start
286+
* with this multihash's code
287+
* @throws IndexOutOfBoundsException if {@code index} is negative or exceeds the
288+
* available range
289+
*/
290+
public long digestLength(byte[] encoded, int index) {
291+
Objects.requireNonNull(encoded);
292+
293+
if (encoded.length < (codeVarint.length + 2)) {
294+
throw new IllegalArgumentException(
295+
"The value to decode must be a non-empty byte array with a minimum length of "
296+
+ (codeVarint.length + 2) + " bytes, but the actual length is " + encoded.length + " bytes.");
297+
}
298+
299+
if (!IntStream.range(0, codeVarint.length).allMatch(i -> codeVarint[i] == encoded[i + index])) {
300+
throw new IllegalArgumentException(
301+
"The provided value is not encoded with this multihash: " + toString() + ".");
302+
}
303+
304+
return UVarInt.decode(encoded, index + codeVarint.length);
305+
}
306+
245307
@Override
246308
public String toString() {
247309
return "Multihash [name=" + name + ", tag=" + tag + ", code=" + code + "]";

src/main/java/com/apicatalog/uvarint/UVarInt.java

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.apicatalog.uvarint;
22

3+
import java.util.Objects;
4+
35
/**
46
* Utility class for encoding and decoding
57
* <a href="https://en.wikipedia.org/wiki/LEB128">unsigned variable-length
@@ -70,17 +72,38 @@ public static final byte[] encode(final long value) {
7072
}
7173

7274
/**
73-
* Writes the UVarInt encoding of the given value into the target array starting
74-
* at the given index.
75+
* Writes the UVarInt-encoded form of the given non-negative value into the
76+
* target array starting at the specified index.
7577
*
76-
* @param value the value to encode
77-
* @param uvarint the target byte array
78-
* @param index the starting index
79-
* @throws IllegalArgumentException if {@code value} cannot be encoded within
80-
* {@link #MAX_VAR_LENGTH} bytes
81-
* @throws NullPointerException if {@code uvarint} is {@code null}
78+
* <p>
79+
* The encoding uses seven data bits per byte. The most significant bit (MSB) of
80+
* each byte is set if more bytes follow. The method returns the number of bytes
81+
* written.
82+
* </p>
83+
*
84+
* @param value the value to encode (must be {@code >= 0})
85+
* @param uvarint the destination byte array
86+
* @param index the starting index in {@code uvarint}
87+
* @return the number of bytes written
88+
*
89+
* @throws NullPointerException if {@code uvarint} is {@code null}
90+
* @throws IllegalArgumentException if {@code value} is negative
91+
* @throws IndexOutOfBoundsException if {@code index} is negative, or if the
92+
* destination array does not have enough
93+
* space to hold the encoding
8294
*/
83-
public static final void write(final long value, final byte[] uvarint, final int index) {
95+
public static final int write(final long value, final byte[] uvarint, final int index) {
96+
97+
Objects.requireNonNull(uvarint, "Target array must not be null.");
98+
99+
if (value < 0) {
100+
throw new IllegalArgumentException("Value must be non-negative.");
101+
}
102+
103+
if (index < 0) {
104+
throw new IndexOutOfBoundsException("Index must be non-negative: " + index + ".");
105+
}
106+
84107
int offset = 0;
85108
long bytes = value;
86109
boolean next = false;
@@ -90,12 +113,20 @@ public static final void write(final long value, final byte[] uvarint, final int
90113
uvarint[offset + index - 1] |= CONTINUE_BIT;
91114
}
92115

116+
if ((index + offset) >= uvarint.length) {
117+
throw new IndexOutOfBoundsException(
118+
"Insufficient space at position " + (index + offset) +
119+
" (array length: " + uvarint.length + ").");
120+
}
121+
93122
uvarint[offset + index] = (byte) (bytes & SEGMENT_BITS);
94123

95124
bytes >>>= 7;
96125
next = bytes != 0;
97126
offset++;
98127
} while (next);
128+
129+
return offset;
99130
}
100131

101132
/**

src/test/java/com/apicatalog/multihash/MultihashTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import org.junit.jupiter.params.provider.Arguments;
1212
import org.junit.jupiter.params.provider.MethodSource;
1313

14-
import com.apicatalog.multicodec.MulticodecDecoder;
1514
import com.apicatalog.multicodec.Multicodec.Tag;
15+
import com.apicatalog.multicodec.MulticodecDecoder;
1616
import com.apicatalog.multicodec.codec.MultihashCodec;
1717

1818
class MultihashTest {
@@ -94,6 +94,12 @@ void testDecoderGet(byte[] input, Multihash expected) {
9494
assertEquals(expected, DECODER.getCodec(input).orElseThrow(IllegalArgumentException::new));
9595
}
9696

97+
@ParameterizedTest(name = "{index}")
98+
@MethodSource("testData")
99+
void testDigestLength(byte[] input, Multihash codec, byte[] expected) {
100+
assertEquals(expected.length, codec.digestLength(input));
101+
}
102+
97103
static Stream<Arguments> testData() {
98104
return Stream.of(
99105
Arguments.of(Base64.getDecoder().decode("ERSIwvEfss45KstbKYbmQCEcRpAHPg=="),

0 commit comments

Comments
 (0)