feat(core): HTTP/2 Phase 4 — byte-layer foundations

Builds dev.relism.flash.bytes: ByteScan (scanning/comparison/hashing,
scalar + SWAR, property-tested against each other on every boundary and
20,000 random fuzz trials each), ArrayBackedByteView/SegmentedByteView
capability hierarchy, PooledSlice/SlicePool, ByteWriter, Pairs.

Cashes in the allocation and scanning wins the existing code left on the
table: EX-04 (word-at-a-time router matching, verified directly against
fpr-core's own ByteCompare), EX-05 (pooled views replacing per-call
anonymous ByteView allocations in HeaderMap/QueryParams/PathParams),
EX-09 (HeaderMap index built once per reset() instead of rescanning per
lookup), EX-19 (reusable PathParams on the router's per-connection
scratch), EX-25/EX-26 (single-allocation String construction), EX-33
(SWAR header-terminator scan in RequestParser).

Also closes EX-06's router half, missing from this phase's own EX-item
list in the plan (same class of omission DEC-12 recorded for Phase 1):
FastPathRouterImpl/FastPathWsRouterImpl's ThreadLocals (unbounded under
one-virtual-thread-per-connection) are replaced by an opaque,
caller-owned per-connection scratch object (AbstractRouter#newScratch),
not by extending ConnectionScratch as its own Javadoc originally assumed
-- that would have created transport's first dependency on routing in
the reverse direction. Full rationale in DEC-19.

Every optimization is measured, not asserted (DEC-20): SWAR scan 35.4%
faster than scalar, kept; EX-04's word-path 32.1% faster than
byte-at-a-time at the mechanism level, kept for its real future
consumers even though today's router doesn't yet route through it
(MethodPathByteView stays deliberately non-array-backed, per the plan's
own text). Router matching itself is ~0 B/op including parametric
routes. The full h1 pipeline is not literally 0 B/op yet -- 120 B/op is
Request/RequestBody/RequestLine construction, honestly attributed to
Phase 6's explicit scope rather than hidden.

395/395 tests green, both with and without -Pjmh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-13 14:07:29 +00:00
co-authored by Claude Sonnet 5
parent 2bf261e4e2
commit 704a00a551
38 changed files with 2993 additions and 239 deletions
@@ -0,0 +1,67 @@
package dev.relism.flash.bytes;
import org.junit.jupiter.api.Test;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* Randomized agreement testing for {@link ByteScan}'s SWAR methods against their scalar
* counterparts, per Phase 4's task 1 ("property-test SWAR against scalar on random inputs of
* every length 0..256 ... including unaligned starts"). {@link ByteScanTest} already covers
* every exact boundary deterministically; this class instead throws a large volume of fully
* random bytes and random sub-ranges at both implementations, on a fixed seed for reproducible
* CI failures, purely to catch any interaction between random byte content and the SWAR bit
* tricks that a hand-picked boundary test would miss.
*/
class ByteScanFuzzTest {
private static final int TRIALS = 20_000;
private static final int MAX_LEN = 300;
@Test
void indexOf_agreesWithScalar_onFullyRandomInputs() {
Random rnd = new Random(1234567);
for (int t = 0; t < TRIALS; t++) {
int len = rnd.nextInt(MAX_LEN + 1);
byte[] buf = new byte[len];
rnd.nextBytes(buf);
byte target = (byte) rnd.nextInt(256);
int from = len == 0 ? 0 : rnd.nextInt(len + 1);
int to = from == len ? len : from + rnd.nextInt(len - from + 1);
int expected = ByteScan.indexOfScalar(buf, from, to, target);
int actual = assertDoesNotThrow(() -> ByteScan.indexOf(buf, from, to, target));
int trial = t;
assertEquals(expected, actual,
() -> "mismatch at trial " + trial + ", len=" + len + ", from=" + from + ", to=" + to);
}
}
@Test
void indexOfCrLfCrLf_agreesWithScalar_onFullyRandomInputs() {
Random rnd = new Random(9876543);
for (int t = 0; t < TRIALS; t++) {
int len = rnd.nextInt(MAX_LEN + 1);
byte[] buf = new byte[len];
rnd.nextBytes(buf);
// Occasionally bias toward CR/LF bytes so real matches (and near-matches) show up,
// not just "no CR anywhere" cases.
if (rnd.nextInt(3) == 0) {
for (int i = 0; i < len; i++) {
if (rnd.nextInt(4) == 0) buf[i] = rnd.nextBoolean() ? (byte) '\r' : (byte) '\n';
}
}
int from = len == 0 ? 0 : rnd.nextInt(len + 1);
int to = from == len ? len : from + rnd.nextInt(len - from + 1);
int expected = ByteScan.indexOfCrLfCrLfScalar(buf, from, to);
int actual = assertDoesNotThrow(() -> ByteScan.indexOfCrLfCrLf(buf, from, to));
int trial = t;
assertEquals(expected, actual,
() -> "mismatch at trial " + trial + ", len=" + len + ", from=" + from + ", to=" + to);
}
}
}
@@ -0,0 +1,247 @@
package dev.relism.flash.bytes;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
class ByteScanTest {
private static final class ArrayView implements ByteView {
final byte[] buf;
ArrayView(String s) { this.buf = s.getBytes(StandardCharsets.US_ASCII); }
@Override public int length() { return buf.length; }
@Override public byte byteAt(int i) { return buf[i]; }
}
// ── isTChar ──────────────────────────────────────────────────────────────
@Test
void isTChar_acceptsRfc9110TCharSet() {
for (char c = '0'; c <= '9'; c++) assertTrue(ByteScan.isTChar((byte) c));
for (char c = 'A'; c <= 'Z'; c++) assertTrue(ByteScan.isTChar((byte) c));
for (char c = 'a'; c <= 'z'; c++) assertTrue(ByteScan.isTChar((byte) c));
for (byte b : "!#$%&'*+-.^_`|~".getBytes(StandardCharsets.US_ASCII)) assertTrue(ByteScan.isTChar(b));
}
@Test
void isTChar_rejectsDelimitersAndControlAndHighBytes() {
for (byte b : " \t\":;,()<>[]{}=?@\\/".getBytes(StandardCharsets.US_ASCII)) {
assertFalse(ByteScan.isTChar(b), "byte '" + (char) b + "' must not be a tchar");
}
assertFalse(ByteScan.isTChar((byte) 0));
assertFalse(ByteScan.isTChar((byte) 127));
assertFalse(ByteScan.isTChar((byte) -1)); // high-bit byte, e.g. UTF-8 continuation
}
// ── indexOf: SWAR vs scalar, every boundary ─────────────────────────────
@Test
void indexOf_swarAgreesWithScalar_everyLengthAndPosition() {
Random rnd = new Random(42);
for (int len = 0; len <= 256; len++) {
byte[] buf = new byte[len];
rnd.nextBytes(buf);
// Ensure the target byte value (7) doesn't appear anywhere except where we plant it.
for (int i = 0; i < len; i++) if (buf[i] == 7) buf[i] = 8;
assertEquals(-1, ByteScan.indexOfScalar(buf, 0, len, (byte) 7));
assertEquals(ByteScan.indexOfScalar(buf, 0, len, (byte) 7), ByteScan.indexOf(buf, 0, len, (byte) 7));
for (int pos = 0; pos < len; pos++) {
byte[] planted = buf.clone();
planted[pos] = 7;
int expected = ByteScan.indexOfScalar(planted, 0, len, (byte) 7);
assertEquals(pos, expected, "scalar oracle disagrees with itself at pos " + pos);
assertEquals(expected, ByteScan.indexOf(planted, 0, len, (byte) 7),
"SWAR disagrees with scalar at len=" + len + " pos=" + pos);
}
}
}
@Test
void indexOf_unalignedStart_agreesWithScalar() {
Random rnd = new Random(7);
byte[] buf = new byte[64];
rnd.nextBytes(buf);
for (int i = 0; i < buf.length; i++) if (buf[i] == 9) buf[i] = 10;
buf[40] = 9;
for (int from = 0; from < 8; from++) {
assertEquals(ByteScan.indexOfScalar(buf, from, buf.length, (byte) 9),
ByteScan.indexOf(buf, from, buf.length, (byte) 9));
}
}
// ── indexOfCrLfCrLf: SWAR vs scalar, every boundary ─────────────────────
@Test
void indexOfCrLfCrLf_swarAgreesWithScalar_everyLengthAndPosition() {
Random rnd = new Random(99);
for (int len = 4; len <= 128; len++) {
byte[] base = new byte[len];
rnd.nextBytes(base);
// Strip any accidental \r or \n so only the planted match exists.
for (int i = 0; i < len; i++) {
if (base[i] == '\r' || base[i] == '\n') base[i] = 'x';
}
assertEquals(-1, ByteScan.indexOfCrLfCrLfScalar(base, 0, len));
assertEquals(-1, ByteScan.indexOfCrLfCrLf(base, 0, len));
for (int pos = 0; pos <= len - 4; pos++) {
byte[] planted = base.clone();
planted[pos] = '\r'; planted[pos + 1] = '\n'; planted[pos + 2] = '\r'; planted[pos + 3] = '\n';
int expected = ByteScan.indexOfCrLfCrLfScalar(planted, 0, len);
assertEquals(pos, expected);
assertEquals(expected, ByteScan.indexOfCrLfCrLf(planted, 0, len),
"SWAR disagrees with scalar at len=" + len + " pos=" + pos);
}
}
}
@Test
void indexOfCrLfCrLf_matchAtVeryLastPossiblePosition() {
byte[] buf = "GET / HTTP/1.1\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
int expected = buf.length - 4;
assertEquals(expected, ByteScan.indexOfCrLfCrLf(buf, 0, buf.length));
}
@Test
void indexOfCrLfCrLf_bareCrNotFollowedByLf_isNotAMatch() {
byte[] buf = "a\r\rb\r\n\r\nc".getBytes(StandardCharsets.US_ASCII);
int expected = ByteScan.indexOfCrLfCrLfScalar(buf, 0, buf.length);
assertEquals(expected, ByteScan.indexOfCrLfCrLf(buf, 0, buf.length));
assertTrue(expected >= 0);
}
@Test
void indexOfCrLfCrLf_lengthNotMultipleOfEight_doesNotOverrun() {
for (int len = 4; len <= 20; len++) {
byte[] buf = new byte[len];
for (int i = 0; i < len; i++) buf[i] = 'x';
assertEquals(-1, ByteScan.indexOfCrLfCrLf(buf, 0, len));
if (len >= 4) {
buf[len - 4] = '\r'; buf[len - 3] = '\n'; buf[len - 2] = '\r'; buf[len - 1] = '\n';
assertEquals(len - 4, ByteScan.indexOfCrLfCrLf(buf, 0, len));
}
}
}
// ── Case-insensitive comparison ─────────────────────────────────────────
@Test
void equalsIgnoreCaseAscii_array_matchesRegardlessOfCase() {
byte[] buf = "Content-Type".getBytes(StandardCharsets.US_ASCII);
assertTrue(ByteScan.equalsIgnoreCaseAscii(buf, 0, buf.length, "content-type"));
assertTrue(ByteScan.equalsIgnoreCaseAscii(buf, 0, buf.length, "CONTENT-TYPE"));
assertFalse(ByteScan.equalsIgnoreCaseAscii(buf, 0, buf.length, "content-length"));
}
@Test
void equalsIgnoreCaseAscii_twoArrays() {
byte[] a = "Accept".getBytes(StandardCharsets.US_ASCII);
byte[] b = "aCCEPT".getBytes(StandardCharsets.US_ASCII);
assertTrue(ByteScan.equalsIgnoreCaseAscii(a, 0, a.length, b, 0, b.length));
byte[] c = "Accept-X".getBytes(StandardCharsets.US_ASCII);
assertFalse(ByteScan.equalsIgnoreCaseAscii(a, 0, a.length, c, 0, c.length));
}
@Test
void equalsIgnoreCase_view() {
ArrayView v = new ArrayView("Keep-Alive");
assertTrue(ByteScan.equalsIgnoreCase(v, 0, v.length(), "keep-alive"));
assertFalse(ByteScan.equalsIgnoreCase(v, 0, v.length(), "close"));
}
// ── Token lists ──────────────────────────────────────────────────────────
@Test
void tokenListContains_findsTokenAmongMultiple() {
ArrayView v = new ArrayView("keep-alive, Upgrade");
assertTrue(ByteScan.tokenListContains(v, "upgrade"));
assertTrue(ByteScan.tokenListContains(v, "keep-alive"));
assertFalse(ByteScan.tokenListContains(v, "close"));
}
@Test
void tokenListContains_singleToken() {
ArrayView v = new ArrayView("close");
assertTrue(ByteScan.tokenListContains(v, "close"));
}
@Test
void tokenListContains_emptyList() {
ArrayView v = new ArrayView("");
assertFalse(ByteScan.tokenListContains(v, "close"));
}
// ── Header-name hash ─────────────────────────────────────────────────────
@Test
void hashNameIgnoreCaseAscii_isCaseInsensitive() {
byte[] lower = "content-length".getBytes(StandardCharsets.US_ASCII);
byte[] mixed = "Content-Length".getBytes(StandardCharsets.US_ASCII);
byte[] upper = "CONTENT-LENGTH".getBytes(StandardCharsets.US_ASCII);
int h1 = ByteScan.hashNameIgnoreCaseAscii(lower, 0, lower.length);
int h2 = ByteScan.hashNameIgnoreCaseAscii(mixed, 0, mixed.length);
int h3 = ByteScan.hashNameIgnoreCaseAscii(upper, 0, upper.length);
assertEquals(h1, h2);
assertEquals(h2, h3);
}
@Test
void hashNameIgnoreCaseAscii_stringOverloadAgreesWithByteArrayOverload() {
for (String name : new String[]{"content-length", "Content-Length", "CONTENT-LENGTH", "x", ""}) {
byte[] b = name.getBytes(StandardCharsets.US_ASCII);
assertEquals(ByteScan.hashNameIgnoreCaseAscii(b, 0, b.length), ByteScan.hashNameIgnoreCaseAscii(name));
}
}
@Test
void hashNameIgnoreCaseAscii_differentNamesUsuallyDiffer() {
String[] names = {"content-length", "content-type", "authorization", "cookie", "accept",
"host", "user-agent", "x-forwarded-for", "connection", "upgrade"};
java.util.Set<Integer> hashes = new java.util.HashSet<>();
for (String n : names) {
byte[] b = n.getBytes(StandardCharsets.US_ASCII);
hashes.add(ByteScan.hashNameIgnoreCaseAscii(b, 0, b.length));
}
assertEquals(names.length, hashes.size(), "expected no collisions among common header names");
}
// ── Decimal / hex parsing ────────────────────────────────────────────────
@Test
void parseDecimalStrict_validAndInvalidCases() {
assertEquals(0L, parse("0"));
assertEquals(12345L, parse("12345"));
assertEquals(Long.MAX_VALUE, parse(Long.toString(Long.MAX_VALUE)));
assertEquals(ByteScan.PARSE_INVALID, parse(""));
assertEquals(ByteScan.PARSE_INVALID, parse("12a45"));
assertEquals(ByteScan.PARSE_INVALID, parse("-1"));
assertEquals(ByteScan.PARSE_INVALID, parse("+1"));
assertEquals(ByteScan.PARSE_INVALID, parse("99999999999999999999")); // overflow
assertEquals(ByteScan.PARSE_INVALID, parse("10000000000000000000")); // > Long.MAX_VALUE, 20 digits already rejected by length
}
private static long parse(String s) {
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
return ByteScan.parseDecimalStrict(b, 0, b.length);
}
@Test
void parseHexStrict_validAndInvalidCases() {
assertEquals(0xFFL, hex("ff", 8));
assertEquals(0xABCDL, hex("aBcD", 8));
assertEquals(ByteScan.PARSE_INVALID, hex("", 8));
assertEquals(ByteScan.PARSE_INVALID, hex("xyz", 8));
assertEquals(ByteScan.PARSE_INVALID, hex("123456789", 8)); // too many digits
}
private static long hex(String s, int maxDigits) {
byte[] b = s.getBytes(StandardCharsets.US_ASCII);
return ByteScan.parseHexStrict(b, 0, b.length, maxDigits);
}
}
@@ -0,0 +1,124 @@
package dev.relism.flash.bytes;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class ByteWriterTest {
private static String asString(ByteWriter w) {
return new String(w.array(), 0, w.length(), StandardCharsets.US_ASCII);
}
@Test
void writeByte_and_writeBytes() {
ByteWriter w = new ByteWriter(4);
w.writeByte((byte) 'H');
w.writeBytes("ello".getBytes(StandardCharsets.US_ASCII));
assertEquals("Hello", asString(w));
}
@Test
void writeBytes_offsetAndLength() {
ByteWriter w = new ByteWriter(4);
byte[] src = "xxHELLOxx".getBytes(StandardCharsets.US_ASCII);
w.writeBytes(src, 2, 5);
assertEquals("HELLO", asString(w));
}
@Test
void growsPastInitialCapacity_withoutLosingData() {
ByteWriter w = new ByteWriter(2);
StringBuilder expected = new StringBuilder();
for (int i = 0; i < 1000; i++) {
w.writeByte((byte) ('a' + (i % 26)));
expected.append((char) ('a' + (i % 26)));
}
assertEquals(expected.toString(), asString(w));
}
@Test
void reset_reusesBufferFromScratch() {
ByteWriter w = new ByteWriter(16);
w.writeBytes("first".getBytes(StandardCharsets.US_ASCII));
byte[] bufBeforeReset = w.array();
w.reset();
assertEquals(0, w.length());
w.writeBytes("second".getBytes(StandardCharsets.US_ASCII));
assertEquals("second", asString(w));
assertSame(bufBeforeReset, w.array(), "reset() must not reallocate when capacity already suffices");
}
@Test
void writeDecimal_variousValues() {
assertDecimal("0", 0);
assertDecimal("7", 7);
assertDecimal("42", 42);
assertDecimal("1000000", 1_000_000);
assertDecimal(Long.toString(Long.MAX_VALUE), Long.MAX_VALUE);
}
private static void assertDecimal(String expected, long value) {
ByteWriter w = new ByteWriter(4);
w.writeDecimal(value);
assertEquals(expected, asString(w));
}
@Test
void writeDecimal_rejectsNegative() {
ByteWriter w = new ByteWriter(4);
assertThrows(IllegalArgumentException.class, () -> w.writeDecimal(-1));
}
@Test
void writeHex_variousValues() {
assertHex("0", 0);
assertHex("ff", 0xFF);
assertHex("1a2b3c", 0x1A2B3C);
assertHex("ffffffff", 0xFFFFFFFF);
}
private static void assertHex(String expected, int value) {
ByteWriter w = new ByteWriter(4);
w.writeHex(value);
assertEquals(expected, asString(w));
}
@Test
void writeAsciiLower_lowersUppercaseOnly() {
ByteWriter w = new ByteWriter(4);
w.writeAsciiLower("Content-TYPE");
assertEquals("content-type", asString(w));
}
@Test
void writeUInt16_bigEndian() {
ByteWriter w = new ByteWriter(4);
w.writeUInt16(0x1234);
assertArrayEquals(new byte[]{0x12, 0x34}, java.util.Arrays.copyOf(w.array(), w.length()));
}
@Test
void writeUInt24_bigEndian() {
ByteWriter w = new ByteWriter(4);
w.writeUInt24(0x123456);
assertArrayEquals(new byte[]{0x12, 0x34, 0x56}, java.util.Arrays.copyOf(w.array(), w.length()));
}
@Test
void writeUInt31_masksTopBit() {
ByteWriter w = new ByteWriter(4);
w.writeUInt31(0xFFFFFFFF); // all bits set -> top bit must be cleared
assertArrayEquals(new byte[]{0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF},
java.util.Arrays.copyOf(w.array(), w.length()));
}
@Test
void writeUInt32_bigEndian() {
ByteWriter w = new ByteWriter(4);
w.writeUInt32(0x01020304);
assertArrayEquals(new byte[]{0x01, 0x02, 0x03, 0x04}, java.util.Arrays.copyOf(w.array(), w.length()));
}
}
@@ -0,0 +1,37 @@
package dev.relism.flash.bytes;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PairsTest {
@Test
void packAndUnpack_roundTrip() {
long p = Pairs.pack(1234, 5678);
assertEquals(1234, Pairs.hi(p));
assertEquals(5678, Pairs.lo(p));
}
@Test
void packAndUnpack_zero() {
long p = Pairs.pack(0, 0);
assertEquals(0, Pairs.hi(p));
assertEquals(0, Pairs.lo(p));
}
@Test
void packAndUnpack_maxInts() {
long p = Pairs.pack(Integer.MAX_VALUE, Integer.MAX_VALUE);
assertEquals(Integer.MAX_VALUE, Pairs.hi(p));
assertEquals(Integer.MAX_VALUE, Pairs.lo(p));
}
@Test
void lo_doesNotSignExtendFromHi() {
// hi negative-looking bit pattern must not bleed into lo after unpack.
long p = Pairs.pack(-1, 42);
assertEquals(-1, Pairs.hi(p));
assertEquals(42, Pairs.lo(p));
}
}
@@ -0,0 +1,71 @@
package dev.relism.flash.bytes;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class SegmentedByteViewTest {
@Test
void reset_presentsSegmentsAsOneLogicalSequence() {
byte[][] segments = {
"Hello, ".getBytes(StandardCharsets.US_ASCII),
"World".getBytes(StandardCharsets.US_ASCII),
"!".getBytes(StandardCharsets.US_ASCII),
};
int[] offsets = {0, 0, 0};
int[] lengths = {segments[0].length, segments[1].length, segments[2].length};
SegmentedByteView view = new SegmentedByteView();
view.reset(segments, offsets, lengths, 3);
assertEquals(13, view.length());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < view.length(); i++) sb.append((char) view.byteAt(i));
assertEquals("Hello, World!", sb.toString());
}
@Test
void reset_honorsPerSegmentOffsetAndLength() {
byte[][] segments = { "xxABCxx".getBytes(StandardCharsets.US_ASCII) };
SegmentedByteView view = new SegmentedByteView();
view.reset(segments, new int[]{2}, new int[]{3}, 1);
assertEquals(3, view.length());
assertEquals('A', (char) view.byteAt(0));
assertEquals('C', (char) view.byteAt(2));
}
@Test
void reset_isReusableAcrossCalls() {
SegmentedByteView view = new SegmentedByteView();
view.reset(new byte[][]{"abc".getBytes(StandardCharsets.US_ASCII)}, new int[]{0}, new int[]{3}, 1);
assertEquals(3, view.length());
view.reset(new byte[][]{"de".getBytes(StandardCharsets.US_ASCII)}, new int[]{0}, new int[]{2}, 1);
assertEquals(2, view.length());
assertEquals('d', (char) view.byteAt(0));
}
@Test
void byteAt_outOfBoundsThrows() {
SegmentedByteView view = new SegmentedByteView();
view.reset(new byte[][]{"ab".getBytes(StandardCharsets.US_ASCII)}, new int[]{0}, new int[]{2}, 1);
assertThrows(IndexOutOfBoundsException.class, () -> view.byteAt(2));
assertThrows(IndexOutOfBoundsException.class, () -> view.byteAt(-1));
}
@Test
void supportsLong_alwaysFalse() {
SegmentedByteView view = new SegmentedByteView();
view.reset(new byte[][]{"12345678".getBytes(StandardCharsets.US_ASCII)}, new int[]{0}, new int[]{8}, 1);
assertFalse(view.supportsLong());
}
@Test
void emptySegmentCount_isZeroLength() {
SegmentedByteView view = new SegmentedByteView();
view.reset(new byte[][]{}, new int[]{}, new int[]{}, 0);
assertEquals(0, view.length());
}
}
@@ -0,0 +1,62 @@
package dev.relism.flash.bytes;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class SlicePoolTest {
private static byte[] bytes(String s) { return s.getBytes(StandardCharsets.US_ASCII); }
@Test
void acquire_repositionsAndReturnsRequestedRange() {
SlicePool pool = new SlicePool(4);
byte[] buf = bytes("hello world");
PooledSlice slice = pool.acquire(buf, 6, 5);
assertEquals(5, slice.length());
assertEquals('w', (char) slice.byteAt(0));
assertEquals('d', (char) slice.byteAt(4));
}
@Test
void acquire_withinPoolSize_returnsDistinctLiveSlices() {
SlicePool pool = new SlicePool(4);
byte[] buf = bytes("abcdefgh");
PooledSlice a = pool.acquire(buf, 0, 1); // 'a'
PooledSlice b = pool.acquire(buf, 1, 1); // 'b'
PooledSlice c = pool.acquire(buf, 2, 1); // 'c'
// All three still valid simultaneously — the pool hasn't wrapped yet (size 4).
assertEquals('a', (char) a.byteAt(0));
assertEquals('b', (char) b.byteAt(0));
assertEquals('c', (char) c.byteAt(0));
}
@Test
void wraparoundAliasesThePreviouslyReturnedSlice() {
// Demonstrates the documented hazard: retaining a slice past `size` further acquire()
// calls observes it silently repositioned to unrelated data.
SlicePool pool = new SlicePool(2);
byte[] buf = bytes("AABB");
PooledSlice first = pool.acquire(buf, 0, 2); // "AA"
assertEquals('A', (char) first.byteAt(0));
pool.acquire(buf, 2, 2); // "BB" — slot 2, pool size 2 so this is still a fresh slot
PooledSlice thirdCall = pool.acquire(buf, 2, 2); // wraps back to `first`'s slot
assertSame(first, thirdCall, "pool of size 2 must reuse the first slot on the 3rd acquire()");
// `first` is now silently "BB", not "AA" — the documented lifetime contract in action.
assertEquals('B', (char) first.byteAt(0));
}
@Test
void constructor_rejectsNonPositiveSize() {
assertThrows(IllegalArgumentException.class, () -> new SlicePool(0));
assertThrows(IllegalArgumentException.class, () -> new SlicePool(-1));
}
@Test
void size_reportsConstructedCapacity() {
assertEquals(4, new SlicePool(4).size());
}
}
@@ -0,0 +1,151 @@
package dev.relism.flash.models;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-09}: dedicated correctness coverage for {@link HeaderMap}'s per-{@code reset()}
* index — duplicate names, case variation, zero headers, and growth past the initial index
* capacity up to {@code Http1Limits.MAX_HEADER_COUNT}. {@link HeaderMapTest} already covers the
* ordinary lookup/forEach contract; this class targets the index machinery specifically.
*/
class HeaderMapIndexTest {
private static HeaderMap parse(String... headers) {
StringBuilder sb = new StringBuilder();
for (String h : headers) sb.append(h).append("\r\n");
byte[] buffer = sb.toString().getBytes(StandardCharsets.UTF_8);
HeaderMap map = new HeaderMap();
map.reset(buffer, 0, buffer.length);
return map;
}
@Test
void zeroHeaders_everyLookupIsEmpty() {
HeaderMap map = parse();
assertNull(map.first("Host"));
assertTrue(map.all("Host").isEmpty());
assertTrue(map.all().isEmpty());
assertNull(map.view("Host"));
assertFalse(map.valueEqualsIgnoreCase("Connection", "close"));
}
@Test
void duplicateHeaderNames_firstReturnsTheFirstOne_allReturnsAllInOrder() {
HeaderMap map = parse("X-Trace: a", "X-Trace: b", "X-Trace: c");
assertEquals("a", map.first("X-Trace"));
assertEquals(List.of("a", "b", "c"), map.all("X-Trace"));
}
@Test
void caseVariation_indexHashAndCompareBothIgnoreCase() {
HeaderMap map = parse("X-Custom-Header: value1");
assertEquals("value1", map.first("x-custom-header"));
assertEquals("value1", map.first("X-CUSTOM-HEADER"));
assertEquals("value1", map.first("X-cUsToM-hEaDeR"));
}
@Test
void similarButDistinctNames_doNotCollideInTheIndex() {
// Names sharing a hash-prefix-adjacent shape must still resolve independently.
HeaderMap map = parse("Accept: a", "Accept-Encoding: b", "Accept-Language: c");
assertEquals("a", map.first("Accept"));
assertEquals("b", map.first("Accept-Encoding"));
assertEquals("c", map.first("Accept-Language"));
}
@Test
void growsPastInitialIndexCapacity_upToMaxHeaderCount_andStaysCorrect() {
int n = dev.relism.flash.http.Http1Limits.MAX_HEADER_COUNT;
String[] headers = new String[n];
for (int i = 0; i < n; i++) headers[i] = "X-Header-" + i + ": value-" + i;
HeaderMap map = parse(headers);
assertEquals("value-0", map.first("X-Header-0"));
assertEquals("value-" + (n - 1), map.first("X-Header-" + (n - 1)));
assertEquals("value-" + (n / 2), map.first("X-Header-" + (n / 2)));
assertEquals(n, map.all().size());
}
@Test
void reset_rebuildsIndexFromScratch_noStaleEntriesFromPreviousRequest() {
HeaderMap map = parse("Host: first-request");
assertEquals("first-request", map.first("Host"));
assertNull(map.first("X-Only-In-Second"));
byte[] second = "Host: second-request\r\nX-Only-In-Second: yes\r\n".getBytes(StandardCharsets.UTF_8);
map.reset(second, 0, second.length);
assertEquals("second-request", map.first("Host"));
assertEquals("yes", map.first("X-Only-In-Second"));
}
@Test
void repeatedResetsAcrossVaryingHeaderCounts_shrinkAndGrowSafely() {
// A connection whose successive keep-alive requests have very different header counts
// must never see stale entries from a larger previous request bleed into a smaller one.
HeaderMap map = new HeaderMap();
for (int round = 0; round < 5; round++) {
int n = (round % 2 == 0) ? 20 : 2;
String[] headers = new String[n];
for (int i = 0; i < n; i++) headers[i] = "H" + i + ": v" + i + "-" + round;
byte[] buf = String.join("\r\n", headers).concat("\r\n").getBytes(StandardCharsets.UTF_8);
map.reset(buf, 0, buf.length);
assertEquals(n, map.all().size(), "round " + round);
assertEquals("v0-" + round, map.first("H0"));
if (n < 20) assertNull(map.first("H19"), "round " + round + " must not see a stale H19");
}
}
@Test
void allocation_indexArraysAreNotReallocatedOnceWarm() {
// The rigorous 0 B/op verification is the Phase 17 JMH gate (-prof gc); this is a
// unit-test-level structural guarantee that repeated first()/all()/view() lookups never
// re-trigger index growth (Arrays.copyOf inside ensureIndexCapacity) after the first
// reset() has already sized the arrays for this header count — asserted by identity: the
// backing array references must be the exact same objects before and after 100k lookups.
HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4");
int[] namesBefore = arrayFieldValue(map, "nameOffsets");
for (int i = 0; i < 100_000; i++) {
assertEquals("2", map.first("B"));
assertNotNull(map.view("C"));
assertFalse(map.all("D").isEmpty());
}
int[] namesAfter = arrayFieldValue(map, "nameOffsets");
assertSame(namesBefore, namesAfter, "lookups alone must never reallocate the index arrays");
}
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
// EX-05's documented hazard, demonstrated through the actual public API: HeaderMap's
// view() pool is sized 4 (VIEW_POOL_SIZE); a 5th call in the same request wraps around
// and silently repositions the object the 1st call returned.
dev.relism.fpr.core.ByteView v1 = null;
HeaderMap map = parse("A: 1", "B: 2", "C: 3", "D: 4", "E: 5");
for (String name : new String[]{"A", "B", "C", "D"}) {
dev.relism.fpr.core.ByteView v = map.view(name);
if (v1 == null) v1 = v;
}
assertEquals('1', v1.byteAt(0)); // still "A"'s value — pool has not wrapped yet
dev.relism.fpr.core.ByteView v5 = map.view("E"); // 5th call — wraps back to v1's slot
assertSame(v1, v5, "the 5th view() call must reuse the 1st call's slice instance");
assertEquals('5', v1.byteAt(0)); // v1 is now silently "E"'s value, not "A"'s
}
private static int[] arrayFieldValue(HeaderMap map, String fieldName) {
try {
var field = HeaderMap.class.getDeclaredField(fieldName);
field.setAccessible(true);
return (int[]) field.get(map);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
}
@@ -1,5 +1,6 @@
package dev.relism.flash.models;
import dev.relism.flash.routing.routers.fastpathrouter.FastPathViews;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.Test;
@@ -64,4 +65,33 @@ class PathParamsTest {
PathParams params = of("/users/123", "userId", "123");
assertNull(params.view("unknown"));
}
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
// EX-05's pooled path only engages when `source` is array-backed (ArrayBackedByteView) —
// unlike of()'s plain inline ByteView (which exercises the non-pooled fallback, still
// correct but not the code path this test targets), use the same view type RequestParser
// actually produces.
String path = "/a/1/b/2/c/3/d/4/e/5";
byte[] bytes = path.getBytes(StandardCharsets.UTF_8);
ByteView source = new FastPathViews.RequestByteView(bytes, 0, bytes.length);
String[] names = {"a", "b", "c", "d", "e"};
int[] starts = new int[names.length];
int[] lens = new int[names.length];
String[] values = {"1", "2", "3", "4", "5"};
for (int i = 0; i < names.length; i++) {
starts[i] = path.indexOf(values[i]);
lens[i] = values[i].length();
}
PathParams params = new PathParams(source, names, starts, lens);
ByteView v1 = params.view("a");
params.view("b");
params.view("c");
params.view("d"); // pool size 4 — not wrapped yet
assertEquals('1', v1.byteAt(0));
ByteView v5 = params.view("e"); // 5th call wraps back to v1's slot
assertSame(v1, v5);
assertEquals('5', v1.byteAt(0));
}
}
@@ -0,0 +1,95 @@
package dev.relism.flash.models;
import dev.relism.flash.routing.routers.fastpathrouter.FastPathViews;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-26}: the clean-value (no {@code %}/{@code +}) fast path in {@code QueryParams.decode}
* must produce byte-for-byte identical results to the percent-decoding slow path it bypasses —
* verified here across clean values, values needing every kind of decoding, and the boundary
* between them. Also covers {@code EX-05}'s pooled {@code view()}.
*/
class QueryParamsFastPathTest {
private static QueryParams of(String query) {
byte[] bytes = query.getBytes(StandardCharsets.US_ASCII);
return new QueryParams(new FastPathViews.RequestByteView(bytes, 0, bytes.length));
}
@Test
void cleanValue_noPercentOrPlus_decodesToItself() {
QueryParams qp = of("name=hello&city=NewYork");
assertEquals("hello", qp.get("name"));
assertEquals("NewYork", qp.get("city"));
}
@Test
void valueWithPlus_decodesToSpace_takesSlowPath() {
QueryParams qp = of("q=hello+world");
assertEquals("hello world", qp.get("q"));
}
@Test
void valueWithPercentEscape_decodesCorrectly_takesSlowPath() {
QueryParams qp = of("q=hello%20world");
assertEquals("hello world", qp.get("q"));
}
@Test
void valueWithInvalidPercentEscape_keepsLiteralPercent() {
QueryParams qp = of("q=100%25off");
assertEquals("100%off", qp.get("q"));
QueryParams qp2 = of("q=trailing%2");
assertEquals("trailing%2", qp2.get("q"));
}
@Test
void emptyValue_isClean_decodesToEmptyString() {
QueryParams qp = of("a=&b=1");
assertEquals("", qp.get("a"));
assertEquals("1", qp.get("b"));
}
@Test
void mixedCleanAndEncodedValues_inSameQueryString() {
QueryParams qp = of("clean=abc&encoded=a%20b&plussed=a+b");
assertEquals("abc", qp.get("clean"));
assertEquals("a b", qp.get("encoded"));
assertEquals("a b", qp.get("plussed"));
}
// ── EX-05: pooled view() ────────────────────────────────────────────────
@Test
void view_returnsRawUndecodedBytes() {
QueryParams qp = of("q=a+b");
ByteView v = qp.view("q");
assertNotNull(v);
assertEquals(3, v.length());
assertEquals('+', (char) v.byteAt(1)); // raw, not percent/plus-decoded
}
@Test
void view_missingKey_returnsNull() {
QueryParams qp = of("q=1");
assertNull(qp.view("missing"));
}
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
QueryParams qp = of("a=1&b=2&c=3&d=4&e=5");
ByteView v1 = qp.view("a");
qp.view("b");
qp.view("c");
qp.view("d"); // pool size 4 — not wrapped yet
assertEquals('1', v1.byteAt(0));
ByteView v5 = qp.view("e"); // 5th call wraps back to v1's slot
assertSame(v1, v5);
assertEquals('5', v1.byteAt(0));
}
}
@@ -17,7 +17,7 @@ class AbstractRouterTest {
String lastAddedPath;
@Override
public RequestHandler route(Request request) { return null; }
public RequestHandler route(Request request, Object scratch) { return null; }
@Override
protected AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler) {
@@ -15,7 +15,7 @@ class AbstractWsRouterTest {
String lastPath;
@Override
public WebSocketHandler route(Request request) { return null; }
public WebSocketHandler route(Request request, Object scratch) { return null; }
@Override
protected AbstractWsRouter addRoute(HttpMethod method, String path, WebSocketHandler handler) {
@@ -33,12 +33,13 @@ class FastPathRouterImplTest {
FastPathRouterImpl router = new FastPathRouterImpl();
router.doRegister(HttpMethod.GET, "/a", new SimpleHandler((req, res) -> "A"), NO_MW);
router.doRegister(HttpMethod.POST, "/b", new SimpleHandler((req, res) -> "B"), NO_MW);
Object scratch = router.newScratch();
RequestHandler res1 = router.route(mockRequest(HttpMethod.GET, "/a"));
RequestHandler res1 = router.route(mockRequest(HttpMethod.GET, "/a"), scratch);
assertNotNull(res1);
assertEquals("A", res1.handle(null, null));
RequestHandler res2 = router.route(mockRequest(HttpMethod.POST, "/b"));
RequestHandler res2 = router.route(mockRequest(HttpMethod.POST, "/b"), scratch);
assertNotNull(res2);
assertEquals("B", res2.handle(null, null));
}
@@ -47,9 +48,10 @@ class FastPathRouterImplTest {
void route_noMatch_returnsNull() {
FastPathRouterImpl router = new FastPathRouterImpl();
router.doRegister(HttpMethod.GET, "/a", new SimpleHandler((req, res) -> "A"), NO_MW);
Object scratch = router.newScratch();
assertNull(router.route(mockRequest(HttpMethod.GET, "/b")));
assertNull(router.route(mockRequest(HttpMethod.POST, "/a")));
assertNull(router.route(mockRequest(HttpMethod.GET, "/b"), scratch));
assertNull(router.route(mockRequest(HttpMethod.POST, "/a"), scratch));
}
@Test
@@ -59,7 +61,7 @@ class FastPathRouterImplTest {
new SimpleHandler((req, res) -> "Extract"), NO_MW);
Request request = mockRequest(HttpMethod.GET, "/users/123/items/456");
RequestHandler handler = router.route(request);
RequestHandler handler = router.route(request, router.newScratch());
assertNotNull(handler);
assertEquals("Extract", handler.handle(request, null));
@@ -67,4 +69,35 @@ class FastPathRouterImplTest {
assertEquals("123", request.param("id"));
assertEquals("456", request.param("itemId"));
}
@Test
void route_reusesScratchAcrossManyRequests_includingGrowingParamCapacity() throws Exception {
// EX-19: the same scratch, reused across a mix of param counts, must keep matching
// correctly as its arrays grow past their initial size (8) and get reused afterward.
FastPathRouterImpl router = new FastPathRouterImpl();
router.doRegister(HttpMethod.GET, "/a/{p1}/{p2}/{p3}/{p4}/{p5}/{p6}/{p7}/{p8}/{p9}/{p10}",
new SimpleHandler((req, res) -> "many"), NO_MW);
router.doRegister(HttpMethod.GET, "/b/{id}", new SimpleHandler((req, res) -> "one"), NO_MW);
Object scratch = router.newScratch();
for (int i = 0; i < 3; i++) {
Request oneParam = mockRequest(HttpMethod.GET, "/b/123");
assertEquals("one", router.route(oneParam, scratch).handle(oneParam, null));
assertEquals("123", oneParam.param("id"));
Request tenParams = mockRequest(HttpMethod.GET, "/a/1/2/3/4/5/6/7/8/9/10");
assertEquals("many", router.route(tenParams, scratch).handle(tenParams, null));
assertEquals("10", tenParams.param("p10"));
assertEquals("1", tenParams.param("p1"));
// The 1-param request that follows a 10-param one must not see stale params left
// over from the larger match in the shared, oversized arrays.
Request oneParamAgain = mockRequest(HttpMethod.GET, "/b/456");
RequestHandler h = router.route(oneParamAgain, scratch);
assertNotNull(h);
h.handle(oneParamAgain, null);
assertEquals("456", oneParamAgain.param("id"));
assertNull(oneParamAgain.param("p10"));
}
}
}
@@ -0,0 +1,116 @@
package dev.relism.flash.routing.routers.fastpathrouter;
import dev.relism.fpr.core.FastPathRouter;
import dev.relism.fpr.core.MatchResult;
import dev.relism.fpr.core.RouterBuilder;
import dev.relism.fpr.core.dsl.StringRouteParser;
import dev.relism.fpr.core.internal.runtime.ByteCompare;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-04}: verifies the {@code longAt()}/{@code supportsLong()} contract against
* {@code fpr-core}'s own word-at-a-time comparison code — not merely against a hand-derived
* expectation, per the plan's explicit instruction to verify by testing against {@code fpr-core}
* directly rather than by reading its bytecode (bytecode-reading only informed which byte order
* to use; this test is the actual verification). A wrong endianness or a wrong bounds assumption
* here produces silently mis-routed requests, the worst possible failure mode ({@code EX-04}'s
* own registry entry) — so this covers both the raw word-read contract and an end-to-end router
* match with the long path actually engaged.
*/
class FastPathViewsLongAtTest {
// ── Raw longAt() vs. a hand-assembled little-endian expectation ────────
@Test
void longAt_assemblesLittleEndian() {
byte[] buf = {1, 2, 3, 4, 5, 6, 7, 8};
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(buf, 0, 8);
assertTrue(view.supportsLong());
long expected = 0x0807060504030201L; // byte 0 -> least significant byte
assertEquals(expected, view.longAt(0));
}
@Test
void longAt_respectsViewOffset_notJustArrayOffset() {
byte[] buf = {(byte) 0xFF, (byte) 0xFF, 1, 2, 3, 4, 5, 6, 7, 8, (byte) 0xFF};
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(buf, 2, 8);
long expected = 0x0807060504030201L;
assertEquals(expected, view.longAt(0));
}
// ── Cross-checked against fpr-core's own ByteCompare, the actual consumer of longAt() ──
@Test
void byteCompareEquals_agreesBetweenLongPathAndByteAtATimePath_onIdenticalContent() {
byte[] content = "GET/users/1234567890/profile".getBytes(StandardCharsets.US_ASCII);
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(content, 0, content.length);
byte[] other = content.clone();
assertTrue(ByteCompare.equals(view, 0, other, 0, content.length, true));
assertTrue(ByteCompare.equals(view, 0, other, 0, content.length, false));
}
@Test
void byteCompareEquals_agreesBetweenLongPathAndByteAtATimePath_onDivergingContent() {
// Diverge at every position across an 8+-byte range, including inside a word, at a word
// boundary, and in the scalar tail — a wrong longAt() would only show up at some of these.
byte[] base = "abcdefghijklmnopqrstuvwxyz012345".getBytes(StandardCharsets.US_ASCII);
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(base, 0, base.length);
for (int diffAt = 0; diffAt < base.length; diffAt++) {
byte[] other = base.clone();
other[diffAt] = (byte) (other[diffAt] + 1);
boolean withLong = ByteCompare.equals(view, 0, other, 0, base.length, true);
boolean withoutLong = ByteCompare.equals(view, 0, other, 0, base.length, false);
assertFalse(withLong, "long path failed to detect divergence at " + diffAt);
assertEquals(withoutLong, withLong, "long/byte-at-a-time paths disagree at diffAt=" + diffAt);
}
}
@Test
void byteCompareIndexOf_agreesBetweenLongPathAndByteAtATimePath() {
byte[] haystack = "xxxxxxxxxxxxxxxxxTARGETxxxxxxxxxxxxxxxxxx".getBytes(StandardCharsets.US_ASCII);
byte[] needle = "TARGET".getBytes(StandardCharsets.US_ASCII);
FastPathViews.RequestByteView view = new FastPathViews.RequestByteView(haystack, 0, haystack.length);
int withLong = ByteCompare.indexOf(view, 0, haystack.length, needle, 0, needle.length, true);
int withoutLong = ByteCompare.indexOf(view, 0, haystack.length, needle, 0, needle.length, false);
assertEquals(withoutLong, withLong);
assertTrue(withLong >= 0);
}
// ── End-to-end: a real router, literal routes >= 8 bytes, long path actually engaged ────
@Test
void router_matchesCorrectly_withLongLiteralSegmentsAndTheLongPathEnabled() {
RouterBuilder<String> builder = new RouterBuilder<>();
builder.add(StringRouteParser.parse("GET/aaaaaaaaaaaaaaaaaaaa"), "route-a");
builder.add(StringRouteParser.parse("GET/bbbbbbbbbbbbbbbbbbbb"), "route-b");
builder.add(StringRouteParser.parse("GET/aaaaaaaaaaaaaaaaaaab"), "route-a-near-miss");
FastPathRouter<dev.relism.fpr.core.ByteView, String> router = builder.compile();
assertEquals("route-a", matchOne(router, "GET", "/aaaaaaaaaaaaaaaaaaaa"));
assertEquals("route-b", matchOne(router, "GET", "/bbbbbbbbbbbbbbbbbbbb"));
// Differs only in the very last byte — must not be conflated with route-a by a
// word-at-a-time comparison that got the tail handling wrong.
assertEquals("route-a-near-miss", matchOne(router, "GET", "/aaaaaaaaaaaaaaaaaaab"));
assertNull(matchOne(router, "GET", "/aaaaaaaaaaaaaaaaaaac"));
assertNull(matchOne(router, "GET", "/ccccccccccccccccccccc"));
}
private static String matchOne(FastPathRouter<dev.relism.fpr.core.ByteView, String> router,
String method, String path) {
byte[] methodBytes = method.getBytes(StandardCharsets.US_ASCII);
FastPathViews.RequestByteView pathView =
new FastPathViews.RequestByteView(path.getBytes(StandardCharsets.US_ASCII), 0, path.length());
FastPathViews.MethodPathByteView combined = new FastPathViews.MethodPathByteView();
combined.reset(methodBytes, pathView);
MatchResult<String> result = new MatchResult<>(8, 32);
int labelId = router.match(combined, result);
return labelId == FastPathRouter.NO_MATCH ? null : result.handler();
}
}