Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

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

## 2.2.2
Expand Down
29 changes: 22 additions & 7 deletions src/main/java/org/stellar/sdk/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.stellar.sdk.exception.UnexpectedException;

/**
Expand All @@ -23,7 +21,13 @@ public class Util {
* @return hex representation of the byte array (uppercase)
*/
public static String bytesToHex(byte[] bytes) {
return Hex.encodeHexString(bytes, false);
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_DIGITS[v >>> 4];
hexChars[i * 2 + 1] = HEX_DIGITS[v & 0x0F];
}
return new String(hexChars);
}

/**
Expand All @@ -34,13 +38,24 @@ public static String bytesToHex(byte[] bytes) {
* @throws IllegalArgumentException if the string contains non-hex characters or has odd length
*/
public static byte[] hexToBytes(String s) {
try {
return Hex.decodeHex(s);
} catch (DecoderException e) {
throw new IllegalArgumentException("Invalid hex string: " + s, e);
int len = s.length();
if (len % 2 != 0) {
throw new IllegalArgumentException("Invalid hex string: " + s);
}
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int high = Character.digit(s.charAt(i), 16);
int low = Character.digit(s.charAt(i + 1), 16);
if (high == -1 || low == -1) {
throw new IllegalArgumentException("Invalid hex string: " + s);
}
bytes[i / 2] = (byte) ((high << 4) | low);
}
return bytes;
}

private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();

/**
* Returns SHA-256 hash of <code>data</code>.
*
Expand Down
99 changes: 99 additions & 0 deletions src/test/kotlin/org/stellar/sdk/UtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.stellar.sdk

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class UtilTest :
FunSpec({
context("bytesToHex") {
test("empty array") { Util.bytesToHex(byteArrayOf()) shouldBe "" }

test("single byte") {
Util.bytesToHex(byteArrayOf(0x00)) shouldBe "00"
Util.bytesToHex(byteArrayOf(0x0A)) shouldBe "0A"
Util.bytesToHex(byteArrayOf(0x7F)) shouldBe "7F"
Util.bytesToHex(byteArrayOf(0x80.toByte())) shouldBe "80"
Util.bytesToHex(byteArrayOf(0xFF.toByte())) shouldBe "FF"
}

test("multiple bytes") {
Util.bytesToHex(byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)) shouldBe "48656C6C6F"
Util.bytesToHex(
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
) shouldBe "DEADBEEF"
Util.bytesToHex(byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04)) shouldBe "0001020304"
}

test("output is uppercase") {
Util.bytesToHex(byteArrayOf(0xAB.toByte(), 0xCD.toByte(), 0xEF.toByte())) shouldBe "ABCDEF"
}

test("all 256 byte values") {
val allBytes = ByteArray(256) { it.toByte() }
val hex = Util.bytesToHex(allBytes)
hex.length shouldBe 512
// spot check a few values
hex.substring(0, 2) shouldBe "00"
hex.substring(2, 4) shouldBe "01"
hex.substring(30, 32) shouldBe "0F"
hex.substring(32, 34) shouldBe "10"
hex.substring(510, 512) shouldBe "FF"
}
}

context("hexToBytes") {
test("empty string") { Util.hexToBytes("") shouldBe byteArrayOf() }

test("single byte") {
Util.hexToBytes("00") shouldBe byteArrayOf(0x00)
Util.hexToBytes("FF") shouldBe byteArrayOf(0xFF.toByte())
Util.hexToBytes("ff") shouldBe byteArrayOf(0xFF.toByte())
Util.hexToBytes("0a") shouldBe byteArrayOf(0x0A)
Util.hexToBytes("0A") shouldBe byteArrayOf(0x0A)
}

test("multiple bytes") {
Util.hexToBytes("48656C6C6F") shouldBe byteArrayOf(0x48, 0x65, 0x6C, 0x6C, 0x6F)
Util.hexToBytes("DEADBEEF") shouldBe
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
Util.hexToBytes("deadbeef") shouldBe
byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())
}

test("mixed case") {
Util.hexToBytes("AbCdEf") shouldBe byteArrayOf(0xAB.toByte(), 0xCD.toByte(), 0xEF.toByte())
}

test("odd length throws IllegalArgumentException") {
shouldThrow<IllegalArgumentException> { Util.hexToBytes("ABC") }
shouldThrow<IllegalArgumentException> { Util.hexToBytes("A") }
}

test("invalid characters throw IllegalArgumentException") {
shouldThrow<IllegalArgumentException> { Util.hexToBytes("ZZZZ") }
shouldThrow<IllegalArgumentException> { Util.hexToBytes("GH") }
shouldThrow<IllegalArgumentException> { Util.hexToBytes("0G") }
}
}

context("round trip") {
test("bytesToHex then hexToBytes") {
val original = byteArrayOf(0x00, 0x11, 0x22, 0xAA.toByte(), 0xBB.toByte(), 0xFF.toByte())
val hex = Util.bytesToHex(original)
Util.hexToBytes(hex) shouldBe original
}

test("all 256 byte values round trip") {
val allBytes = ByteArray(256) { it.toByte() }
val hex = Util.bytesToHex(allBytes)
Util.hexToBytes(hex) shouldBe allBytes
}

test("lowercase hex input round trips") {
val hex = "deadbeef0123456789abcdef"
val bytes = Util.hexToBytes(hex)
Util.bytesToHex(bytes).lowercase() shouldBe hex
}
}
})
Loading