Skip to content

Commit f373859

Browse files
authored
fix: replace commons-codec Hex with pure-Java impl to fix Android API 24-27 crash (#763)
1 parent 0bac883 commit f373859

File tree

3 files changed

+122
-7
lines changed

3 files changed

+122
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## Pending
4+
- fix: replace `commons-codec` Hex with pure-Java implementation in `Util` to fix `NoSuchMethodError` crash on Android API 24-27.
45
- refactor!: remove `InvokeHostFunctionOperation.createStellarAssetContractOperationBuilder(Address, byte[])`, use `InvokeHostFunctionOperation.createContractOperationBuilder` instead.
56

67
## 2.2.2

src/main/java/org/stellar/sdk/Util.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import java.security.MessageDigest;
77
import java.security.NoSuchAlgorithmException;
88
import java.util.Arrays;
9-
import org.apache.commons.codec.DecoderException;
10-
import org.apache.commons.codec.binary.Hex;
119
import org.stellar.sdk.exception.UnexpectedException;
1210

1311
/**
@@ -23,7 +21,13 @@ public class Util {
2321
* @return hex representation of the byte array (uppercase)
2422
*/
2523
public static String bytesToHex(byte[] bytes) {
26-
return Hex.encodeHexString(bytes, false);
24+
char[] hexChars = new char[bytes.length * 2];
25+
for (int i = 0; i < bytes.length; i++) {
26+
int v = bytes[i] & 0xFF;
27+
hexChars[i * 2] = HEX_DIGITS[v >>> 4];
28+
hexChars[i * 2 + 1] = HEX_DIGITS[v & 0x0F];
29+
}
30+
return new String(hexChars);
2731
}
2832

2933
/**
@@ -34,13 +38,24 @@ public static String bytesToHex(byte[] bytes) {
3438
* @throws IllegalArgumentException if the string contains non-hex characters or has odd length
3539
*/
3640
public static byte[] hexToBytes(String s) {
37-
try {
38-
return Hex.decodeHex(s);
39-
} catch (DecoderException e) {
40-
throw new IllegalArgumentException("Invalid hex string: " + s, e);
41+
int len = s.length();
42+
if (len % 2 != 0) {
43+
throw new IllegalArgumentException("Invalid hex string: " + s);
44+
}
45+
byte[] bytes = new byte[len / 2];
46+
for (int i = 0; i < len; i += 2) {
47+
int high = Character.digit(s.charAt(i), 16);
48+
int low = Character.digit(s.charAt(i + 1), 16);
49+
if (high == -1 || low == -1) {
50+
throw new IllegalArgumentException("Invalid hex string: " + s);
51+
}
52+
bytes[i / 2] = (byte) ((high << 4) | low);
4153
}
54+
return bytes;
4255
}
4356

57+
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
58+
4459
/**
4560
* Returns SHA-256 hash of <code>data</code>.
4661
*
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.stellar.sdk
2+
3+
import io.kotest.assertions.throwables.shouldThrow
4+
import io.kotest.core.spec.style.FunSpec
5+
import io.kotest.matchers.shouldBe
6+
7+
class UtilTest :
8+
FunSpec({
9+
context("bytesToHex") {
10+
test("empty array") { Util.bytesToHex(byteArrayOf()) shouldBe "" }
11+
12+
test("single byte") {
13+
Util.bytesToHex(byteArrayOf(0x00)) shouldBe "00"
14+
Util.bytesToHex(byteArrayOf(0x0A)) shouldBe "0A"
15+
Util.bytesToHex(byteArrayOf(0x7F)) shouldBe "7F"
16+
Util.bytesToHex(byteArrayOf(0x80.toByte())) shouldBe "80"
17+
Util.bytesToHex(byteArrayOf(0xFF.toByte())) shouldBe "FF"
18+
}
19+
20+
test("multiple bytes") {
21+
Util.bytesToHex(byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)) shouldBe "48656C6C6F"
22+
Util.bytesToHex(
23+
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
24+
) shouldBe "DEADBEEF"
25+
Util.bytesToHex(byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04)) shouldBe "0001020304"
26+
}
27+
28+
test("output is uppercase") {
29+
Util.bytesToHex(byteArrayOf(0xAB.toByte(), 0xCD.toByte(), 0xEF.toByte())) shouldBe "ABCDEF"
30+
}
31+
32+
test("all 256 byte values") {
33+
val allBytes = ByteArray(256) { it.toByte() }
34+
val hex = Util.bytesToHex(allBytes)
35+
hex.length shouldBe 512
36+
// spot check a few values
37+
hex.substring(0, 2) shouldBe "00"
38+
hex.substring(2, 4) shouldBe "01"
39+
hex.substring(30, 32) shouldBe "0F"
40+
hex.substring(32, 34) shouldBe "10"
41+
hex.substring(510, 512) shouldBe "FF"
42+
}
43+
}
44+
45+
context("hexToBytes") {
46+
test("empty string") { Util.hexToBytes("") shouldBe byteArrayOf() }
47+
48+
test("single byte") {
49+
Util.hexToBytes("00") shouldBe byteArrayOf(0x00)
50+
Util.hexToBytes("FF") shouldBe byteArrayOf(0xFF.toByte())
51+
Util.hexToBytes("ff") shouldBe byteArrayOf(0xFF.toByte())
52+
Util.hexToBytes("0a") shouldBe byteArrayOf(0x0A)
53+
Util.hexToBytes("0A") shouldBe byteArrayOf(0x0A)
54+
}
55+
56+
test("multiple bytes") {
57+
Util.hexToBytes("48656C6C6F") shouldBe byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)
58+
Util.hexToBytes("DEADBEEF") shouldBe
59+
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
60+
Util.hexToBytes("deadbeef") shouldBe
61+
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
62+
}
63+
64+
test("mixed case") {
65+
Util.hexToBytes("AbCdEf") shouldBe byteArrayOf(0xAB.toByte(), 0xCD.toByte(), 0xEF.toByte())
66+
}
67+
68+
test("odd length throws IllegalArgumentException") {
69+
shouldThrow<IllegalArgumentException> { Util.hexToBytes("ABC") }
70+
shouldThrow<IllegalArgumentException> { Util.hexToBytes("A") }
71+
}
72+
73+
test("invalid characters throw IllegalArgumentException") {
74+
shouldThrow<IllegalArgumentException> { Util.hexToBytes("ZZZZ") }
75+
shouldThrow<IllegalArgumentException> { Util.hexToBytes("GH") }
76+
shouldThrow<IllegalArgumentException> { Util.hexToBytes("0G") }
77+
}
78+
}
79+
80+
context("round trip") {
81+
test("bytesToHex then hexToBytes") {
82+
val original = byteArrayOf(0x00, 0x11, 0x22, 0xAA.toByte(), 0xBB.toByte(), 0xFF.toByte())
83+
val hex = Util.bytesToHex(original)
84+
Util.hexToBytes(hex) shouldBe original
85+
}
86+
87+
test("all 256 byte values round trip") {
88+
val allBytes = ByteArray(256) { it.toByte() }
89+
val hex = Util.bytesToHex(allBytes)
90+
Util.hexToBytes(hex) shouldBe allBytes
91+
}
92+
93+
test("lowercase hex input round trips") {
94+
val hex = "deadbeef0123456789abcdef"
95+
val bytes = Util.hexToBytes(hex)
96+
Util.bytesToHex(bytes).lowercase() shouldBe hex
97+
}
98+
}
99+
})

0 commit comments

Comments
 (0)