@@ -251,6 +251,63 @@ static int wireTotalLength(int headerLen, int bodyLen) {
251251 return (int ) total ;
252252 }
253253
254+ /**
255+ * Decode and validate the u32 BE header-length prefix at bytes {@code 0..4}
256+ * of a heap wire frame — the <strong>single source of truth</strong> for
257+ * the frame split shared by {@link #decodeResponse} and the
258+ * {@link VesperaProxyController} write/build paths, so the bounds contract
259+ * can never drift between the (previously duplicated) call sites.
260+ *
261+ * <p>The prefix is read from absolute bytes (big-endian, order-independent),
262+ * never {@code ByteBuffer.getInt} which honours the buffer's current byte
263+ * order.
264+ *
265+ * @return the header JSON length {@code N} (so the body is {@code wire[4+N..]})
266+ * @throws IllegalArgumentException if {@code wire} is shorter than the
267+ * 4-byte prefix, or the decoded length is negative or overflows the frame
268+ */
269+ static int readHeaderLength (byte [] wire ) {
270+ if (wire == null || wire .length < 4 ) {
271+ throw new IllegalArgumentException (
272+ "wire response too short: "
273+ + (wire == null ? "null" : wire .length + " bytes" ));
274+ }
275+ int headerLen = ((wire [0 ] & 0xFF ) << 24 ) | ((wire [1 ] & 0xFF ) << 16 )
276+ | ((wire [2 ] & 0xFF ) << 8 ) | (wire [3 ] & 0xFF );
277+ if (headerLen < 0 || 4L + headerLen > wire .length ) {
278+ throw new IllegalArgumentException (
279+ "wire header_len " + headerLen
280+ + " overflows response (" + wire .length + " bytes)" );
281+ }
282+ return headerLen ;
283+ }
284+
285+ /**
286+ * {@link ByteBuffer} sibling of {@link #readHeaderLength(byte[])} — decodes
287+ * the u32 BE header-length prefix from absolute bytes {@code 0..4} of
288+ * {@code wire} (honouring neither the buffer's position nor its byte order),
289+ * validating against {@code wire.limit()}.
290+ *
291+ * @return the header JSON length {@code N}
292+ * @throws IllegalArgumentException if the buffer is shorter than the 4-byte
293+ * prefix, or the decoded length is negative or overflows the limit
294+ */
295+ static int readHeaderLength (ByteBuffer wire ) {
296+ int limit = wire .limit ();
297+ if (limit < 4 ) {
298+ throw new IllegalArgumentException ("wire response too short: " + limit + " bytes" );
299+ }
300+ int headerLen = ((wire .get (0 ) & 0xFF ) << 24 )
301+ | ((wire .get (1 ) & 0xFF ) << 16 )
302+ | ((wire .get (2 ) & 0xFF ) << 8 )
303+ | (wire .get (3 ) & 0xFF );
304+ if (headerLen < 0 || 4L + headerLen > limit ) {
305+ throw new IllegalArgumentException (
306+ "wire header_len " + headerLen + " overflows response (" + limit + " bytes)" );
307+ }
308+ return headerLen ;
309+ }
310+
254311 /** Internal: write {@code [u32 BE len | headerJson[0..headerLen] | body]} at position 0. */
255312 static int assembleInto (byte [] headerJson , int headerLen , byte [] body , ByteBuffer target ) {
256313 int total = wireTotalLength (headerLen , body .length );
@@ -499,18 +556,7 @@ private static void writeJsonString(ExposedByteArrayOutputStream out, String s)
499556 * @throws IllegalArgumentException if the wire bytes are malformed
500557 */
501558 static DecodedResponse decodeResponse (byte [] wire ) {
502- if (wire == null || wire .length < 4 ) {
503- throw new IllegalArgumentException (
504- "wire response too short: "
505- + (wire == null ? "null" : wire .length + " bytes" ));
506- }
507- int headerLen = ((wire [0 ] & 0xFF ) << 24 ) | ((wire [1 ] & 0xFF ) << 16 )
508- | ((wire [2 ] & 0xFF ) << 8 ) | (wire [3 ] & 0xFF );
509- if (headerLen < 0 || (long ) 4 + headerLen > wire .length ) {
510- throw new IllegalArgumentException (
511- "wire header_len " + headerLen
512- + " overflows response (" + wire .length + " bytes)" );
513- }
559+ int headerLen = readHeaderLength (wire );
514560 // Manual decode via the allocation-lean WireHeaderReader tokenizer
515561 // (the same parser the DIRECT / streaming header callbacks use)
516562 // instead of a Jackson JsonParser — drops the per-response parser +
0 commit comments