feat(applet): canonical CBOR uint/bstr/tstr encoders (M2B.4)

This commit is contained in:
michael
2026-06-17 15:51:30 -07:00
parent 6373bf1109
commit 047503e8c5
2 changed files with 235 additions and 0 deletions

View File

@@ -360,6 +360,150 @@ class StructuralCborTest {
assertEquals(4, span);
}
// ---------------------------------------------------------------------
// Canonical encoder tests (M2B.4) — uint / bstr-header / tstr-header
// writers used by M2D.2 (DeviceResponseBuilder).
//
// Canonical CBOR (RFC 8949 §4.2.1): shortest argument size that fits the
// value. Boundary at each size step (0..23 immediate, 24..0xff one-byte,
// 0x100..0xffff two-byte, 0x10000..0xffffffff four-byte).
// ---------------------------------------------------------------------
@Test
void encodeUint_zeroImmediate() {
// 0 fits in the immediate range — single header byte 0x00.
byte[] out = new byte[8];
short n = StructuralCbor.encodeUint(0, out, (short) 0);
assertEquals(1, n);
assertArrayPrefix(out, "00", n);
}
@Test
void encodeUint_twentyThreeImmediate() {
// 23 is the largest immediate value — header byte 0x17.
byte[] out = new byte[8];
short n = StructuralCbor.encodeUint(23, out, (short) 0);
assertEquals(1, n);
assertArrayPrefix(out, "17", n);
}
@Test
void encodeUint_twentyFourOneByteBoundary() {
// 24 is the lower boundary of the 1-byte argument form — 0x18 0x18.
// Canonical rule forbids encoding 24 as 0x19 0x00 0x18 etc.
byte[] out = new byte[8];
short n = StructuralCbor.encodeUint(24, out, (short) 0);
assertEquals(2, n);
assertArrayPrefix(out, "1818", n);
}
@Test
void encodeUint_twoByteBoundary() {
// 0x100 = 256 is the lower boundary of the 2-byte argument form.
byte[] out = new byte[8];
short n = StructuralCbor.encodeUint(0x100, out, (short) 0);
assertEquals(3, n);
assertArrayPrefix(out, "190100", n);
}
@Test
void encodeUint_fourByteBoundary() {
// 0x10000 = 65536 is the lower boundary of the 4-byte argument form.
byte[] out = new byte[8];
short n = StructuralCbor.encodeUint(0x10000, out, (short) 0);
assertEquals(5, n);
assertArrayPrefix(out, "1a00010000", n);
}
@Test
void encodeUint_negativeRejected() {
// Aliro mdoc CBOR never uses negative integers (those are major type 1).
byte[] out = new byte[8];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.encodeUint(-1, out, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void encodeBstrHeader_zero() {
// bstr len 0 — header byte 0x40 (major type 2 | additional info 0).
byte[] out = new byte[8];
short n = StructuralCbor.encodeBstrHeader((short) 0, out, (short) 0);
assertEquals(1, n);
assertArrayPrefix(out, "40", n);
}
@Test
void encodeBstrHeader_twentyThreeImmediate() {
// Largest immediate-length bstr header — 0x57.
byte[] out = new byte[8];
short n = StructuralCbor.encodeBstrHeader((short) 23, out, (short) 0);
assertEquals(1, n);
assertArrayPrefix(out, "57", n);
}
@Test
void encodeBstrHeader_twentyFourOneByteBoundary() {
// 24 forces into 1-byte argument form — 0x58 0x18.
byte[] out = new byte[8];
short n = StructuralCbor.encodeBstrHeader((short) 24, out, (short) 0);
assertEquals(2, n);
assertArrayPrefix(out, "5818", n);
}
@Test
void encodeBstrHeader_twoByteBoundary() {
// 0x100 = 256 forces into 2-byte argument form — 0x59 0x01 0x00.
byte[] out = new byte[8];
short n = StructuralCbor.encodeBstrHeader((short) 0x100, out, (short) 0);
assertEquals(3, n);
assertArrayPrefix(out, "590100", n);
}
@Test
void encodeBstrHeader_negativeLenRejected() {
byte[] out = new byte[8];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.encodeBstrHeader((short) -1, out, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void encodeTstrHeader_zero() {
// tstr len 0 — header byte 0x60 (major type 3 | additional info 0).
byte[] out = new byte[8];
short n = StructuralCbor.encodeTstrHeader((short) 0, out, (short) 0);
assertEquals(1, n);
assertArrayPrefix(out, "60", n);
}
@Test
void encodeTstrHeader_twentyFourOneByteBoundary() {
// 24 forces into 1-byte argument form — 0x78 0x18.
byte[] out = new byte[8];
short n = StructuralCbor.encodeTstrHeader((short) 24, out, (short) 0);
assertEquals(2, n);
assertArrayPrefix(out, "7818", n);
}
@Test
void encodeTstrHeader_twoByteBoundary() {
// 0x100 forces into 2-byte argument form — 0x79 0x01 0x00.
byte[] out = new byte[8];
short n = StructuralCbor.encodeTstrHeader((short) 0x100, out, (short) 0);
assertEquals(3, n);
assertArrayPrefix(out, "790100", n);
}
/** Helper: assert {@code out[0..n)} equals the hex-decoded expected bytes. */
private static void assertArrayPrefix(byte[] out, String expectedHex, short n) {
byte[] expected = hex(expectedHex);
assertEquals(expected.length, n, "expected " + expectedHex + " (" + expected.length + " bytes)");
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], out[i], "byte " + i + " mismatch");
}
}
private static byte[] hex(String s) {
s = s.replaceAll("\\s+", "");
byte[] out = new byte[s.length() / 2];