Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
1445 lines
67 KiB
Java
1445 lines
67 KiB
Java
/*
|
|
* Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import jdk.internal.net.http.common.Utils;
|
|
import jdk.internal.net.http.quic.CodingContext;
|
|
import jdk.internal.net.http.quic.PeerConnectionId;
|
|
import jdk.internal.net.http.quic.QuicConnectionIdFactory;
|
|
import jdk.internal.net.http.quic.packets.LongHeader;
|
|
import jdk.internal.net.quic.QuicKeyUnavailableException;
|
|
import jdk.internal.net.quic.QuicOneRttContext;
|
|
import jdk.internal.net.quic.QuicTransportException;
|
|
import jdk.internal.net.quic.QuicVersion;
|
|
import jdk.internal.net.http.quic.frames.CryptoFrame;
|
|
import jdk.internal.net.http.quic.frames.PaddingFrame;
|
|
import jdk.internal.net.http.quic.frames.QuicFrame;
|
|
import jdk.internal.net.http.quic.packets.HandshakePacket;
|
|
import jdk.internal.net.http.quic.packets.InitialPacket;
|
|
import jdk.internal.net.http.quic.packets.LongHeaderPacket;
|
|
import jdk.internal.net.http.quic.packets.OneRttPacket;
|
|
import jdk.internal.net.http.quic.packets.QuicPacket;
|
|
import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType;
|
|
import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace;
|
|
import jdk.internal.net.http.quic.packets.QuicPacket.PacketType;
|
|
import jdk.internal.net.http.quic.packets.QuicPacketDecoder;
|
|
import jdk.internal.net.http.quic.packets.QuicPacketEncoder;
|
|
import jdk.internal.net.http.quic.packets.QuicPacketNumbers;
|
|
import jdk.internal.net.http.quic.packets.RetryPacket;
|
|
import jdk.internal.net.http.quic.packets.ShortHeaderPacket;
|
|
import jdk.internal.net.http.quic.packets.VersionNegotiationPacket;
|
|
import jdk.internal.net.http.quic.packets.ZeroRttPacket;
|
|
import jdk.internal.net.http.quic.QuicConnectionId;
|
|
import jdk.internal.net.quic.QuicTLSEngine;
|
|
import jdk.internal.net.quic.QuicTransportParametersConsumer;
|
|
import jdk.internal.net.http.quic.VariableLengthEncoder;
|
|
|
|
import javax.crypto.AEADBadTagException;
|
|
import javax.net.ssl.SSLParameters;
|
|
import javax.net.ssl.SSLSession;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HexFormat;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.function.IntFunction;
|
|
import java.util.stream.Collectors;
|
|
|
|
import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.MethodSource;
|
|
|
|
/*
|
|
* @test
|
|
* @library /test/lib
|
|
* @summary test packet encoding and decoding in unencrypted form and without
|
|
* any network involvement.
|
|
* @run junit/othervm -Dseed=2646683818688275736 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=-3723256402256409075 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=-3689060484817342283 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=2425718686525936108 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=-2996954753243104355 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=8750823652999067800 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=2906555779406889127 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=902801756808168822 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=5643545543196691308 PacketEncodingTest
|
|
* @run junit/othervm -Dseed=2646683818688275736 PacketEncodingTest
|
|
* @run junit/othervm -Djdk.internal.httpclient.debug=true PacketEncodingTest
|
|
*/
|
|
public class PacketEncodingTest {
|
|
|
|
public static Object[][] longHeaderPacketProvider() {
|
|
final QuicVersion[] quicVersions = QuicVersion.values();
|
|
final List<Object[]> params = new ArrayList<>();
|
|
for (final QuicVersion version : quicVersions) {
|
|
final var p = new Object[][] {
|
|
// quic-version, srcIdLen, dstIdLen, pn, largestAck
|
|
new Object[] {version, 20, 20, 0L, -1L},
|
|
new Object[] {version, 10, 20, 1L, 0L},
|
|
new Object[] {version, 10, 20, 255L, 0L},
|
|
new Object[] {version, 12, 15, 0xFFFFL, 0L},
|
|
new Object[] {version, 9, 8, 0x7FFFFFFFL, 255L},
|
|
new Object[] {version, 13, 11, 0x8FFFFFFFL, 0x10000000L},
|
|
new Object[] {version, 19, 6, 0xFFFFFFFFL, 0xFFFFFFFEL},
|
|
new Object[] {version, 6, 17, 0xFFFFFFFFFFL, 0xFFFFFFFF00L},
|
|
new Object[] {version, 15, 14, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L},
|
|
new Object[] {version, 7, 9, 0xa82f9b32L, 0xa82f30eaL},
|
|
new Object[] {version, 18, 16, 0xace8feL, 0xabe8b3L},
|
|
new Object[] {version, 16, 19, 0xac5c02L, 0xabe8b3L}
|
|
};
|
|
params.addAll(Arrays.asList(p));
|
|
}
|
|
return params.toArray(Object[][]::new);
|
|
}
|
|
|
|
public static Object[][] shortHeaderPacketProvider() {
|
|
final QuicVersion[] quicVersions = QuicVersion.values();
|
|
final List<Object[]> params = new ArrayList<>();
|
|
for (final QuicVersion version : quicVersions) {
|
|
final var p = new Object[][] {
|
|
new Object[] {version, 20, 0L, -1L},
|
|
new Object[] {version, 17, 1L, 0L},
|
|
new Object[] {version, 10, 255L, 0L},
|
|
new Object[] {version, 12, 0xFFFFL, 0L},
|
|
new Object[] {version, 9, 0x7FFFFFFFL, 255L},
|
|
new Object[] {version, 13, 0x8FFFFFFFL, 0x10000000L},
|
|
new Object[] {version, 19, 0xFFFFFFFFL, 0xFFFFFFFEL},
|
|
new Object[] {version, 6, 0xFFFFFFFFFFL, 0xFFFFFFFF00L},
|
|
new Object[] {version, 15, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L},
|
|
new Object[] {version, 7, 0xa82f9b32L, 0xa82f30eaL},
|
|
new Object[] {version, 18, 0xace8feL, 0xabe8b3L},
|
|
new Object[] {version, 16, 0xac5c02L, 0xabe8b3L},
|
|
};
|
|
params.addAll(Arrays.asList(p));
|
|
}
|
|
return params.toArray(Object[][]::new);
|
|
}
|
|
|
|
public static Object[][] versionAndRetryProvider() {
|
|
final QuicVersion[] quicVersions = QuicVersion.values();
|
|
final List<Object[]> params = new ArrayList<>();
|
|
for (final QuicVersion version : quicVersions) {
|
|
final var p = new Object[][] {
|
|
// quic-version, srcIdLen, dstIdLen, pn, largestAck
|
|
new Object[] {version, 20, 20},
|
|
new Object[] {version, 10, 20},
|
|
new Object[] {version, 12, 15},
|
|
new Object[] {version, 9, 8},
|
|
new Object[] {version, 13, 11},
|
|
new Object[] {version, 19, 6},
|
|
new Object[] {version, 6, 17},
|
|
new Object[] {version, 15, 14},
|
|
new Object[] {version, 7, 9},
|
|
new Object[] {version, 18, 16},
|
|
new Object[] {version, 16, 19},
|
|
};
|
|
params.addAll(Arrays.asList(p));
|
|
}
|
|
return params.toArray(Object[][]::new);
|
|
}
|
|
|
|
private static final AtomicLong IDS = new AtomicLong();
|
|
private static final Random RANDOM = jdk.test.lib.RandomFactory.getRandom();
|
|
private static final int MAX_DATAGRAM_IPV6 = 65527;
|
|
|
|
byte[] randomIdBytes(int connectionLength) {
|
|
byte[] bytes = new byte[connectionLength];
|
|
RANDOM.nextBytes(bytes);
|
|
return bytes;
|
|
}
|
|
|
|
private static class DummyQuicTLSEngine implements QuicTLSEngine {
|
|
@Override
|
|
public HandshakeState getHandshakeState() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public boolean isTLSHandshakeComplete() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public KeySpace getCurrentSendKeySpace() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public boolean keysAvailable(KeySpace keySpace) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void discardKeys(KeySpace keySpace) {
|
|
// no-op
|
|
}
|
|
|
|
@Override
|
|
public void setLocalQuicTransportParameters(ByteBuffer params) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public void restartHandshake() throws IOException {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { }
|
|
@Override
|
|
public int getHeaderProtectionSampleSize(KeySpace keySpace) {
|
|
return 0;
|
|
}
|
|
@Override
|
|
public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) {
|
|
return ByteBuffer.allocate(5);
|
|
}
|
|
|
|
@Override
|
|
public int getAuthTagSize() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void encryptPacket(KeySpace keySpace, long packetNumber,
|
|
IntFunction<ByteBuffer> headerGenerator,
|
|
ByteBuffer packetPayload, ByteBuffer output)
|
|
throws QuicKeyUnavailableException, QuicTransportException {
|
|
// this dummy QUIC TLS engine doesn't do any encryption.
|
|
// we just copy over the raw packet payload into the output buffer
|
|
output.put(packetPayload);
|
|
}
|
|
|
|
@Override
|
|
public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase,
|
|
ByteBuffer packet, int headerLength, ByteBuffer output) {
|
|
packet.position(packet.position() + headerLength);
|
|
output.put(packet);
|
|
}
|
|
|
|
@Override
|
|
public void signRetryPacket(QuicVersion version,
|
|
ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) {
|
|
output.put(ByteBuffer.allocate(16));
|
|
}
|
|
@Override
|
|
public void verifyRetryPacket(QuicVersion version,
|
|
ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException {
|
|
}
|
|
@Override
|
|
public ByteBuffer getHandshakeBytes(KeySpace keySpace) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public Runnable getDelegatedTask() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public boolean tryMarkHandshakeDone() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public boolean tryReceiveHandshakeDone() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public Set<QuicVersion> getSupportedQuicVersions() {
|
|
return Set.of(QuicVersion.QUIC_V1);
|
|
}
|
|
|
|
@Override
|
|
public void setUseClientMode(boolean mode) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public boolean getUseClientMode() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public SSLParameters getSSLParameters() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public void setSSLParameters(SSLParameters sslParameters) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public String getApplicationProtocol() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public SSLSession getSession() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public SSLSession getHandshakeSession() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public void versionNegotiated(QuicVersion quicVersion) {
|
|
// no-op
|
|
}
|
|
|
|
@Override
|
|
public void setOneRttContext(QuicOneRttContext ctx) {
|
|
// no-op
|
|
}
|
|
}
|
|
|
|
private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine();
|
|
private static abstract class TestCodingContext implements CodingContext {
|
|
TestCodingContext() { }
|
|
@Override
|
|
public int writePacket(QuicPacket packet, ByteBuffer buffer) {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public QuicPacket parsePacket(ByteBuffer src) throws IOException {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
@Override
|
|
public boolean verifyToken(QuicConnectionId destinationID, byte[] token) {
|
|
return true;
|
|
}
|
|
@Override
|
|
public QuicConnectionId originalServerConnId() {
|
|
throw new AssertionError("should not come here!");
|
|
}
|
|
|
|
@Override
|
|
public QuicTLSEngine getTLSEngine() {
|
|
return TLS_ENGINE;
|
|
}
|
|
|
|
@Override
|
|
public int minShortPacketPayloadSize(int destConnectionIdLength) {
|
|
return 100 - (destConnectionIdLength - connectionIdLength());
|
|
}
|
|
}
|
|
|
|
private void checkLongHeaderPacket(LongHeaderPacket packet,
|
|
PacketType packetType,
|
|
int versionNumber,
|
|
PacketNumberSpace packetNumberSpace,
|
|
long packetNumber,
|
|
QuicConnectionId srcConnectionId,
|
|
QuicConnectionId destConnectionId,
|
|
List<QuicFrame> payload,
|
|
int padding) {
|
|
List<QuicFrame> expected;
|
|
if (padding == 0) {
|
|
expected = payload;
|
|
} else if (payload.get(0) instanceof PaddingFrame pf) {
|
|
expected = new ArrayList<>(payload);
|
|
expected.set(0, new PaddingFrame(padding + pf.size()));
|
|
} else {
|
|
expected = new ArrayList<>(payload.size()+1);
|
|
expected.add(new PaddingFrame(padding));
|
|
expected.addAll(payload);
|
|
}
|
|
checkLongHeaderPacket(packet, packetType, versionNumber, packetNumberSpace, packetNumber,
|
|
srcConnectionId, destConnectionId, expected);
|
|
|
|
}
|
|
|
|
private void checkLongHeaderPacket(LongHeaderPacket packet,
|
|
PacketType packetType,
|
|
int versionNumber,
|
|
PacketNumberSpace packetNumberSpace,
|
|
long packetNumber,
|
|
QuicConnectionId srcConnectionId,
|
|
QuicConnectionId destConnectionId,
|
|
List<QuicFrame> payload) {
|
|
// Check created packet
|
|
assertEquals(HeadersType.LONG, packet.headersType());
|
|
assertEquals(packetType, packet.packetType());
|
|
boolean hasLength = switch (packetType) {
|
|
case VERSIONS, RETRY -> false;
|
|
default -> true;
|
|
};
|
|
assertEquals(hasLength, packet.hasLength());
|
|
assertEquals(packetNumberSpace, packet.numberSpace());
|
|
if (payload == null) {
|
|
assertTrue(packet.frames().isEmpty());
|
|
} else {
|
|
assertEquals(getBuffers(payload), getBuffers(packet.frames()));
|
|
}
|
|
assertEquals(versionNumber, packet.version());
|
|
assertEquals(packetNumber, packet.packetNumber());
|
|
assertEquals(srcConnectionId, packet.sourceId());
|
|
assertEquals(destConnectionId, packet.destinationId());
|
|
|
|
}
|
|
|
|
private static ByteBuffer encodeFrame(QuicFrame frame) {
|
|
ByteBuffer result = ByteBuffer.allocate(frame.size());
|
|
frame.encode(result);
|
|
return result;
|
|
}
|
|
private static List<ByteBuffer> getBuffers(List<QuicFrame> payload) {
|
|
return payload.stream().map(PacketEncodingTest::encodeFrame).toList();
|
|
}
|
|
private static List<ByteBuffer> getBuffers(List<QuicFrame> payload, int minSize) {
|
|
int payloadSize = payload.stream().mapToInt(QuicFrame::size).sum();
|
|
if (payloadSize < minSize) {
|
|
payload = new ArrayList<>(payload);
|
|
payload.add(0, new PaddingFrame(minSize - payloadSize));
|
|
}
|
|
return payload.stream().map(PacketEncodingTest::encodeFrame).toList();
|
|
}
|
|
|
|
private static String toHex(ByteBuffer buffer) {
|
|
byte[] bytes = new byte[buffer.remaining()];
|
|
buffer.get(bytes, 0, buffer.remaining());
|
|
return HexFormat.of().formatHex(bytes);
|
|
}
|
|
|
|
private static String toHex(List<ByteBuffer> byteBuffers) {
|
|
return "0x" + byteBuffers.stream()
|
|
.map(PacketEncodingTest::toHex)
|
|
.collect(Collectors.joining(":"));
|
|
}
|
|
|
|
private void checkLongHeaderPacketAt(ByteBuffer datagram, int offset,
|
|
PacketType packetType, int versionNumber,
|
|
QuicConnectionId srcConnectionId,
|
|
QuicConnectionId destConnectionId) {
|
|
assertEquals(HeadersType.LONG, QuicPacketDecoder.peekHeaderType(datagram, offset));
|
|
assertEquals(packetType, QuicPacketDecoder.of(datagram, offset).peekPacketType(datagram, offset));
|
|
LongHeader header = QuicPacketDecoder.peekLongHeader(datagram, offset);
|
|
assertNotNull(header, "Could not parse packet header");
|
|
assertEquals(versionNumber, header.version());
|
|
assertTrue(header.destinationId()
|
|
.matches(destConnectionId.asReadOnlyBuffer()), "Destination ID doesn't match");
|
|
assertTrue(header.sourceId()
|
|
.matches(srcConnectionId.asReadOnlyBuffer()), "Source ID doesn't match");
|
|
}
|
|
|
|
private List<QuicFrame> frames(byte[] payload) throws IOException {
|
|
return frames(payload, false);
|
|
}
|
|
|
|
private List<QuicFrame> frames(byte[] payload, boolean insert) throws IOException {
|
|
int payloadSize = payload.length;
|
|
ByteBuffer buf = ByteBuffer.wrap(payload);
|
|
List<QuicFrame> frames = new ArrayList<>();
|
|
int remaining = payloadSize;
|
|
while (remaining > 7) {
|
|
int size = RANDOM.nextInt(1, remaining - 6);
|
|
byte[] data = new byte[size];
|
|
RANDOM.nextBytes(data);
|
|
QuicFrame frame = new CryptoFrame(0, size, ByteBuffer.wrap(data));
|
|
int encoded = frame.size();
|
|
assertTrue(encoded > 0, String.valueOf(encoded));
|
|
assertTrue(encoded <= remaining, String.valueOf(encoded));
|
|
if (insert) {
|
|
frames.add(0, frame);
|
|
buf.position(remaining - encoded);
|
|
} else {
|
|
frames.add(frame);
|
|
}
|
|
frame.encode(buf);
|
|
remaining -= encoded;
|
|
}
|
|
if (remaining > 0) {
|
|
var padding = new PaddingFrame(remaining);
|
|
if (insert) {
|
|
frames.add(0, padding);
|
|
buf.position(0);
|
|
} else {
|
|
frames.add(padding);
|
|
}
|
|
padding.encode(buf);
|
|
}
|
|
if (insert) {
|
|
assertEquals(remaining, buf.position());
|
|
assertEquals(payloadSize - remaining, buf.remaining());
|
|
} else {
|
|
assertEquals(0, buf.remaining());
|
|
}
|
|
return List.copyOf(frames);
|
|
}
|
|
|
|
private ByteBuffer toByteBuffer(QuicPacketEncoder encoder, QuicPacket outgoingQuicPacket, CodingContext context)
|
|
throws Exception {
|
|
int size = outgoingQuicPacket.size();
|
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
|
encoder.encode(outgoingQuicPacket, buffer, context);
|
|
assertEquals(size, buffer.position(), " for " + outgoingQuicPacket);
|
|
buffer.flip();
|
|
return buffer;
|
|
}
|
|
|
|
private void checkShortHeaderPacket(ShortHeaderPacket packet,
|
|
PacketType packetType,
|
|
PacketNumberSpace packetNumberSpace,
|
|
long packetNumber,
|
|
QuicConnectionId destConnectionId,
|
|
List<QuicFrame> payload,
|
|
int minSize) {
|
|
// Check created packet
|
|
assertEquals(HeadersType.SHORT, packet.headersType());
|
|
assertEquals(packetType, packet.packetType());
|
|
assertEquals(false, packet.hasLength());
|
|
assertEquals(packetNumberSpace, packet.numberSpace());
|
|
assertEquals(getBuffers(payload, minSize), getBuffers(packet.frames()));
|
|
assertEquals(packetNumber, packet.packetNumber());
|
|
assertEquals(destConnectionId, packet.destinationId());
|
|
}
|
|
|
|
private void checkShortHeaderPacketAt(ByteBuffer datagram, int offset,
|
|
PacketType packetType,
|
|
QuicConnectionId destConnectionId,
|
|
CodingContext context) {
|
|
assertEquals(HeadersType.SHORT, QuicPacketDecoder.peekHeaderType(datagram, offset));
|
|
assertEquals(packetType, QuicPacketDecoder.of(QuicVersion.QUIC_V1).peekPacketType(datagram, offset));
|
|
assertEquals(0, QuicPacketDecoder.peekVersion(datagram, offset));
|
|
int pos = datagram.position();
|
|
if (pos != offset) datagram.position(offset);
|
|
try {
|
|
assertEquals(-1, QuicPacketDecoder.peekShortConnectionId(datagram, destConnectionId.length())
|
|
.mismatch(destConnectionId.asReadOnlyBuffer()));
|
|
} finally {
|
|
if (pos != offset) datagram.position(pos);
|
|
}
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("longHeaderPacketProvider")
|
|
public void testInitialPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength,
|
|
long packetNumber, long largestAcked) throws Exception {
|
|
System.out.printf("%ntestInitialPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
final QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
assertEquals(destConnectionId.length(), destid.length, "dcid length");
|
|
destIdLength = destid.length;
|
|
|
|
byte[] srcid = randomIdBytes(srcIdLength);
|
|
final QuicConnectionId srcConnectionId = new PeerConnectionId(srcid);
|
|
assertEquals(srcConnectionId.length(), srcid.length, "scid length");
|
|
|
|
int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7
|
|
- QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked)
|
|
- VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6);
|
|
|
|
// ensure that bound - tokenLength - 1 > 0
|
|
assert bound > 4;
|
|
int tokenLength = RANDOM.nextInt(bound - 4);
|
|
|
|
byte[] token = tokenLength == 0 ? null : new byte[tokenLength];
|
|
if (token != null) RANDOM.nextBytes(token);
|
|
int packetNumberLength =
|
|
QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked);
|
|
int payloadSize = Math.max(RANDOM.nextInt(bound - tokenLength - 1) + 1, 4 - packetNumberLength);
|
|
System.out.printf("testInitialPacket.encode(scid:%s, dcid:%s, token:%d, payload:%d)%n",
|
|
srcIdLength, destIdLength, tokenLength, payloadSize);
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1;
|
|
}
|
|
@Override public int connectionIdLength() {
|
|
return srcIdLength;
|
|
}
|
|
};
|
|
int minsize = encoder.computeMaxInitialPayloadSize(context,
|
|
computePacketNumberLength(packetNumber,
|
|
context.largestAckedPN(PacketNumberSpace.INITIAL)),
|
|
tokenLength, srcIdLength,
|
|
destIdLength, 1200);
|
|
int padding = (payloadSize < minsize) ? minsize - payloadSize : 0;
|
|
System.out.println("testInitialPacket: available=%s, payload=%s, padding=%s"
|
|
.formatted(minsize, payloadSize, padding));
|
|
|
|
|
|
byte[] payload = new byte[payloadSize];
|
|
List<QuicFrame> frames = frames(payload, padding != 0);
|
|
assertEquals(payloadSize, frames.stream().mapToInt(QuicFrame::size)
|
|
.reduce(0, Math::addExact));
|
|
|
|
|
|
// Create an initial packet
|
|
var packet = encoder.newInitialPacket(srcConnectionId,
|
|
destConnectionId,
|
|
token,
|
|
packetNumber,
|
|
largestAcked,
|
|
frames,
|
|
context);
|
|
|
|
if (padding > 0) {
|
|
var frames2 = new ArrayList<>(frames);
|
|
frames2.add(0, new PaddingFrame(padding));
|
|
var packet2 = encoder.newInitialPacket(srcConnectionId,
|
|
destConnectionId,
|
|
token,
|
|
packetNumber,
|
|
largestAcked,
|
|
frames2,
|
|
context);
|
|
assertEquals(padding + (1200 - packet2.size()), padding);
|
|
}
|
|
|
|
// Check created packet
|
|
assertTrue(packet instanceof InitialPacket);
|
|
var initialPacket = (InitialPacket) packet;
|
|
System.out.printf("%s: pn:%s, tklen:%s, payloadSize:%s, padding:%s, packet::size:%s, " +
|
|
"\n\tinputFrames: %s, " +
|
|
"\n\tencodedFrames:%s%n",
|
|
PacketType.INITIAL, packetNumber, tokenLength, payload.length, padding,
|
|
packet.size(), frames, packet.frames());
|
|
checkLongHeaderPacket(initialPacket, PacketType.INITIAL, quicVersion.versionNumber(),
|
|
PacketNumberSpace.INITIAL, packetNumber,
|
|
srcConnectionId, destConnectionId, frames, padding);
|
|
assertEquals(tokenLength, initialPacket.tokenLength());
|
|
assertEquals(token, initialPacket.token());
|
|
assertEquals(true, initialPacket.hasLength());
|
|
assertEquals(packetNumberLength + payloadSize + padding, initialPacket.length());
|
|
|
|
// Check that peeking at the encoded packet returns correct information
|
|
|
|
// Decode the two packets in the datagram
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkLongHeaderPacketAt(encoded, 0, PacketType.INITIAL, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// coalesce two packets in a single datagram and check
|
|
// the peek methods again
|
|
int offset = RANDOM.nextInt(256);
|
|
int second = offset + encoded.limit();
|
|
System.out.printf("testInitialPacket.encode(offset:%d, second:%d)%n",
|
|
offset, second);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
|
|
// check header, type and version of both packets
|
|
System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n",
|
|
offset, second, datagram.position(), datagram.limit());
|
|
System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, offset, PacketType.INITIAL, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n",
|
|
second, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, second, PacketType.INITIAL, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// check that skip packet can skip both packets
|
|
datagram.position(0);
|
|
datagram.limit(datagram.capacity());
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(second, datagram.position());
|
|
decoder.skipPacket(datagram, second);
|
|
assertEquals(offset, datagram.remaining());
|
|
|
|
datagram.position(offset);
|
|
int size = second - offset;
|
|
for (int i=0; i<2; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof InitialPacket, "decoded: " + decodedPacket);
|
|
InitialPacket initialDecoded = InitialPacket.class.cast(decodedPacket);
|
|
checkLongHeaderPacket(initialDecoded, PacketType.INITIAL, quicVersion.versionNumber(),
|
|
PacketNumberSpace.INITIAL, packetNumber,
|
|
srcConnectionId, destConnectionId, frames, padding);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
assertEquals(tokenLength, initialDecoded.tokenLength());
|
|
assertArrayEquals(token, initialDecoded.token());
|
|
assertEquals(initialPacket.length(), initialDecoded.length());
|
|
assertEquals(packetNumberLength + payloadSize + padding, initialDecoded.length());
|
|
}
|
|
assertEquals(second + second - offset, datagram.position());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("longHeaderPacketProvider")
|
|
public void testHandshakePacket(QuicVersion quicVersion, int srcIdLength, int destIdLength,
|
|
long packetNumber, long largestAcked) throws Exception {
|
|
System.out.printf("%ntestHandshakePacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
byte[] srcid = randomIdBytes(srcIdLength);
|
|
QuicConnectionId srcConnectionId = new PeerConnectionId(srcid);
|
|
int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7
|
|
- QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked)
|
|
- VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6);
|
|
|
|
int packetNumberLength =
|
|
QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked);
|
|
int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength);
|
|
byte[] payload = new byte[payloadSize];
|
|
var frames = frames(payload);
|
|
System.out.printf("testHandshakePacket.encode(payload:%d)%n", payloadSize);
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1;
|
|
}
|
|
@Override public int connectionIdLength() {
|
|
return srcIdLength;
|
|
}
|
|
};
|
|
// Create an initial packet
|
|
var packet = encoder.newHandshakePacket(srcConnectionId,
|
|
destConnectionId,
|
|
packetNumber,
|
|
largestAcked,
|
|
frames,
|
|
context);
|
|
|
|
// Check created packet
|
|
assertTrue(packet instanceof HandshakePacket);
|
|
var handshakePacket = (HandshakePacket) packet;
|
|
checkLongHeaderPacket(handshakePacket, PacketType.HANDSHAKE, quicVersion.versionNumber(),
|
|
PacketNumberSpace.HANDSHAKE, packetNumber,
|
|
srcConnectionId, destConnectionId, frames);
|
|
assertEquals(true, handshakePacket.hasLength());
|
|
assertEquals(packetNumberLength + payloadSize, handshakePacket.length());
|
|
|
|
// Decode the two packets in the datagram
|
|
// Check that peeking at the encoded packet returns correct information
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkLongHeaderPacketAt(encoded, 0, PacketType.HANDSHAKE, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// coalesce two packets in a single datagram and check
|
|
// the peek methods again
|
|
int offset = RANDOM.nextInt(256);
|
|
int second = offset + encoded.limit();
|
|
System.out.printf("testHandshakePacket.encode(offset:%d, second:%d)%n",
|
|
offset, second);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
|
|
// check header, type and version of both packets
|
|
System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n",
|
|
offset, second, datagram.position(), datagram.limit());
|
|
// set position to first packet to check connection ids
|
|
System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, offset, PacketType.HANDSHAKE, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n",
|
|
second, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, second, PacketType.HANDSHAKE, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// check that skip packet can skip both packets
|
|
datagram.position(0);
|
|
datagram.limit(datagram.capacity());
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(second, datagram.position());
|
|
decoder.skipPacket(datagram, second);
|
|
assertEquals(offset, datagram.remaining());
|
|
|
|
datagram.position(offset);
|
|
int size = second - offset;
|
|
for (int i=0; i<2; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof HandshakePacket, "decoded: " + decodedPacket);
|
|
HandshakePacket handshakeDecoded = HandshakePacket.class.cast(decodedPacket);
|
|
checkLongHeaderPacket(handshakeDecoded, PacketType.HANDSHAKE, quicVersion.versionNumber(),
|
|
PacketNumberSpace.HANDSHAKE, packetNumber,
|
|
srcConnectionId, destConnectionId, frames);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
assertEquals(handshakePacket.length(), handshakeDecoded.length());
|
|
assertEquals(packetNumberLength + payloadSize, handshakeDecoded.length());
|
|
}
|
|
assertEquals(second + second - offset, datagram.position());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("longHeaderPacketProvider")
|
|
public void testZeroRTTPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength,
|
|
long packetNumber, long largestAcked) throws Exception {
|
|
System.out.printf("%ntestZeroRTTPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
byte[] srcid = randomIdBytes(srcIdLength);
|
|
QuicConnectionId srcConnectionId = new PeerConnectionId(srcid);
|
|
int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7
|
|
- QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked)
|
|
- VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6);
|
|
|
|
int packetNumberLength =
|
|
QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked);
|
|
int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength);
|
|
byte[] payload = new byte[payloadSize];
|
|
var frames = frames(payload);
|
|
System.out.printf("testZeroRTTPacket.encode(payload:%d)%n", payloadSize);
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1;
|
|
}
|
|
@Override public int connectionIdLength() {
|
|
return srcIdLength;
|
|
}
|
|
};
|
|
// Create an initial packet
|
|
var packet = encoder.newZeroRttPacket(srcConnectionId,
|
|
destConnectionId,
|
|
packetNumber,
|
|
largestAcked,
|
|
frames,
|
|
context);
|
|
|
|
// Check created packet
|
|
assertTrue(packet instanceof ZeroRttPacket);
|
|
var zeroRttPacket = (ZeroRttPacket) packet;
|
|
checkLongHeaderPacket(zeroRttPacket, PacketType.ZERORTT, quicVersion.versionNumber(),
|
|
PacketNumberSpace.APPLICATION, packetNumber,
|
|
srcConnectionId, destConnectionId, frames);
|
|
assertEquals(true, zeroRttPacket.hasLength());
|
|
assertEquals(packetNumberLength + payloadSize, zeroRttPacket.length());
|
|
|
|
// Check that peeking at the encoded packet returns correct information
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkLongHeaderPacketAt(encoded, 0, PacketType.ZERORTT, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// coalesce two packets in a single datagram and check
|
|
// the peek methods again
|
|
int offset = RANDOM.nextInt(256);
|
|
int second = offset + encoded.limit();
|
|
System.out.printf("testZeroRTTPacket.encode(offset:%d, second:%d)%n",
|
|
offset, second);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
|
|
// check header, type and version of both packets
|
|
System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n",
|
|
offset, second, datagram.position(), datagram.limit());
|
|
// set position to first packet to check connection ids
|
|
System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, offset, PacketType.ZERORTT, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n",
|
|
second, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, second, PacketType.ZERORTT, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// check that skip packet can skip both packets
|
|
datagram.position(0);
|
|
datagram.limit(datagram.capacity());
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(second, datagram.position());
|
|
decoder.skipPacket(datagram, second);
|
|
assertEquals(offset, datagram.remaining());
|
|
|
|
// Decode the two packets in the datagram
|
|
datagram.position(offset);
|
|
int size = second - offset;
|
|
for (int i=0; i<2; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof ZeroRttPacket, "decoded: " + decodedPacket);
|
|
ZeroRttPacket zeroRttDecoded = ZeroRttPacket.class.cast(decodedPacket);
|
|
checkLongHeaderPacket(zeroRttDecoded, PacketType.ZERORTT, quicVersion.versionNumber(),
|
|
PacketNumberSpace.APPLICATION, packetNumber,
|
|
srcConnectionId, destConnectionId, frames);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
assertEquals(zeroRttPacket.length(), zeroRttDecoded.length());
|
|
assertEquals(packetNumberLength + payloadSize, zeroRttDecoded.length());
|
|
}
|
|
assertEquals(second + second - offset, datagram.position());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("versionAndRetryProvider")
|
|
public void testVersionNegotiationPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength)
|
|
throws Exception {
|
|
System.out.printf("%ntestVersionNegotiationPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, srcIdLength, destIdLength, -1, -1);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
byte[] srcid = randomIdBytes(srcIdLength);
|
|
QuicConnectionId srcConnectionId = new PeerConnectionId(srcid);
|
|
|
|
final List<Integer> versionList = new ArrayList<>();
|
|
for (final QuicVersion qv : QuicVersion.values()) {
|
|
versionList.add(qv.versionNumber());
|
|
}
|
|
System.out.printf("testVersionNegotiationPacket.encode(versions:%d)%n", versionList.size());
|
|
|
|
// Create an initial packet
|
|
var packet = QuicPacketEncoder.newVersionNegotiationPacket(srcConnectionId,
|
|
destConnectionId,
|
|
versionList.stream().mapToInt(Integer::intValue).toArray());
|
|
|
|
// Check created packet
|
|
assertTrue(packet instanceof VersionNegotiationPacket);
|
|
var versionPacket = (VersionNegotiationPacket) packet;
|
|
checkLongHeaderPacket(versionPacket, PacketType.VERSIONS, 0,
|
|
PacketNumberSpace.NONE, -1,
|
|
srcConnectionId, destConnectionId, null);
|
|
assertEquals(false, versionPacket.hasLength());
|
|
assertArrayEquals(versionList.stream().mapToInt(Integer::intValue).toArray(),
|
|
versionPacket.supportedVersions());
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return -1;
|
|
}
|
|
@Override public int connectionIdLength() {
|
|
return srcIdLength;
|
|
}
|
|
};
|
|
// Check that peeking at the encoded packet returns correct information
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkLongHeaderPacketAt(encoded, 0, PacketType.VERSIONS, 0,
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// version negotiation packets can't be coalesced
|
|
int offset = RANDOM.nextInt(256);
|
|
int end = offset + encoded.limit();
|
|
System.out.printf("testVersionNegotiationPacket.encode(offset:%d, end:%d)%n",
|
|
offset, end);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
|
|
// check header, type and version of both packets
|
|
System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
// set position to first packet to check connection ids
|
|
System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, offset, PacketType.VERSIONS, 0,
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// check that skip packet can skip packet
|
|
datagram.position(0);
|
|
datagram.limit(datagram.capacity());
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(end, datagram.position());
|
|
assertEquals(0, datagram.remaining());
|
|
|
|
// Decode the two packets in the datagram
|
|
datagram.position(offset);
|
|
int size = end - offset;
|
|
for (int i=0; i<1; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof VersionNegotiationPacket, "decoded: " + decodedPacket);
|
|
VersionNegotiationPacket decodedVersion = VersionNegotiationPacket.class.cast(decodedPacket);
|
|
checkLongHeaderPacket(decodedVersion, PacketType.VERSIONS, 0,
|
|
PacketNumberSpace.NONE, -1,
|
|
srcConnectionId, destConnectionId, null);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
assertArrayEquals(versionList.stream().mapToInt(Integer::intValue).toArray(),
|
|
decodedVersion.supportedVersions());
|
|
}
|
|
assertEquals(end, datagram.position());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("versionAndRetryProvider")
|
|
public void testRetryPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength)
|
|
throws Exception {
|
|
System.out.printf("%ntestRetryPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, srcIdLength, destIdLength, -1, -1);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
byte[] srcid = randomIdBytes(srcIdLength);
|
|
QuicConnectionId srcConnectionId = new PeerConnectionId(srcid);
|
|
byte[] origId = randomIdBytes(destIdLength);
|
|
QuicConnectionId origConnectionId = new PeerConnectionId(origId);
|
|
int bound = (MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7);
|
|
|
|
int retryTokenLength = RANDOM.nextInt(bound - 16) + 1;
|
|
byte[] retryToken = new byte[retryTokenLength];
|
|
RANDOM.nextBytes(retryToken);
|
|
System.out.printf("testRetryPacket.encode(token:%d)%n", retryTokenLength);
|
|
int expectedSize = 7 + 16 + destid.length + srcIdLength + retryTokenLength;
|
|
|
|
// Create an initial packet
|
|
var packet = encoder.newRetryPacket(srcConnectionId,
|
|
destConnectionId,
|
|
retryToken);
|
|
|
|
// Check created packet
|
|
assertTrue(packet instanceof RetryPacket);
|
|
var retryPacket = (RetryPacket) packet;
|
|
checkLongHeaderPacket(retryPacket, PacketType.RETRY, quicVersion.versionNumber(),
|
|
PacketNumberSpace.NONE, -1,
|
|
srcConnectionId, destConnectionId, null);
|
|
assertEquals(false, retryPacket.hasLength());
|
|
assertArrayEquals(retryToken, retryPacket.retryToken());
|
|
assertEquals(expectedSize, retryPacket.size());
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return -1;
|
|
}
|
|
@Override public int connectionIdLength() {
|
|
return srcIdLength;
|
|
}
|
|
@Override public QuicConnectionId originalServerConnId() { return origConnectionId; }
|
|
};
|
|
// Check that peeking at the encoded packet returns correct information
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkLongHeaderPacketAt(encoded, 0, PacketType.RETRY, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// version negotiation packets can't be coalesced
|
|
int offset = RANDOM.nextInt(256);
|
|
int end = offset + encoded.limit();
|
|
System.out.printf("testRetryPacket.encode(offset:%d, end:%d)%n",
|
|
offset, end);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
|
|
// check header, type and version of both packets
|
|
System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
// set position to first packet to check connection ids
|
|
System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkLongHeaderPacketAt(datagram, offset, PacketType.RETRY, quicVersion.versionNumber(),
|
|
srcConnectionId, destConnectionId);
|
|
|
|
// check that skip packet can skip packet
|
|
datagram.position(0);
|
|
datagram.limit(datagram.capacity());
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(end, datagram.position());
|
|
assertEquals(0, datagram.remaining());
|
|
|
|
// Decode the two packets in the datagram
|
|
datagram.position(offset);
|
|
int size = end - offset;
|
|
for (int i=0; i<1; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof RetryPacket, "decoded: " + decodedPacket);
|
|
RetryPacket decodedRetry = RetryPacket.class.cast(decodedPacket);
|
|
checkLongHeaderPacket(decodedRetry, PacketType.RETRY, quicVersion.versionNumber(),
|
|
PacketNumberSpace.NONE, -1,
|
|
srcConnectionId, destConnectionId, null);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
assertEquals(expectedSize, decodedPacket.size());
|
|
assertArrayEquals(retryToken, decodedRetry.retryToken());
|
|
}
|
|
assertEquals(end, datagram.position());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@MethodSource("shortHeaderPacketProvider")
|
|
public void testOneRTTPacket(QuicVersion quicVersion, int destIdLength,
|
|
long packetNumber, long largestAcked) throws Exception {
|
|
System.out.printf("%ntestOneRTTPacket(qv:%s, dcid:%d, pn:%d, ack:%d)%n",
|
|
quicVersion, destIdLength, packetNumber, largestAcked);
|
|
QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion);
|
|
QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion);
|
|
byte[] destid = QuicConnectionIdFactory.getClient()
|
|
.newConnectionId(destIdLength, IDS.incrementAndGet());
|
|
assert destid.length <= 20;
|
|
QuicConnectionId destConnectionId = new PeerConnectionId(destid);
|
|
int bound = MAX_DATAGRAM_IPV6 - destid.length - 7
|
|
- QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked)
|
|
- VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6);
|
|
|
|
int packetNumberLength =
|
|
QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked);
|
|
int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength);
|
|
byte[] payload = new byte[payloadSize];
|
|
var frames = frames(payload);
|
|
|
|
CodingContext context = new TestCodingContext() {
|
|
@Override public long largestProcessedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1;
|
|
}
|
|
@Override public long largestAckedPN(PacketNumberSpace packetSpace) {
|
|
return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1;
|
|
}
|
|
// since we're going to decode the short packet, we need to return
|
|
// the same length that was used as destination cid in the packet
|
|
@Override public int connectionIdLength() {
|
|
return destid.length;
|
|
}
|
|
};
|
|
|
|
int paddedPayLoadSize = Math.max(payloadSize + packetNumberLength, context.minShortPacketPayloadSize(destid.length));
|
|
System.out.printf("testOneRTTPacket.encode(payload:%d, padded:%d, destid.length: %d)%n",
|
|
payloadSize, paddedPayLoadSize, destid.length);
|
|
int expectedSize = 1 + destid.length + paddedPayLoadSize;
|
|
// Create an 1-RTT packet
|
|
OneRttPacket packet = encoder.newOneRttPacket(destConnectionId,
|
|
packetNumber,
|
|
largestAcked,
|
|
frames,
|
|
context);
|
|
|
|
int minPayloadSize = context.minShortPacketPayloadSize(destConnectionId.length()) - packetNumberLength;
|
|
checkShortHeaderPacket(packet, PacketType.ONERTT,
|
|
PacketNumberSpace.APPLICATION, packetNumber,
|
|
destConnectionId, frames, minPayloadSize);
|
|
assertEquals(false, packet.hasLength());
|
|
assertEquals(expectedSize, packet.size());
|
|
|
|
// Check that peeking at the encoded packet returns correct information
|
|
ByteBuffer encoded = toByteBuffer(encoder, packet, context);
|
|
checkShortHeaderPacketAt(encoded, 0, PacketType.ONERTT,
|
|
destConnectionId, context);
|
|
|
|
// write packet at an offset in the datagram to simulate
|
|
// short packet coalesced after long packet and check
|
|
// the peek methods again
|
|
int offset = RANDOM.nextInt(256);
|
|
int end = offset + encoded.limit();
|
|
System.out.printf("testOneRTTPacket.encode(offset:%d, end:%d)%n",
|
|
offset, end);
|
|
ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset * 2);
|
|
datagram.position(offset);
|
|
datagram.put(encoded);
|
|
encoded.flip();
|
|
datagram.flip();
|
|
assert datagram.limit() == offset + encoded.remaining();
|
|
|
|
// set position to first packet to check connection ids
|
|
System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n",
|
|
offset, datagram.position(), datagram.limit());
|
|
checkShortHeaderPacketAt(datagram, offset, PacketType.ONERTT,
|
|
destConnectionId, context);
|
|
|
|
// check that skip packet can skip packet at offset
|
|
datagram.position(0);
|
|
datagram.limit(end);
|
|
decoder.skipPacket(datagram, offset);
|
|
assertEquals(offset + expectedSize, datagram.position());
|
|
assertEquals(datagram.limit(), datagram.position());
|
|
assertEquals(datagram.capacity() - offset, datagram.position());
|
|
|
|
|
|
// Decode the packet in the datagram
|
|
datagram.position(offset);
|
|
int size = expectedSize;
|
|
for (int i=0; i<1; i++) {
|
|
int pos = datagram.position();
|
|
System.out.printf("Decoding packet: %d at %d%n", (i+1), pos);
|
|
var decodedPacket = decoder.decode(datagram, context);
|
|
assertEquals(pos + size, datagram.position());
|
|
assertTrue(decodedPacket instanceof OneRttPacket, "decoded: " + decodedPacket);
|
|
OneRttPacket oneRttDecoded = OneRttPacket.class.cast(decodedPacket);
|
|
List<QuicFrame> expectedFrames = frames;
|
|
if (frames.size() > 0 && frames.get(0) instanceof PaddingFrame) {
|
|
// The first frame should be a crypto frame, except if payloadSize
|
|
// was less than 7.
|
|
int frameSizes = frames.stream().mapToInt(QuicFrame::size).sum();
|
|
assert frameSizes == payloadSize;
|
|
assert frameSizes <= 7;
|
|
// decoder will coalesce padding frames. So instead of finding
|
|
// two padding frames in the decoded packet we will find just one.
|
|
// To make the check pass, we should expect a bigger padding frame.
|
|
if (minPayloadSize > frameSizes) {
|
|
// replace the first frame with a bigger padding frame
|
|
expectedFrames = new ArrayList<>(frames);
|
|
var first = frames.get(0);
|
|
// replace the first frame with a bigger padding frame that
|
|
// coalesce the first padding payload frame with the padding that
|
|
// should have been added by the encoder.
|
|
// We will then be able to check that the decoded packet contains
|
|
// that single bigger padding frame.
|
|
expectedFrames.set(0, new PaddingFrame(minPayloadSize - frameSizes + first.size()));
|
|
}
|
|
}
|
|
checkShortHeaderPacket(oneRttDecoded, PacketType.ONERTT,
|
|
PacketNumberSpace.APPLICATION, packetNumber,
|
|
destConnectionId, expectedFrames, minPayloadSize);
|
|
assertEquals(packet.size(), decodedPacket.size());
|
|
assertEquals(size, decodedPacket.size());
|
|
}
|
|
assertEquals(offset + size, datagram.position());
|
|
assertEquals(0, datagram.remaining());
|
|
assertEquals(end, datagram.limit());
|
|
|
|
}
|
|
|
|
@Test
|
|
public void testNoMismatch() {
|
|
List<ByteBuffer> match1 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4}),
|
|
ByteBuffer.wrap(new byte[] {5, 6}),
|
|
ByteBuffer.wrap(new byte[] {7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11, 12}),
|
|
ByteBuffer.wrap(new byte[] {13, 14, 15, 16}),
|
|
ByteBuffer.wrap(new byte[] {17, 18, 19, 20})
|
|
);
|
|
List<ByteBuffer> match2 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4, 5}),
|
|
ByteBuffer.wrap(new byte[] {6}),
|
|
ByteBuffer.wrap(new byte[] {7}),
|
|
ByteBuffer.wrap(new byte[] {8, 9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20})
|
|
);
|
|
List<ByteBuffer> match3 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9, 10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20})
|
|
);
|
|
assertEquals(-1, Utils.mismatch(match1, match1));
|
|
assertEquals(-1, Utils.mismatch(match2, match2));
|
|
assertEquals(-1, Utils.mismatch(match3, match3));
|
|
assertEquals(-1, Utils.mismatch(match1, match2));
|
|
assertEquals(-1, Utils.mismatch(match2, match1));
|
|
assertEquals(-1, Utils.mismatch(match1, match3));
|
|
assertEquals(-1, Utils.mismatch(match3, match1));
|
|
assertEquals(-1, Utils.mismatch(match2, match3));
|
|
assertEquals(-1, Utils.mismatch(match3, match2));
|
|
}
|
|
|
|
@Test
|
|
public void testMismatch() {
|
|
// match1, match2, match3 match with each others
|
|
List<ByteBuffer> match1 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4}),
|
|
ByteBuffer.wrap(new byte[] {5, 6}),
|
|
ByteBuffer.wrap(new byte[] {7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11, 12}),
|
|
ByteBuffer.wrap(new byte[] {13, 14, 15, 16}),
|
|
ByteBuffer.wrap(new byte[] {17, 18, 19, 20})
|
|
);
|
|
List<ByteBuffer> match2 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4, 5}),
|
|
ByteBuffer.wrap(new byte[] {6}),
|
|
ByteBuffer.wrap(new byte[] {7}),
|
|
ByteBuffer.wrap(new byte[] {8, 9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20})
|
|
);
|
|
List<ByteBuffer> match3 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9, 10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20})
|
|
);
|
|
// nomatch0, nomatch10, nomatch19 differ from the previous
|
|
// list at some index in [0..20[
|
|
// nomatch0 mismatches at index 0
|
|
List<ByteBuffer> nomatch0 = List.of(
|
|
ByteBuffer.wrap(new byte[] {21, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4}),
|
|
ByteBuffer.wrap(new byte[] {5, 6}),
|
|
ByteBuffer.wrap(new byte[] {7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11, 12}),
|
|
ByteBuffer.wrap(new byte[] {13, 14, 15, 16}),
|
|
ByteBuffer.wrap(new byte[] {17, 18, 19, 20})
|
|
);
|
|
// nomatch10 mismatches at index 10
|
|
List<ByteBuffer> nomatch10 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4, 5}),
|
|
ByteBuffer.wrap(new byte[] {6}),
|
|
ByteBuffer.wrap(new byte[] {7}),
|
|
ByteBuffer.wrap(new byte[] {8, 9}),
|
|
ByteBuffer.wrap(new byte[] {10, 31}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20})
|
|
);
|
|
// nomatch19 mismatches at index 19
|
|
List<ByteBuffer> nomatch19 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9, 10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 40})
|
|
);
|
|
// morematch1 has one more byte at the end
|
|
List<ByteBuffer> morematch1 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4}),
|
|
ByteBuffer.wrap(new byte[] {5, 6}),
|
|
ByteBuffer.wrap(new byte[] {7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11, 12}),
|
|
ByteBuffer.wrap(new byte[] {13, 14, 15, 16}),
|
|
ByteBuffer.wrap(new byte[] {17, 18, 19, 20, 41})
|
|
);
|
|
// morematch2 and morematch3 have the same 3 additional
|
|
// bytes at the end
|
|
List<ByteBuffer> morematch2 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3}),
|
|
ByteBuffer.wrap(new byte[] {4, 5}),
|
|
ByteBuffer.wrap(new byte[] {6}),
|
|
ByteBuffer.wrap(new byte[] {7}),
|
|
ByteBuffer.wrap(new byte[] {8, 9}),
|
|
ByteBuffer.wrap(new byte[] {10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20}),
|
|
ByteBuffer.wrap(new byte[] {41, 42, 43})
|
|
);
|
|
List<ByteBuffer> morematch3 = List.of(
|
|
ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}),
|
|
ByteBuffer.wrap(new byte[] {9, 10, 11}),
|
|
ByteBuffer.wrap(new byte[] {12, 13, 14}),
|
|
ByteBuffer.wrap(new byte[] {15}),
|
|
ByteBuffer.wrap(new byte[] {16, 17}),
|
|
ByteBuffer.wrap(new byte[] {18, 19, 20, 41, 42, 43})
|
|
);
|
|
|
|
assertEquals(-1L, Utils.mismatch(nomatch0, nomatch0));
|
|
assertEquals(-1L, Utils.mismatch(nomatch10, nomatch10));
|
|
assertEquals(-1L, Utils.mismatch(nomatch19, nomatch19));
|
|
assertEquals(-1L, Utils.mismatch(morematch1, morematch1));
|
|
assertEquals(-1L, Utils.mismatch(morematch2, morematch2));
|
|
assertEquals(-1L, Utils.mismatch(morematch3, morematch3));
|
|
assertEquals(-1L, Utils.mismatch(morematch2, morematch3));
|
|
assertEquals(-1L, Utils.mismatch(morematch3, morematch2));
|
|
|
|
for (var match : List.of(match1, match2, match3)) {
|
|
assertEquals(0L, Utils.mismatch(match, nomatch0));
|
|
assertEquals(10L, Utils.mismatch(match, nomatch10));
|
|
assertEquals(19L, Utils.mismatch(match, nomatch19));
|
|
assertEquals(0L, Utils.mismatch(nomatch0, match));
|
|
assertEquals(10L, Utils.mismatch(nomatch10, match));
|
|
assertEquals(19L, Utils.mismatch(nomatch19, match));
|
|
for (var morematch : List.of(morematch1, morematch2, morematch3)) {
|
|
assertEquals(20L, Utils.mismatch(match, morematch));
|
|
assertEquals(20L, Utils.mismatch(morematch, match));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|