diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d8663fab1a..4d1e8a8be3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -327,8 +327,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-x64 - runs-on: 'macos-13' - xcode-toolset-version: '14.3.1' + runs-on: 'macos-15-intel' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -340,8 +340,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-aarch64 - runs-on: 'macos-14' - xcode-toolset-version: '15.4' + runs-on: 'macos-15' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -432,9 +432,9 @@ jobs: with: platform: macos-aarch64 bootjdk-platform: macos-aarch64 - runs-on: macos-14 + runs-on: macos-15 dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} - xcode-toolset-version: '15.4' + xcode-toolset-version: '16.4' debug-suffix: -debug test-windows-x64: diff --git a/.gitignore b/.gitignore index 9145a9fa67b..852b692f99b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ NashornProfile.txt **/core.[0-9]* *.rej *.orig +test/benchmarks/**/target diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html index 7be6867b3ca..f1c25dab7f4 100644 --- a/doc/hotspot-style.html +++ b/doc/hotspot-style.html @@ -1859,8 +1859,6 @@ difference.

Additional Undecided Features

- + * * This ensures that the natural ordering of {@code Double} * objects imposed by this method is consistent with * equals; see {@linkplain ##equivalenceRelation this * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * The inclusion of a total order idiom in the Java SE API + * predates the inclusion of that functionality in the IEEE 754 + * standard. The ordering of the totalOrder predicate chosen by + * IEEE 754 differs from the total order chosen by this method. + * While this method treats all NaN representations as being in + * the same equivalence class, the IEEE 754 total order defines an + * ordering based on the bit patterns of the NaN among the + * different NaN representations. The IEEE 754 order regards + * "negative" NaN representations, that is NaN representations + * whose sign bit is set, to be less than any finite or infinite + * value and less than any "positive" NaN. In addition, the IEEE + * order regards all positive NaN values as greater than positive + * infinity. See the IEEE 754 standard for full details of its + * total ordering. + * * @param anotherDouble the {@code Double} to be compared. * @return the value {@code 0} if {@code anotherDouble} is * numerically equal to this {@code Double}; a value diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java index 3ee4f4ce619..c553dc41c2c 100644 --- a/src/java.base/share/classes/java/lang/Float.java +++ b/src/java.base/share/classes/java/lang/Float.java @@ -70,9 +70,6 @@ import jdk.internal.vm.annotation.IntrinsicCandidate; * @spec https://standards.ieee.org/ieee/754/6210/ * IEEE Standard for Floating-Point Arithmetic * - * @author Lee Boynton - * @author Arthur van Hoff - * @author Joseph D. Darcy * @since 1.0 */ @jdk.internal.ValueBased @@ -411,7 +408,6 @@ public final class Float extends Number * @param f the {@code float} to be converted. * @return a hex string representation of the argument. * @since 1.5 - * @author Joseph D. Darcy */ public static String toHexString(float f) { if (Math.abs(f) < Float.MIN_NORMAL @@ -420,10 +416,12 @@ public final class Float extends Number // replace subnormal double exponent with subnormal float // exponent String s = Double.toHexString(Math.scalb((double)f, - /* -1022+126 */ - Double.MIN_EXPONENT- + // -1022 + 126 + Double.MIN_EXPONENT - Float.MIN_EXPONENT)); - return s.replaceFirst("p-1022$", "p-126"); + // The char sequence "-1022" can only appear in the + // representation of the exponent, not in the (hex) significand. + return s.replace("-1022", "-126"); } else // double string will be the same as float string return Double.toHexString(f); @@ -1253,6 +1251,10 @@ public final class Float extends Number * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * For a discussion of differences between the total order of this + * method compared to the total order defined by the IEEE 754 + * standard, see the note in {@link Double#compareTo(Double)}. * * @param anotherFloat the {@code Float} to be compared. * @return the value {@code 0} if {@code anotherFloat} is diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java index 41487a469b6..20d1edb6d5f 100644 --- a/src/java.base/share/classes/java/lang/Integer.java +++ b/src/java.base/share/classes/java/lang/Integer.java @@ -68,10 +68,6 @@ import static java.lang.String.UTF16; * Delight, (Addison Wesley, 2002) and Hacker's * Delight, Second Edition, (Pearson Education, 2013). * - * @author Lee Boynton - * @author Arthur van Hoff - * @author Josh Bloch - * @author Joseph D. Darcy * @since 1.0 */ @jdk.internal.ValueBased @@ -367,15 +363,9 @@ public final class Integer extends Number // assert shift > 0 && shift <=5 : "Illegal shift value"; int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val); int chars = Math.max(((mag + (shift - 1)) / shift), 1); - if (COMPACT_STRINGS) { - byte[] buf = new byte[chars]; - formatUnsignedInt(val, shift, buf, chars); - return new String(buf, LATIN1); - } else { - byte[] buf = new byte[chars * 2]; - formatUnsignedIntUTF16(val, shift, buf, chars); - return new String(buf, UTF16); - } + byte[] buf = new byte[chars]; + formatUnsignedInt(val, shift, buf, chars); + return String.newStringWithLatin1Bytes(buf); } /** @@ -398,26 +388,6 @@ public final class Integer extends Number } while (charPos > 0); } - /** - * Format an {@code int} (treated as unsigned) into a byte buffer (UTF16 version). If - * {@code len} exceeds the formatted ASCII representation of {@code val}, - * {@code buf} will be padded with leading zeroes. - * - * @param val the unsigned int to format - * @param shift the log2 of the base to format in (4 for hex, 3 for octal, 1 for binary) - * @param buf the byte buffer to write to - * @param len the number of characters to write - */ - private static void formatUnsignedIntUTF16(int val, int shift, byte[] buf, int len) { - int charPos = len; - int radix = 1 << shift; - int mask = radix - 1; - do { - StringUTF16.putChar(buf, --charPos, Integer.digits[val & mask]); - val >>>= shift; - } while (charPos > 0); - } - /** * Returns a {@code String} object representing the * specified integer. The argument is converted to signed decimal @@ -431,15 +401,9 @@ public final class Integer extends Number @IntrinsicCandidate public static String toString(int i) { int size = DecimalDigits.stringSize(i); - if (COMPACT_STRINGS) { - byte[] buf = new byte[size]; - DecimalDigits.uncheckedGetCharsLatin1(i, size, buf); - return new String(buf, LATIN1); - } else { - byte[] buf = new byte[size * 2]; - DecimalDigits.uncheckedGetCharsUTF16(i, size, buf); - return new String(buf, UTF16); - } + byte[] buf = new byte[size]; + DecimalDigits.uncheckedGetCharsLatin1(i, size, buf); + return String.newStringWithLatin1Bytes(buf); } /** diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java index 2fb2d18a78c..b0477fdab6d 100644 --- a/src/java.base/share/classes/java/lang/Long.java +++ b/src/java.base/share/classes/java/lang/Long.java @@ -68,10 +68,6 @@ import static java.lang.String.UTF16; * Delight, (Addison Wesley, 2002) and Hacker's * Delight, Second Edition, (Pearson Education, 2013). * - * @author Lee Boynton - * @author Arthur van Hoff - * @author Josh Bloch - * @author Joseph D. Darcy * @since 1.0 */ @jdk.internal.ValueBased @@ -395,15 +391,9 @@ public final class Long extends Number // assert shift > 0 && shift <=5 : "Illegal shift value"; int mag = Long.SIZE - Long.numberOfLeadingZeros(val); int chars = Math.max(((mag + (shift - 1)) / shift), 1); - if (COMPACT_STRINGS) { - byte[] buf = new byte[chars]; - formatUnsignedLong0(val, shift, buf, 0, chars); - return new String(buf, LATIN1); - } else { - byte[] buf = new byte[chars * 2]; - formatUnsignedLong0UTF16(val, shift, buf, 0, chars); - return new String(buf, UTF16); - } + byte[] buf = new byte[chars]; + formatUnsignedLong0(val, shift, buf, 0, chars); + return String.newStringWithLatin1Bytes(buf); } /** @@ -427,27 +417,6 @@ public final class Long extends Number } while (charPos > offset); } - /** - * Format a long (treated as unsigned) into a byte buffer (UTF16 version). If - * {@code len} exceeds the formatted ASCII representation of {@code val}, - * {@code buf} will be padded with leading zeroes. - * - * @param val the unsigned long to format - * @param shift the log2 of the base to format in (4 for hex, 3 for octal, 1 for binary) - * @param buf the byte buffer to write to - * @param offset the offset in the destination buffer to start at - * @param len the number of characters to write - */ - private static void formatUnsignedLong0UTF16(long val, int shift, byte[] buf, int offset, int len) { - int charPos = offset + len; - int radix = 1 << shift; - int mask = radix - 1; - do { - StringUTF16.putChar(buf, --charPos, Integer.digits[((int) val) & mask]); - val >>>= shift; - } while (charPos > offset); - } - /** * Returns a {@code String} object representing the specified * {@code long}. The argument is converted to signed decimal @@ -460,15 +429,9 @@ public final class Long extends Number */ public static String toString(long i) { int size = DecimalDigits.stringSize(i); - if (COMPACT_STRINGS) { - byte[] buf = new byte[size]; - DecimalDigits.uncheckedGetCharsLatin1(i, size, buf); - return new String(buf, LATIN1); - } else { - byte[] buf = new byte[size * 2]; - DecimalDigits.uncheckedGetCharsUTF16(i, size, buf); - return new String(buf, UTF16); - } + byte[] buf = new byte[size]; + DecimalDigits.uncheckedGetCharsLatin1(i, size, buf); + return String.newStringWithLatin1Bytes(buf); } /** diff --git a/src/java.base/share/classes/java/lang/Math.java b/src/java.base/share/classes/java/lang/Math.java index ef5d1214b11..0f39ecf0a8a 100644 --- a/src/java.base/share/classes/java/lang/Math.java +++ b/src/java.base/share/classes/java/lang/Math.java @@ -2529,7 +2529,6 @@ public final class Math { * * @param d the floating-point value whose ulp is to be returned * @return the size of an ulp of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static double ulp(double d) { @@ -2576,7 +2575,6 @@ public final class Math { * * @param f the floating-point value whose ulp is to be returned * @return the size of an ulp of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static float ulp(float f) { @@ -2617,7 +2615,6 @@ public final class Math { * * @param d the floating-point value whose signum is to be returned * @return the signum function of the argument - * @author Joseph D. Darcy * @since 1.5 */ @IntrinsicCandidate @@ -2639,7 +2636,6 @@ public final class Math { * * @param f the floating-point value whose signum is to be returned * @return the signum function of the argument - * @author Joseph D. Darcy * @since 1.5 */ @IntrinsicCandidate diff --git a/src/java.base/share/classes/java/lang/Object.java b/src/java.base/share/classes/java/lang/Object.java index 11dcab1b005..185882cc7cc 100644 --- a/src/java.base/share/classes/java/lang/Object.java +++ b/src/java.base/share/classes/java/lang/Object.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 2025, 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 @@ -383,7 +383,7 @@ public class Object { try { wait0(timeoutMillis); } catch (InterruptedException e) { - // virtual thread's interrupt status needs to be cleared + // virtual thread's interrupted status needs to be cleared vthread.getAndClearInterrupt(); throw e; } diff --git a/src/java.base/share/classes/java/lang/Process.java b/src/java.base/share/classes/java/lang/Process.java index 0a55343926f..577ee538326 100644 --- a/src/java.base/share/classes/java/lang/Process.java +++ b/src/java.base/share/classes/java/lang/Process.java @@ -774,7 +774,7 @@ public abstract class Process { * @implSpec * This implementation executes {@link #waitFor()} in a separate thread * repeatedly until it returns successfully. If the execution of - * {@code waitFor} is interrupted, the thread's interrupt status is preserved. + * {@code waitFor} is interrupted, the thread's interrupted status is preserved. *

* When {@link #waitFor()} returns successfully the CompletableFuture is * {@linkplain java.util.concurrent.CompletableFuture#complete completed} regardless diff --git a/src/java.base/share/classes/java/lang/Short.java b/src/java.base/share/classes/java/lang/Short.java index f0ae8b28e45..4c64427b6df 100644 --- a/src/java.base/share/classes/java/lang/Short.java +++ b/src/java.base/share/classes/java/lang/Short.java @@ -55,8 +55,6 @@ import static java.lang.constant.ConstantDescs.DEFAULT_NAME; * use instances for synchronization, or unpredictable behavior may * occur. For example, in a future release, synchronization may fail. * - * @author Nakul Saraiya - * @author Joseph D. Darcy * @see java.lang.Number * @since 1.1 */ diff --git a/src/java.base/share/classes/java/lang/StrictMath.java b/src/java.base/share/classes/java/lang/StrictMath.java index 266d98e3947..499fce73aee 100644 --- a/src/java.base/share/classes/java/lang/StrictMath.java +++ b/src/java.base/share/classes/java/lang/StrictMath.java @@ -101,7 +101,6 @@ import jdk.internal.vm.annotation.IntrinsicCandidate; * @spec https://standards.ieee.org/ieee/754/6210/ * IEEE Standard for Floating-Point Arithmetic * - * @author Joseph D. Darcy * @since 1.3 */ public final class StrictMath { @@ -493,7 +492,6 @@ public final class StrictMath { * @param a a value. * @return the closest floating-point value to {@code a} that is * equal to a mathematical integer. - * @author Joseph D. Darcy */ public static double rint(double a) { /* @@ -2014,7 +2012,6 @@ public final class StrictMath { * * @param d the floating-point value whose ulp is to be returned * @return the size of an ulp of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static double ulp(double d) { @@ -2041,7 +2038,6 @@ public final class StrictMath { * * @param f the floating-point value whose ulp is to be returned * @return the size of an ulp of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static float ulp(float f) { @@ -2062,7 +2058,6 @@ public final class StrictMath { * * @param d the floating-point value whose signum is to be returned * @return the signum function of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static double signum(double d) { @@ -2083,7 +2078,6 @@ public final class StrictMath { * * @param f the floating-point value whose signum is to be returned * @return the signum function of the argument - * @author Joseph D. Darcy * @since 1.5 */ public static float signum(float f) { diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java index 24ead22e283..52f908c9e98 100644 --- a/src/java.base/share/classes/java/lang/String.java +++ b/src/java.base/share/classes/java/lang/String.java @@ -914,11 +914,10 @@ public final class String return ba; } - int blen = (coder == LATIN1) ? ae.encodeFromLatin1(val, 0, len, ba) - : ae.encodeFromUTF16(val, 0, len, ba); - if (blen != -1) { - return trimArray(ba, blen); - } + int blen = coder == LATIN1 + ? ae.encodeFromLatin1(val, 0, len, ba, 0) + : ae.encodeFromUTF16(val, 0, len, ba, 0); + return trimArray(ba, blen); } byte[] ba = new byte[en]; @@ -3710,7 +3709,7 @@ public final class String if (len < 0L || (len <<= coder) != (int) len) { throw new OutOfMemoryError("Requested string length exceeds VM limit"); } - byte[] value = StringConcatHelper.newArray(len); + byte[] value = StringConcatHelper.newArray((int) len); int off = 0; prefix.getBytes(value, off, coder); off += prefix.length(); diff --git a/src/java.base/share/classes/java/lang/StringConcatHelper.java b/src/java.base/share/classes/java/lang/StringConcatHelper.java index a5f6abbfdf1..aeff494cabb 100644 --- a/src/java.base/share/classes/java/lang/StringConcatHelper.java +++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java @@ -141,262 +141,6 @@ final class StringConcatHelper { // no instantiation } - /** - * Return the coder for the character. - * @param value character - * @return coder - */ - static long coder(char value) { - return StringLatin1.canEncode(value) ? LATIN1 : UTF16; - } - - /** - * Check for overflow, throw exception on overflow. - * - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @return the given parameter value, if valid - */ - private static long checkOverflow(long lengthCoder) { - if ((int)lengthCoder >= 0) { - return lengthCoder; - } - throw new OutOfMemoryError("Overflow: String length out of range"); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, boolean value) { - return checkOverflow(lengthCoder + (value ? 4 : 5)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, char value) { - return checkOverflow(lengthCoder + 1) | coder(value); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, int value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, long value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, String value) { - lengthCoder += value.length(); - if (!value.isLatin1()) { - lengthCoder |= UTF16; - } - return checkOverflow(lengthCoder); - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, boolean value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - if (value) { - index -= 4; - buf[index] = 't'; - buf[index + 1] = 'r'; - buf[index + 2] = 'u'; - buf[index + 3] = 'e'; - } else { - index -= 5; - buf[index] = 'f'; - buf[index + 1] = 'a'; - buf[index + 2] = 'l'; - buf[index + 3] = 's'; - buf[index + 4] = 'e'; - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - if (value) { - index -= 4; - StringUTF16.putChar(buf, index, 't'); - StringUTF16.putChar(buf, index + 1, 'r'); - StringUTF16.putChar(buf, index + 2, 'u'); - StringUTF16.putChar(buf, index + 3, 'e'); - } else { - index -= 5; - StringUTF16.putChar(buf, index, 'f'); - StringUTF16.putChar(buf, index + 1, 'a'); - StringUTF16.putChar(buf, index + 2, 'l'); - StringUTF16.putChar(buf, index + 3, 's'); - StringUTF16.putChar(buf, index + 4, 'e'); - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value char value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, char value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - buf[--index] = (byte) (value & 0xFF); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - StringUTF16.putChar(buf, --index, value); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value int value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, int value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value long value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, long value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, String value, String prefix) { - int index = ((int)indexCoder) - value.length(); - if (indexCoder < UTF16) { - value.getBytes(buf, index, String.LATIN1); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - value.getBytes(buf, index, String.UTF16); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Instantiates the String with given buffer and coder - * @param buf buffer to use - * @param indexCoder remaining index (should be zero) and coder - * @return String resulting string - */ - static String newString(byte[] buf, long indexCoder) { - // Use the private, non-copying constructor (unsafe!) - if (indexCoder == LATIN1) { - return new String(buf, String.LATIN1); - } else if (indexCoder == UTF16) { - return new String(buf, String.UTF16); - } else { - throw new InternalError("Storage is not completely initialized, " + - (int)indexCoder + " bytes left"); - } - } - /** * Perform a simple concatenation between two objects. Added for startup * performance, but also demonstrates the code that would be emitted by @@ -466,10 +210,6 @@ final class StringConcatHelper { return (value == null || (s = value.toString()) == null) ? "null" : s; } - private static final long LATIN1 = (long)String.LATIN1 << 32; - - private static final long UTF16 = (long)String.UTF16 << 32; - private static final Unsafe UNSAFE = Unsafe.getUnsafe(); static String stringOf(float value) { @@ -530,41 +270,6 @@ final class StringConcatHelper { return checkOverflow(length + value.length()); } - /** - * Allocates an uninitialized byte array based on the length and coder - * information, then prepends the given suffix string at the end of the - * byte array before returning it. The calling code must adjust the - * indexCoder so that it's taken the coder of the suffix into account, but - * subtracted the length of the suffix. - * - * @param suffix - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArrayWithSuffix(String suffix, long indexCoder) { - byte[] buf = newArray(indexCoder + suffix.length()); - if (indexCoder < UTF16) { - suffix.getBytes(buf, (int)indexCoder, String.LATIN1); - } else { - suffix.getBytes(buf, (int)indexCoder, String.UTF16); - } - return buf; - } - - /** - * Allocates an uninitialized byte array based on the length and coder information - * in indexCoder - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArray(long indexCoder) { - byte coder = (byte)(indexCoder >> 32); - int index = ((int)indexCoder) << coder; - return newArray(index); - } - /** * Allocates an uninitialized byte array based on the length * @param length @@ -578,14 +283,6 @@ final class StringConcatHelper { return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length); } - /** - * Provides the initial coder for the String. - * @return initial coder, adjusted into the upper half - */ - static long initialCoder() { - return String.COMPACT_STRINGS ? LATIN1 : UTF16; - } - static MethodHandle lookupStatic(String name, MethodType methodType) { try { return MethodHandles.lookup() @@ -603,7 +300,8 @@ final class StringConcatHelper { * subtracted the length of the suffix. * * @param suffix - * @param indexCoder + * @param index final char index in the buffer + * @param coder coder of the buffer * @return the newly allocated byte array */ @ForceInline diff --git a/src/java.base/share/classes/java/lang/StringLatin1.java b/src/java.base/share/classes/java/lang/StringLatin1.java index da3acbe5f0a..61c62d049bc 100644 --- a/src/java.base/share/classes/java/lang/StringLatin1.java +++ b/src/java.base/share/classes/java/lang/StringLatin1.java @@ -41,61 +41,49 @@ import static java.lang.String.checkIndex; import static java.lang.String.checkOffset; final class StringLatin1 { - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value.length); return (char)(value[index] & 0xff); } - public static boolean canEncode(char cp) { + static boolean canEncode(char cp) { return cp <= 0xff; } - public static boolean canEncode(int cp) { + static boolean canEncode(int cp) { return cp >=0 && cp <= 0xff; } - public static byte coderFromChar(char cp) { + static byte coderFromChar(char cp) { return (byte)((0xff - cp) >>> (Integer.SIZE - 1)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length; } - public static int codePointAt(byte[] value, int index, int end) { - return value[index] & 0xff; - } - - public static int codePointBefore(byte[] value, int index) { - return value[index - 1] & 0xff; - } - - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { - return endIndex - beginIndex; - } - - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length]; inflate(value, 0, dst, 0, value.length); return dst; } - public static byte[] inflate(byte[] value, int off, int len) { + static byte[] inflate(byte[] value, int off, int len) { byte[] ret = StringUTF16.newBytesFor(len); inflate(value, off, ret, 0, len); return ret; } - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { inflate(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } @IntrinsicCandidate - public static boolean equals(byte[] value, byte[] other) { + static boolean equals(byte[] value, byte[] other) { if (value.length == other.length) { for (int i = 0; i < value.length; i++) { if (value[i] != other[i]) { @@ -108,20 +96,20 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; return compareTo(value, other, len1, len2); } - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { int lim = Math.min(len1, len2); int k = ArraysSupport.mismatch(value, other, lim); return (k < 0) ? len1 - len2 : getChar(value, k) - getChar(other, k); } @IntrinsicCandidate - public static int compareToUTF16(byte[] value, byte[] other) { + static int compareToUTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); return compareToUTF16Values(value, other, len1, len2); @@ -130,7 +118,7 @@ final class StringLatin1 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { + static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, length(value)); checkOffset(len2, StringUTF16.length(other)); @@ -149,7 +137,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; int lim = Math.min(len1, len2); @@ -169,7 +157,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI_UTF16(byte[] value, byte[] other) { + static int compareToCI_UTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); int lim = Math.min(len1, len2); @@ -191,12 +179,12 @@ final class StringLatin1 { return len1 - len2; } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUnsigned(value, 0, value.length, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (!canEncode(ch)) { return -1; } @@ -215,7 +203,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -226,7 +214,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { byte first = str[0]; int max = (valueCount - strCount); for (int i = fromIndex; i <= max; i++) { @@ -248,8 +236,8 @@ final class StringLatin1 { return -1; } - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { int min = tgtCount - 1; int i = min + fromIndex; int strLastIndex = tgtCount - 1; @@ -276,7 +264,7 @@ final class StringLatin1 { } } - public static int lastIndexOf(final byte[] value, int ch, int fromIndex) { + static int lastIndexOf(final byte[] value, int ch, int fromIndex) { if (!canEncode(ch)) { return -1; } @@ -289,7 +277,7 @@ final class StringLatin1 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { if (canEncode(oldChar)) { int len = value.length; int i = -1; @@ -326,8 +314,8 @@ final class StringLatin1 { return null; // for string to return this; } - public static String replace(byte[] value, int valLen, byte[] targ, - int targLen, byte[] repl, int replLen) + static String replace(byte[] value, int valLen, byte[] targ, + int targLen, byte[] repl, int replLen) { assert targLen > 0; int i, j, p = 0; @@ -377,8 +365,8 @@ final class StringLatin1 { } // case insensitive - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { byte b1 = value[toffset++]; @@ -391,8 +379,8 @@ final class StringLatin1 { return true; } - public static boolean regionMatchesCI_UTF16(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI_UTF16(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { char c1 = (char)(value[toffset++] & 0xff); @@ -413,7 +401,7 @@ final class StringLatin1 { return true; } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -480,7 +468,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -560,7 +548,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int len = value.length; int st = 0; while ((st < len) && ((value[st] & 0xff) <= ' ')) { @@ -573,7 +561,7 @@ final class StringLatin1 { newString(value, st, len - st) : null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length; int left = 0; while (left < length) { @@ -586,9 +574,8 @@ final class StringLatin1 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length; while (0 < right) { char ch = getChar(value, right - 1); if (ch != ' ' && ch != '\t' && !CharacterDataLatin1.instance.isWhitespace(ch)) { @@ -599,7 +586,7 @@ final class StringLatin1 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int left = indexOfNonWhitespace(value); if (left == value.length) { return ""; @@ -609,12 +596,12 @@ final class StringLatin1 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, value.length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int right = lastIndexOfNonWhitespace(value); return (right != value.length) ? newString(value, 0, right) : null; } @@ -713,14 +700,14 @@ final class StringLatin1 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; value[i + 3] = (byte)c4; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; @@ -728,32 +715,15 @@ final class StringLatin1 { value[i + 4] = (byte)c5; } - public static void putChar(byte[] val, int index, int c) { - //assert (canEncode(c)); - val[index] = (byte)(c); - } - - public static char getChar(byte[] val, int index) { + static char getChar(byte[] val, int index) { return (char)(val[index] & 0xff); } - public static byte[] toBytes(int[] val, int off, int len) { - byte[] ret = new byte[len]; - for (int i = 0; i < len; i++) { - int cp = val[off++]; - if (!canEncode(cp)) { - return null; - } - ret[i] = (byte)cp; - } - return ret; - } - - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { return new byte[] { (byte)c }; } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -763,7 +733,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> char[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { dst[dstOff++] = (char)(src[srcOff++] & 0xff); } @@ -771,7 +741,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> byte[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { StringUTF16.inflate(src, srcOff, dst, dstOff, len); } @@ -824,7 +794,7 @@ final class StringLatin1 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { diff --git a/src/java.base/share/classes/java/lang/StringUTF16.java b/src/java.base/share/classes/java/lang/StringUTF16.java index 08b4072fc35..4e31c9728e9 100644 --- a/src/java.base/share/classes/java/lang/StringUTF16.java +++ b/src/java.base/share/classes/java/lang/StringUTF16.java @@ -54,13 +54,13 @@ final class StringUTF16 { // Return a new byte array for a UTF16-coded string for len chars // Throw an exception if out of range - public static byte[] newBytesFor(int len) { + static byte[] newBytesFor(int len) { return new byte[newBytesLength(len)]; } // Check the size of a UTF16-coded string // Throw an exception if out of range - public static int newBytesLength(int len) { + static int newBytesLength(int len) { if (len < 0) { throw new NegativeArraySizeException(); } @@ -89,7 +89,7 @@ final class StringUTF16 { ((val[index] & 0xff) << LO_BYTE_SHIFT)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length >> 1; } @@ -111,7 +111,7 @@ final class StringUTF16 { return c1; } - public static int codePointAt(byte[] value, int index, int end) { + static int codePointAt(byte[] value, int index, int end) { return codePointAt(value, index, end, false /* unchecked */); } @@ -134,7 +134,7 @@ final class StringUTF16 { return c2; } - public static int codePointBefore(byte[] value, int index) { + static int codePointBefore(byte[] value, int index) { return codePointBefore(value, index, false /* unchecked */); } @@ -155,11 +155,11 @@ final class StringUTF16 { return count; } - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { + static int codePointCount(byte[] value, int beginIndex, int endIndex) { return codePointCount(value, beginIndex, endIndex, false /* unchecked */); } - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length >> 1]; getChars(value, 0, dst.length, dst, 0); return dst; @@ -173,7 +173,7 @@ final class StringUTF16 { * @param len a length */ @IntrinsicCandidate - public static byte[] toBytes(char[] value, int off, int len) { + static byte[] toBytes(char[] value, int off, int len) { byte[] val = newBytesFor(len); for (int i = 0; i < len; i++) { putChar(val, i, value[off]); @@ -218,7 +218,7 @@ final class StringUTF16 { * @param count count of chars to be compressed, {@code count} > 0 */ @ForceInline - public static byte[] compress(final char[] val, final int off, final int count) { + static byte[] compress(final char[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) { @@ -245,7 +245,7 @@ final class StringUTF16 { * @param off starting offset * @param count count of chars to be compressed, {@code count} > 0 */ - public static byte[] compress(final byte[] val, final int off, final int count) { + static byte[] compress(final byte[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) {// Switch to UTF16 @@ -279,7 +279,7 @@ final class StringUTF16 { * @param off starting offset * @param count length of code points to be compressed, length > 0 */ - public static byte[] compress(final int[] val, int off, final int count) { + static byte[] compress(final int[] val, int off, final int count) { // Optimistically copy all latin1 code points to the destination byte[] latin1 = new byte[count]; final int end = off + count; @@ -389,7 +389,7 @@ final class StringUTF16 { // compressedCopy char[] -> byte[] @IntrinsicCandidate - public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { char c = src[srcOff]; if (c > 0xff) { @@ -404,7 +404,7 @@ final class StringUTF16 { // compressedCopy byte[] -> byte[] @IntrinsicCandidate - public static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'getChar' has no checks checkBoundsOffCount(srcOff, len, src); for (int i = 0; i < len; i++) { @@ -420,7 +420,7 @@ final class StringUTF16 { } // Create the UTF16 buffer for !COMPACT_STRINGS - public static byte[] toBytes(int[] val, int index, int len) { + static byte[] toBytes(int[] val, int index, int len) { final int end = index + len; int n = computeCodePointSize(val, index, end); @@ -428,7 +428,7 @@ final class StringUTF16 { return extractCodepoints(val, index, end, buf, 0); } - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { byte[] result = new byte[2]; putChar(result, 0, c); return result; @@ -442,7 +442,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { // We need a range check here because 'getChar' has no checks if (srcBegin < srcEnd) { checkBoundsOffCount(srcBegin, srcEnd - srcBegin, value); @@ -453,7 +453,7 @@ final class StringUTF16 { } /* @see java.lang.String.getBytes(int, int, byte[], int) */ - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { srcBegin <<= 1; srcEnd <<= 1; for (int i = srcBegin + (1 >> LO_BYTE_SHIFT); i < srcEnd; i += 2) { @@ -462,7 +462,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = length(value); int len2 = length(other); return compareValues(value, other, len1, len2); @@ -471,7 +471,7 @@ final class StringUTF16 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, value); checkOffset(len2, other); @@ -491,15 +491,15 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareToLatin1(byte[] value, byte[] other) { + static int compareToLatin1(byte[] value, byte[] other) { return -StringLatin1.compareToUTF16(other, value); } - public static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { + static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { return -StringLatin1.compareToUTF16(other, value, len2, len1); } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { return compareToCIImpl(value, 0, length(value), other, 0, length(other)); } @@ -512,8 +512,8 @@ final class StringUTF16 { assert olast <= length(other); for (int k1 = toffset, k2 = ooffset; k1 < tlast && k2 < olast; k1++, k2++) { - int cp1 = (int)getChar(value, k1); - int cp2 = (int)getChar(other, k2); + int cp1 = getChar(value, k1); + int cp2 = getChar(other, k2); if (cp1 == cp2 || compareCodePointCI(cp1, cp2) == 0) { continue; @@ -588,16 +588,16 @@ final class StringUTF16 { return cp; } - public static int compareToCI_Latin1(byte[] value, byte[] other) { + static int compareToCI_Latin1(byte[] value, byte[] other) { return -StringLatin1.compareToCI_UTF16(other, value); } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUTF16(value, 0, value.length >> 1, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -608,7 +608,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -619,7 +619,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, valueCount, value); checkBoundsBeginEnd(0, strCount, str); return indexOfUnsafe(value, valueCount, str, strCount, fromIndex); @@ -657,7 +657,7 @@ final class StringUTF16 { * Handles indexOf Latin1 substring in UTF16 string. */ @IntrinsicCandidate - public static int indexOfLatin1(byte[] value, byte[] str) { + static int indexOfLatin1(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -668,13 +668,13 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, srcCount, src); String.checkBoundsBeginEnd(0, tgtCount, tgt.length); return indexOfLatin1Unsafe(src, srcCount, tgt, tgtCount, fromIndex); } - public static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= tgt.length; @@ -730,8 +730,8 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == UTF16 - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= length(tgt); @@ -765,7 +765,7 @@ final class StringUTF16 { } } - public static int lastIndexOf(byte[] value, int ch, int fromIndex) { + static int lastIndexOf(byte[] value, int ch, int fromIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -798,7 +798,7 @@ final class StringUTF16 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { int len = value.length >> 1; int i = -1; while (++i < len) { @@ -829,9 +829,9 @@ final class StringUTF16 { return null; } - public static String replace(byte[] value, int valLen, boolean valLat1, - byte[] targ, int targLen, boolean targLat1, - byte[] repl, int replLen, boolean replLat1) + static String replace(byte[] value, int valLen, boolean valLat1, + byte[] targ, int targLen, boolean targLat1, + byte[] repl, int replLen, boolean replLat1) { assert targLen > 0; assert !valLat1 || !targLat1 || !replLat1; @@ -944,18 +944,18 @@ final class StringUTF16 { return new String(result, UTF16); } - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { return compareToCIImpl(value, toffset, len, other, ooffset, len) == 0; } - public static boolean regionMatchesCI_Latin1(byte[] value, int toffset, - byte[] other, int ooffset, - int len) { + static boolean regionMatchesCI_Latin1(byte[] value, int toffset, + byte[] other, int ooffset, + int len) { return StringLatin1.regionMatchesCI_UTF16(other, ooffset, value, toffset, len); } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -965,7 +965,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -988,7 +988,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (cp == '\u03A3' || // GREEK CAPITAL LETTER SIGMA Character.isSurrogate((char)cp)) { return toLowerCaseEx(str, value, result, i, locale, false); @@ -1003,7 +1003,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1059,7 +1059,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -1069,7 +1069,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -1093,7 +1093,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (Character.isSurrogate((char)cp)) { return toUpperCaseEx(str, value, result, i, locale, false); } @@ -1104,7 +1104,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1164,7 +1164,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int length = value.length >> 1; int len = length; int st = 0; @@ -1179,7 +1179,7 @@ final class StringUTF16 { null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length >> 1; int left = 0; while (left < length) { @@ -1192,9 +1192,8 @@ final class StringUTF16 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length >>> 1; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length >>> 1; while (0 < right) { int codepoint = codePointBefore(value, right); if (codepoint != ' ' && codepoint != '\t' && !Character.isWhitespace(codepoint)) { @@ -1205,7 +1204,7 @@ final class StringUTF16 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); if (left == length) { @@ -1216,13 +1215,13 @@ final class StringUTF16 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int length = value.length >>> 1; int right = lastIndexOfNonWhitespace(value); return (right != length) ? newString(value, 0, right) : null; @@ -1322,7 +1321,7 @@ final class StringUTF16 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -1388,7 +1387,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1473,7 +1472,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1483,12 +1482,12 @@ final class StringUTF16 { //////////////////////////////////////////////////////////////// - public static void putCharSB(byte[] val, int index, int c) { + static void putCharSB(byte[] val, int index, int c) { checkIndex(index, val); putChar(val, index, c); } - public static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { + static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); String.checkBoundsBeginEnd(off, end, ca.length); Unsafe.getUnsafe().copyMemory( @@ -1499,26 +1498,26 @@ final class StringUTF16 { (long) (end - off) << 1); } - public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { + static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); for (int i = off; i < end; i++) { putChar(val, index++, s.charAt(i)); } } - public static int codePointAtSB(byte[] val, int index, int end) { + static int codePointAtSB(byte[] val, int index, int end) { return codePointAt(val, index, end, true /* checked */); } - public static int codePointBeforeSB(byte[] val, int index) { + static int codePointBeforeSB(byte[] val, int index) { return codePointBefore(val, index, true /* checked */); } - public static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { + static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { return codePointCount(val, beginIndex, endIndex, true /* checked */); } - public static boolean contentEquals(byte[] v1, byte[] v2, int len) { + static boolean contentEquals(byte[] v1, byte[] v2, int len) { checkBoundsOffCount(0, len, v2); for (int i = 0; i < len; i++) { if ((char)(v1[i] & 0xff) != getChar(v2, i)) { @@ -1528,7 +1527,7 @@ final class StringUTF16 { return true; } - public static boolean contentEquals(byte[] value, CharSequence cs, int len) { + static boolean contentEquals(byte[] value, CharSequence cs, int len) { checkOffset(len, value); for (int i = 0; i < len; i++) { if (getChar(value, i) != cs.charAt(i)) { @@ -1538,7 +1537,7 @@ final class StringUTF16 { return true; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { int end = i + 4; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1547,7 +1546,7 @@ final class StringUTF16 { putChar(value, i + 3, c4); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { int end = i + 5; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1557,12 +1556,12 @@ final class StringUTF16 { putChar(value, i + 4, c5); } - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value); return getChar(value, index); } - public static void reverse(byte[] val, int count) { + static void reverse(byte[] val, int count) { checkOffset(count, val); int n = count - 1; boolean hasSurrogates = false; @@ -1597,7 +1596,7 @@ final class StringUTF16 { } // inflatedCopy byte[] -> byte[] - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'putChar' has no checks checkBoundsOffCount(dstOff, len, dst); for (int i = 0; i < len; i++) { @@ -1606,7 +1605,7 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == LATIN1 - public static int lastIndexOfLatin1(byte[] src, int srcCount, + static int lastIndexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; @@ -1659,19 +1658,19 @@ final class StringUTF16 { static final int MAX_LENGTH = Integer.MAX_VALUE >> 1; - public static void checkIndex(int off, byte[] val) { + static void checkIndex(int off, byte[] val) { String.checkIndex(off, length(val)); } - public static void checkOffset(int off, byte[] val) { + static void checkOffset(int off, byte[] val) { String.checkOffset(off, length(val)); } - public static void checkBoundsBeginEnd(int begin, int end, byte[] val) { + static void checkBoundsBeginEnd(int begin, int end, byte[] val) { String.checkBoundsBeginEnd(begin, end, length(val)); } - public static void checkBoundsOffCount(int offset, int count, byte[] val) { + static void checkBoundsOffCount(int offset, int count, byte[] val) { String.checkBoundsOffCount(offset, count, length(val)); } diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index bb1775fbc6b..c88cf4ac797 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2185,18 +2185,6 @@ public final class System { return StringConcatHelper.lookupStatic(name, methodType); } - public long stringConcatInitialCoder() { - return StringConcatHelper.initialCoder(); - } - - public long stringConcatMix(long lengthCoder, String constant) { - return StringConcatHelper.mix(lengthCoder, constant); - } - - public long stringConcatMix(long lengthCoder, char value) { - return StringConcatHelper.mix(lengthCoder, value); - } - public Object uncheckedStringConcat1(String[] constants) { return new StringConcatHelper.Concat1(constants); } diff --git a/src/java.base/share/classes/java/lang/Thread.java b/src/java.base/share/classes/java/lang/Thread.java index 2fc20a94f1d..ace29f30a56 100644 --- a/src/java.base/share/classes/java/lang/Thread.java +++ b/src/java.base/share/classes/java/lang/Thread.java @@ -228,7 +228,7 @@ public class Thread implements Runnable { // thread name private volatile String name; - // interrupt status (read/written by VM) + // interrupted status (read/written by VM) volatile boolean interrupted; // context ClassLoader @@ -355,7 +355,7 @@ public class Thread implements Runnable { /* The object in which this thread is blocked in an interruptible I/O * operation, if any. The blocker's interrupt method should be invoked - * after setting this thread's interrupt status. + * after setting this thread's interrupted status. */ private Interruptible nioBlocker; @@ -1527,36 +1527,6 @@ public class Thread implements Runnable { } } - /** - * Throws {@code UnsupportedOperationException}. - * - * @throws UnsupportedOperationException always - * - * @deprecated This method was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@link ThreadDeath}. - * It was inherently unsafe. Stopping a thread caused it to unlock - * all of the monitors that it had locked (as a natural consequence - * of the {@code ThreadDeath} exception propagating up the stack). If - * any of the objects previously protected by these monitors were in - * an inconsistent state, the damaged objects became visible to - * other threads, potentially resulting in arbitrary behavior. - * Usages of {@code stop} should be replaced by code that simply - * modifies some variable to indicate that the target thread should - * stop running. The target thread should check this variable - * regularly, and return from its run method in an orderly fashion - * if the variable indicates that it is to stop running. If the - * target thread waits for long periods (on a condition variable, - * for example), the {@code interrupt} method should be used to - * interrupt the wait. - * For more information, see - * Why - * is Thread.stop deprecated and the ability to stop a thread removed?. - */ - @Deprecated(since="1.2", forRemoval=true) - public final void stop() { - throw new UnsupportedOperationException(); - } - /** * Interrupts this thread. * @@ -1565,22 +1535,22 @@ public class Thread implements Runnable { * Object#wait(long, int) wait(long, int)} methods of the {@link Object} * class, or of the {@link #join()}, {@link #join(long)}, {@link * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)} - * methods of this class, then its interrupt status will be cleared and it + * methods of this class, then its interrupted status will be cleared and it * will receive an {@link InterruptedException}. * *

If this thread is blocked in an I/O operation upon an {@link * java.nio.channels.InterruptibleChannel InterruptibleChannel} - * then the channel will be closed, the thread's interrupt + * then the channel will be closed, the thread's interrupted * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. * *

If this thread is blocked in a {@link java.nio.channels.Selector} - * then the thread's interrupt status will be set and it will return + * then the thread's interrupted status will be set and it will return * immediately from the selection operation, possibly with a non-zero * value, just as if the selector's {@link * java.nio.channels.Selector#wakeup wakeup} method were invoked. * - *

If none of the previous conditions hold then this thread's interrupt + *

If none of the previous conditions hold then this thread's interrupted * status will be set.

* *

Interrupting a thread that is not alive need not have any effect. @@ -1590,7 +1560,7 @@ public class Thread implements Runnable { * will report it via {@link #interrupted()} and {@link #isInterrupted()}. */ public void interrupt() { - // Setting the interrupt status must be done before reading nioBlocker. + // Setting the interrupted status must be done before reading nioBlocker. interrupted = true; interrupt0(); // inform VM of interrupt diff --git a/src/java.base/share/classes/java/lang/ThreadDeath.java b/src/java.base/share/classes/java/lang/ThreadDeath.java index 743f1178abe..e77029a28fc 100644 --- a/src/java.base/share/classes/java/lang/ThreadDeath.java +++ b/src/java.base/share/classes/java/lang/ThreadDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, 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 @@ -27,14 +27,12 @@ package java.lang; /** * An instance of {@code ThreadDeath} was originally specified to be thrown - * by a victim thread when "stopped" with {@link Thread#stop()}. + * by a victim thread when "stopped" with the {@link Thread} API. * - * @deprecated {@link Thread#stop()} was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@code ThreadDeath}. It - * was inherently unsafe and deprecated in an early JDK release. The ability - * to "stop" a thread with {@code Thread.stop} has been removed and the - * {@code Thread.stop} method changed to throw an exception. Consequently, - * {@code ThreadDeath} is also deprecated, for removal. + * @deprecated {@code Thread} originally specified a "{@code stop}" method to stop a + * victim thread by causing the victim thread to throw a {@code ThreadDeath}. It + * was inherently unsafe and deprecated in an early JDK release. The {@code stop} + * method has since been removed and {@code ThreadDeath} is deprecated, for removal. * * @since 1.0 */ diff --git a/src/java.base/share/classes/java/lang/VirtualThread.java b/src/java.base/share/classes/java/lang/VirtualThread.java index 19465eb32db..a23cbb72a6c 100644 --- a/src/java.base/share/classes/java/lang/VirtualThread.java +++ b/src/java.base/share/classes/java/lang/VirtualThread.java @@ -483,12 +483,12 @@ final class VirtualThread extends BaseVirtualThread { Thread carrier = Thread.currentCarrierThread(); setCarrierThread(carrier); - // sync up carrier thread interrupt status if needed + // sync up carrier thread interrupted status if needed if (interrupted) { carrier.setInterrupt(); } else if (carrier.isInterrupted()) { synchronized (interruptLock) { - // need to recheck interrupt status + // need to recheck interrupted status if (!interrupted) { carrier.clearInterrupt(); } @@ -721,7 +721,7 @@ final class VirtualThread extends BaseVirtualThread { /** * Parks until unparked or interrupted. If already unparked then the parking * permit is consumed and this method completes immediately (meaning it doesn't - * yield). It also completes immediately if the interrupt status is set. + * yield). It also completes immediately if the interrupted status is set. */ @Override void park() { @@ -756,7 +756,7 @@ final class VirtualThread extends BaseVirtualThread { * Parks up to the given waiting time or until unparked or interrupted. * If already unparked then the parking permit is consumed and this method * completes immediately (meaning it doesn't yield). It also completes immediately - * if the interrupt status is set or the waiting time is {@code <= 0}. + * if the interrupted status is set or the waiting time is {@code <= 0}. * * @param nanos the maximum number of nanoseconds to wait. */ @@ -799,7 +799,7 @@ final class VirtualThread extends BaseVirtualThread { /** * Parks the current carrier thread up to the given waiting time or until * unparked or interrupted. If the virtual thread is interrupted then the - * interrupt status will be propagated to the carrier thread. + * interrupted status will be propagated to the carrier thread. * @param timed true for a timed park, false for untimed * @param nanos the waiting time in nanoseconds */ diff --git a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java index f81820a7f77..76251cd4d8e 100644 --- a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java +++ b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java @@ -113,13 +113,14 @@ public sealed interface MethodSignature * * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(Signature result, Signature... arguments) { return new SignaturesImpl.MethodSignatureImpl(List.of(), List.of(), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** @@ -131,6 +132,7 @@ public sealed interface MethodSignature * @param exceptions signatures for the exceptions * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(List typeParameters, List exceptions, @@ -140,7 +142,7 @@ public sealed interface MethodSignature List.copyOf(requireNonNull(typeParameters)), List.copyOf(requireNonNull(exceptions)), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** diff --git a/src/java.base/share/classes/java/lang/classfile/Signature.java b/src/java.base/share/classes/java/lang/classfile/Signature.java index f9af71083f9..594c7822e98 100644 --- a/src/java.base/share/classes/java/lang/classfile/Signature.java +++ b/src/java.base/share/classes/java/lang/classfile/Signature.java @@ -43,6 +43,11 @@ import static java.util.Objects.requireNonNull; /** * Models generic Java type signatures, as defined in JVMS {@jvms 4.7.9.1}. + *

+ * Names in signatures are identifiers, which must + * not be empty and must not contain any of the ASCII characters {@code + * . ; [ / < > :}. Top-level class and interface names are denoted by + * slash-separated identifiers. * * @see Type * @see SignatureAttribute @@ -73,6 +78,8 @@ public sealed interface Signature { * signature represents a reifiable type (JLS {@jls 4.7}). * * @param classDesc the symbolic description of the Java type + * @throws IllegalArgumentException if the field descriptor cannot be + * {@linkplain ##identifier denoted} */ public static Signature of(ClassDesc classDesc) { requireNonNull(classDesc); @@ -139,6 +146,31 @@ public sealed interface Signature { /** * Models the signature of a possibly-parameterized class or interface type. + *

+ * These are examples of class type signatures: + *

+ *

+ * If the {@linkplain #outerType() outer type} exists, the {@linkplain + * #className() class name} is the simple name of the nested type. + * Otherwise, it is a {@linkplain ClassEntry##internalname binary name in + * internal form} (separated by {@code /}). + *

+ * If a nested type does not have any enclosing parameterization, it may + * be represented without an outer type and as an internal binary name, + * in which nesting is represented by {@code $} instead of {@code .}. * * @see Type * @see ParameterizedType @@ -152,7 +184,8 @@ public sealed interface Signature { /** * {@return the signature of the class that this class is a member of, * only if this is a member class} Note that the outer class may be - * absent if it is not a parameterized type. + * absent if this is a member class without any parameterized enclosing + * type. * * @jls 4.5 Parameterized Types */ @@ -161,7 +194,8 @@ public sealed interface Signature { /** * {@return the class or interface name; includes the {@linkplain * ClassEntry##internalname slash-separated} package name if there is no - * outer type} + * outer type} Note this may indicate a nested class name with {@code $} + * separators if there is no parameterized enclosing type. */ String className(); @@ -188,10 +222,11 @@ public sealed interface Signature { * @param className the name of the class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassDesc className, TypeArg... typeArgs) { - return of(null, className, typeArgs); + return of(null, Util.toInternalName(className), typeArgs); } /** @@ -201,8 +236,15 @@ public sealed interface Signature { * @param className the name of this class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} + * @deprecated + * The resulting signature does not denote the class represented by + * {@code className} when {@code outerType} is not null. Use {@link + * #of(ClassTypeSig, String, TypeArg...) of(ClassTypeSig, String, TypeArg...)} + * instead. */ + @Deprecated(since = "26", forRemoval = true) public static ClassTypeSig of(ClassTypeSig outerType, ClassDesc className, TypeArg... typeArgs) { requireNonNull(className); return of(outerType, Util.toInternalName(className), typeArgs); @@ -211,8 +253,11 @@ public sealed interface Signature { /** * {@return a class or interface signature without an outer type} * - * @param className the name of the class or interface + * @param className the name of the class or interface, may use + * {@code /} to separate * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(String className, TypeArg... typeArgs) { return of(null, className, typeArgs); @@ -222,12 +267,19 @@ public sealed interface Signature { * {@return a class type signature} * * @param outerType signature of the outer type, may be {@code null} - * @param className the name of this class or interface + * @param className the name of this class or interface, may use + * {@code /} to separate if outer type is absent * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassTypeSig outerType, String className, TypeArg... typeArgs) { - requireNonNull(className); - return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className.replace(".", "/"), List.of(typeArgs)); + if (outerType != null) { + SignaturesImpl.validateIdentifier(className); + } else { + SignaturesImpl.validatePackageSpecifierPlusIdentifier(className); + } + return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className, List.of(typeArgs)); } } @@ -383,9 +435,11 @@ public sealed interface Signature { * {@return a signature for a type variable} * * @param identifier the name of the type variable + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeVarSig of(String identifier) { - return new SignaturesImpl.TypeVarSigImpl(requireNonNull(identifier)); + return new SignaturesImpl.TypeVarSigImpl(SignaturesImpl.validateIdentifier(identifier)); } } @@ -408,9 +462,10 @@ public sealed interface Signature { /** * {@return an array type with the given component type} * @param componentSignature the component type + * @throws IllegalArgumentException if the component type is void */ public static ArrayTypeSig of(Signature componentSignature) { - return of(1, requireNonNull(componentSignature)); + return of(1, SignaturesImpl.validateNonVoid(componentSignature)); } /** @@ -418,10 +473,11 @@ public sealed interface Signature { * @param dims the dimension of the array * @param componentSignature the component type * @throws IllegalArgumentException if {@code dims < 1} or the - * resulting array type exceeds 255 dimensions + * resulting array type exceeds 255 dimensions or the component + * type is void */ public static ArrayTypeSig of(int dims, Signature componentSignature) { - requireNonNull(componentSignature); + SignaturesImpl.validateNonVoid(componentSignature); if (componentSignature instanceof SignaturesImpl.ArrayTypeSigImpl arr) { if (dims < 1 || dims > 255 - arr.arrayDepth()) throw new IllegalArgumentException("illegal array depth value"); @@ -469,10 +525,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the class bound of the type parameter, may be {@code null} * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, RefTypeSig classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), Optional.ofNullable(classBound), List.of(interfaceBounds)); } @@ -483,10 +541,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the optional class bound of the type parameter * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, Optional classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), requireNonNull(classBound), List.of(interfaceBounds)); } diff --git a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html b/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html deleted file mode 100644 index 9f3ad156727..00000000000 --- a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - Java Thread Primitive Deprecation - - -

Java Thread Primitive Deprecation

-
-

Why is Thread.stop deprecated and the ability to -stop a thread removed?

-

Because it was inherently unsafe. Stopping a thread caused it to -unlock all the monitors that it had locked. (The monitors were -unlocked as the ThreadDeath exception propagated up -the stack.) If any of the objects previously protected by these -monitors were in an inconsistent state, other threads may have viewed -these objects in an inconsistent state. Such objects are said to be -damaged. When threads operate on damaged objects, arbitrary -behavior can result. This behavior may be subtle and difficult to -detect, or it may be pronounced. Unlike other unchecked exceptions, -ThreadDeath killed threads silently; thus, the user had -no warning that their program may be corrupted. The corruption could -manifest itself at any time after the actual damage occurs, even -hours or days in the future.

-
-

Couldn't I have just caught ThreadDeath and fixed -the damaged object?

-

In theory, perhaps, but it would vastly complicate the -task of writing correct multithreaded code. The task would be -nearly insurmountable for two reasons:

-
    -
  1. A thread could throw a ThreadDeath exception -almost anywhere. All synchronized methods and blocks would -have to be studied in great detail, with this in mind.
  2. -
  3. A thread could throw a second ThreadDeath exception -while cleaning up from the first (in the catch or -finally clause). Cleanup would have to be repeated till -it succeeded. The code to ensure this would be quite complex.
  4. -
-In sum, it just isn't practical. -
-

What should I use instead of Thread.stop?

-

Most uses of stop should be replaced by code that -simply modifies some variable to indicate that the target thread -should stop running. The target thread should check this variable -regularly, and return from its run method in an orderly fashion if -the variable indicates that it is to stop running. To ensure prompt -communication of the stop-request, the variable must be -volatile (or access to the variable must be -synchronized).

-

For example, suppose your application contains the following -start, stop and run -methods:

-
-    private Thread blinker;
-
-    public void start() {
-        blinker = new Thread(this);
-        blinker.start();
-    }
-
-    public void stop() {
-        blinker.stop();  // UNSAFE!
-    }
-
-    public void run() {
-        while (true) {
-            try {
-                Thread.sleep(interval);
-            } catch (InterruptedException e){
-            }
-            blink();
-        }
-    }
-
-You can avoid the use of Thread.stop by replacing the -application's stop and run methods with: -
-    private volatile Thread blinker;
-
-    public void stop() {
-        blinker = null;
-    }
-
-    public void run() {
-        Thread thisThread = Thread.currentThread();
-        while (blinker == thisThread) {
-            try {
-                Thread.sleep(interval);
-            } catch (InterruptedException e){
-            }
-            blink();
-        }
-    }
-
-
-

How do I stop a thread that waits for long periods (e.g., for -input)?

-

That's what the Thread.interrupt method is for. The -same "state based" signaling mechanism shown above can be used, but -the state change (blinker = null, in the previous -example) can be followed by a call to -Thread.interrupt, to interrupt the wait:

-
-    public void stop() {
-        Thread moribund = waiter;
-        waiter = null;
-        moribund.interrupt();
-    }
-
-For this technique to work, it's critical that any method that -catches an interrupt exception and is not prepared to deal with it -immediately reasserts the exception. We say reasserts -rather than rethrows, because it is not always possible to -rethrow the exception. If the method that catches the -InterruptedException is not declared to throw this -(checked) exception, then it should "reinterrupt itself" with the -following incantation: -
-    Thread.currentThread().interrupt();
-
-This ensures that the Thread will reraise the -InterruptedException as soon as it is able. -
-

What if a thread doesn't respond to -Thread.interrupt?

-

In some cases, you can use application specific tricks. For -example, if a thread is waiting on a known socket, you can close -the socket to cause the thread to return immediately. -Unfortunately, there really isn't any technique that works in -general. It should be noted that in all situations where a -waiting thread doesn't respond to Thread.interrupt, it -wouldn't respond to Thread.stop either. Such -cases include deliberate denial-of-service attacks, and I/O -operations for which thread.stop and thread.interrupt do not work -properly.

- - diff --git a/src/java.base/share/classes/java/lang/invoke/MethodHandleImpl.java b/src/java.base/share/classes/java/lang/invoke/MethodHandleImpl.java index cb1bf8294d2..5b8a4478be5 100644 --- a/src/java.base/share/classes/java/lang/invoke/MethodHandleImpl.java +++ b/src/java.base/share/classes/java/lang/invoke/MethodHandleImpl.java @@ -33,7 +33,6 @@ import jdk.internal.constant.MethodTypeDescImpl; import jdk.internal.foreign.abi.NativeEntryPoint; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.Reflection; -import jdk.internal.vm.annotation.AOTRuntimeSetup; import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.Hidden; @@ -1536,11 +1535,6 @@ abstract class MethodHandleImpl { } static { - runtimeSetup(); - } - - @AOTRuntimeSetup - private static void runtimeSetup() { SharedSecrets.setJavaLangInvokeAccess(new JavaLangInvokeAccess() { @Override public Class getDeclaringClass(Object rmname) { diff --git a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java index 1c7995c4ec7..733714b5786 100644 --- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java +++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java @@ -37,7 +37,6 @@ import jdk.internal.util.ReferenceKey; import jdk.internal.util.ReferencedKeyMap; import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.Stable; -import sun.invoke.util.Wrapper; import java.lang.classfile.Annotation; import java.lang.classfile.ClassBuilder; @@ -119,14 +118,10 @@ import static java.lang.invoke.MethodType.methodType; */ @AOTSafeClassInitializer public final class StringConcatFactory { - private static final int HIGH_ARITY_THRESHOLD; private static final int CACHE_THRESHOLD; private static final int FORCE_INLINE_THRESHOLD; static { - String highArity = VM.getSavedProperty("java.lang.invoke.StringConcat.highArityThreshold"); - HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 0; - String cacheThreshold = VM.getSavedProperty("java.lang.invoke.StringConcat.cacheThreshold"); CACHE_THRESHOLD = cacheThreshold != null ? Integer.parseInt(cacheThreshold) : 256; @@ -391,9 +386,6 @@ public final class StringConcatFactory { try { MethodHandle mh = makeSimpleConcat(concatType, constantStrings); - if (mh == null && concatType.parameterCount() <= HIGH_ARITY_THRESHOLD) { - mh = generateMHInlineCopy(concatType, constantStrings); - } if (mh == null) { mh = InlineHiddenClassStrategy.generate(lookup, concatType, constantStrings); @@ -518,385 +510,6 @@ public final class StringConcatFactory { return null; } - /** - *

This strategy replicates what StringBuilders are doing: it builds the - * byte[] array on its own and passes that byte[] array to String - * constructor. This strategy requires access to some private APIs in JDK, - * most notably, the private String constructor that accepts byte[] arrays - * without copying. - */ - private static MethodHandle generateMHInlineCopy(MethodType mt, String[] constants) { - int paramCount = mt.parameterCount(); - String suffix = constants[paramCount]; - - - // else... fall-through to slow-path - - // Create filters and obtain filtered parameter types. Filters would be used in the beginning - // to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings). - // The filtered argument type list is used all over in the combinators below. - - Class[] ptypes = mt.erase().parameterArray(); - MethodHandle[] objFilters = null; - MethodHandle[] floatFilters = null; - MethodHandle[] doubleFilters = null; - for (int i = 0; i < ptypes.length; i++) { - Class cl = ptypes[i]; - // Use int as the logical type for subword integral types - // (byte and short). char and boolean require special - // handling so don't change the logical type of those - ptypes[i] = promoteToIntType(ptypes[i]); - // Object, float and double will be eagerly transformed - // into a (non-null) String as a first step after invocation. - // Set up to use String as the logical type for such arguments - // internally. - if (cl == Object.class) { - if (objFilters == null) { - objFilters = new MethodHandle[ptypes.length]; - } - objFilters[i] = objectStringifier(); - ptypes[i] = String.class; - } else if (cl == float.class) { - if (floatFilters == null) { - floatFilters = new MethodHandle[ptypes.length]; - } - floatFilters[i] = floatStringifier(); - ptypes[i] = String.class; - } else if (cl == double.class) { - if (doubleFilters == null) { - doubleFilters = new MethodHandle[ptypes.length]; - } - doubleFilters[i] = doubleStringifier(); - ptypes[i] = String.class; - } - } - - // Start building the combinator tree. The tree "starts" with ()String, and "finishes" - // with the (byte[], long)String shape to invoke newString in StringConcatHelper. The combinators are - // assembled bottom-up, which makes the code arguably hard to read. - - // Drop all remaining parameter types, leave only helper arguments: - MethodHandle mh = MethodHandles.dropArgumentsTrusted(newString(), 2, ptypes); - - // Calculate the initialLengthCoder value by looking at all constant values and summing up - // their lengths and adjusting the encoded coder bit if needed - long initialLengthCoder = INITIAL_CODER; - - for (String constant : constants) { - if (constant != null) { - initialLengthCoder = JLA.stringConcatMix(initialLengthCoder, constant); - } - } - - // Mix in prependers. This happens when (byte[], long) = (storage, indexCoder) is already - // known from the combinators below. We are assembling the string backwards, so the index coded - // into indexCoder is the *ending* index. - mh = filterInPrependers(mh, constants, ptypes); - - // Fold in byte[] instantiation at argument 0 - MethodHandle newArrayCombinator; - if (suffix == null || suffix.isEmpty()) { - suffix = ""; - } - // newArray variant that deals with prepending any trailing constant - // - // initialLengthCoder is adjusted to have the correct coder - // and length: The newArrayWithSuffix method expects only the coder of the - // suffix to be encoded into indexCoder - initialLengthCoder -= suffix.length(); - newArrayCombinator = newArrayWithSuffix(suffix); - - mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator, - 1 // index - ); - - // Start combining length and coder mixers. - // - // Length is easy: constant lengths can be computed on the spot, and all non-constant - // shapes have been either converted to Strings, or explicit methods for getting the - // string length out of primitives are provided. - // - // Coders are more interesting. Only Object, String and char arguments (and constants) - // can have non-Latin1 encoding. It is easier to blindly convert constants to String, - // and deduce the coder from there. Arguments would be either converted to Strings - // during the initial filtering, or handled by specializations in MIXERS. - // - // The method handle shape before all mixers are combined in is: - // (long, )String = ("indexCoder", ) - // - // We will bind the initialLengthCoder value to the last mixer (the one that will be - // executed first), then fold that in. This leaves the shape after all mixers are - // combined in as: - // ()String = () - - mh = filterAndFoldInMixers(mh, initialLengthCoder, ptypes); - - // The method handle shape here is (). - - // Apply filters, converting the arguments: - if (objFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, objFilters); - } - if (floatFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, floatFilters); - } - if (doubleFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, doubleFilters); - } - - return mh; - } - - // We need one prepender per argument, but also need to fold in constants. We do so by greedily - // creating prependers that fold in surrounding constants into the argument prepender. This reduces - // the number of unique MH combinator tree shapes we'll create in an application. - // Additionally we do this in chunks to reduce the number of combinators bound to the root tree, - // which simplifies the shape and makes construction of similar trees use less unique LF classes - private static MethodHandle filterInPrependers(MethodHandle mh, String[] constants, Class[] ptypes) { - int pos; - int[] argPositions = null; - MethodHandle prepend; - for (pos = 0; pos < ptypes.length - 3; pos += 4) { - prepend = prepender(pos, constants, ptypes, 4); - argPositions = filterPrependArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - if (pos < ptypes.length) { - int count = ptypes.length - pos; - prepend = prepender(pos, constants, ptypes, count); - argPositions = filterPrependArgPositions(argPositions, pos, count); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - return mh; - } - - static int[] filterPrependArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 2]; - argPositions[0] = 1; // indexCoder - argPositions[1] = 0; // storage - } - int limit = count + 2; - for (int i = 2; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - - // We need one mixer per argument. - private static MethodHandle filterAndFoldInMixers(MethodHandle mh, long initialLengthCoder, Class[] ptypes) { - int pos; - int[] argPositions = null; - for (pos = 0; pos < ptypes.length - 4; pos += 4) { - // Compute new "index" in-place pairwise using old value plus the appropriate arguments. - MethodHandle mix = mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - argPositions = filterMixerArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, - mix, argPositions); - } - - if (pos < ptypes.length) { - // Mix in the last 1 to 4 parameters, insert the initialLengthCoder into the final mixer and - // fold the result into the main combinator - mh = foldInLastMixers(mh, initialLengthCoder, pos, ptypes, ptypes.length - pos); - } else if (ptypes.length == 0) { - // No mixer (constants only concat), insert initialLengthCoder directly - mh = MethodHandles.insertArguments(mh, 0, initialLengthCoder); - } - return mh; - } - - static int[] filterMixerArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 1]; - argPositions[0] = 0; // indexCoder - } - int limit = count + 1; - for (int i = 1; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - private static MethodHandle foldInLastMixers(MethodHandle mh, long initialLengthCoder, int pos, Class[] ptypes, int count) { - MethodHandle mix = switch (count) { - case 1 -> mixer(ptypes[pos]); - case 2 -> mixer(ptypes[pos], ptypes[pos + 1]); - case 3 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - case 4 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - default -> throw new IllegalArgumentException("Unexpected count: " + count); - }; - mix = MethodHandles.insertArguments(mix,0, initialLengthCoder); - // apply selected arguments on the 1-4 arg mixer and fold in the result - return switch (count) { - case 1 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos); - case 2 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos); - case 3 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos); - case 4 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos, 4 + pos); - default -> throw new IllegalArgumentException(); - }; - } - - // Simple prependers, single argument. May be used directly or as a - // building block for complex prepender combinators. - private static MethodHandle prepender(String prefix, Class cl) { - if (prefix == null || prefix.isEmpty()) { - return noPrefixPrepender(cl); - } else { - return MethodHandles.insertArguments( - prepender(cl), 3, prefix); - } - } - - private static MethodHandle prepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = PREPENDERS[idx]; - if (prepend == null) { - PREPENDERS[idx] = prepend = JLA.stringConcatHelper("prepend", - methodType(long.class, long.class, byte[].class, - Wrapper.asPrimitiveType(cl), String.class)).rebind(); - } - return prepend; - } - - private static MethodHandle noPrefixPrepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = NO_PREFIX_PREPENDERS[idx]; - if (prepend == null) { - NO_PREFIX_PREPENDERS[idx] = prepend = MethodHandles.insertArguments(prepender(cl), 3, ""); - } - return prepend; - } - - private static final int INT_IDX = 0, - CHAR_IDX = 1, - LONG_IDX = 2, - BOOLEAN_IDX = 3, - STRING_IDX = 4, - TYPE_COUNT = 5; - private static int classIndex(Class cl) { - if (cl == String.class) return STRING_IDX; - if (cl == int.class) return INT_IDX; - if (cl == boolean.class) return BOOLEAN_IDX; - if (cl == char.class) return CHAR_IDX; - if (cl == long.class) return LONG_IDX; - throw new IllegalArgumentException("Unexpected class: " + cl); - } - - // Constant argument lists used by the prepender MH builders - private static final int[] PREPEND_FILTER_FIRST_ARGS = new int[] { 0, 1, 2 }; - private static final int[] PREPEND_FILTER_SECOND_ARGS = new int[] { 0, 1, 3 }; - private static final int[] PREPEND_FILTER_THIRD_ARGS = new int[] { 0, 1, 4 }; - private static final int[] PREPEND_FILTER_FIRST_PAIR_ARGS = new int[] { 0, 1, 2, 3 }; - private static final int[] PREPEND_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 1, 4, 5 }; - - // Base MH for complex prepender combinators. - private static @Stable MethodHandle PREPEND_BASE; - private static MethodHandle prependBase() { - MethodHandle base = PREPEND_BASE; - if (base == null) { - base = PREPEND_BASE = MethodHandles.dropArguments( - MethodHandles.identity(long.class), 1, byte[].class); - } - return base; - } - - private static final @Stable MethodHandle[][] DOUBLE_PREPENDERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - - private static MethodHandle prepender(String prefix, Class cl, String prefix2, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle prepend = DOUBLE_PREPENDERS[idx1][idx2]; - if (prepend == null) { - prepend = DOUBLE_PREPENDERS[idx1][idx2] = - MethodHandles.dropArguments(prependBase(), 2, cl, cl2); - } - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix, cl), - PREPEND_FILTER_FIRST_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix2, cl2), - PREPEND_FILTER_SECOND_ARGS); - } - - private static MethodHandle prepender(int pos, String[] constants, Class[] ptypes, int count) { - // build the simple cases directly - if (count == 1) { - return prepender(constants[pos], ptypes[pos]); - } - if (count == 2) { - return prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]); - } - // build a tree from an unbound prepender, allowing us to bind the constants in a batch as a final step - MethodHandle prepend = prependBase(); - if (count == 3) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2]), - PREPEND_FILTER_THIRD_ARGS); - } else if (count == 4) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2], constants[pos + 3], ptypes[pos + 3]), - PREPEND_FILTER_SECOND_PAIR_ARGS); - } else { - throw new IllegalArgumentException("Unexpected count: " + count); - } - } - - // Constant argument lists used by the mixer MH builders - private static final int[] MIX_FILTER_SECOND_ARGS = new int[] { 0, 2 }; - private static final int[] MIX_FILTER_THIRD_ARGS = new int[] { 0, 3 }; - private static final int[] MIX_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 3, 4 }; - private static MethodHandle mixer(Class cl) { - int index = classIndex(cl); - MethodHandle mix = MIXERS[index]; - if (mix == null) { - MIXERS[index] = mix = JLA.stringConcatHelper("mix", - methodType(long.class, long.class, Wrapper.asPrimitiveType(cl))).rebind(); - } - return mix; - } - - private static final @Stable MethodHandle[][] DOUBLE_MIXERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - private static MethodHandle mixer(Class cl, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle mix = DOUBLE_MIXERS[idx1][idx2]; - if (mix == null) { - mix = mixer(cl); - mix = MethodHandles.dropArguments(mix, 2, cl2); - DOUBLE_MIXERS[idx1][idx2] = mix = MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl2), MIX_FILTER_SECOND_ARGS); - } - return mix; - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3), MIX_FILTER_THIRD_ARGS); - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3, Class cl4) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3, cl4); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3, cl4), MIX_FILTER_SECOND_PAIR_ARGS); - } - private @Stable static MethodHandle SIMPLE_CONCAT; private static MethodHandle simpleConcat() { MethodHandle mh = SIMPLE_CONCAT; @@ -908,42 +521,11 @@ public final class StringConcatFactory { return mh; } - private @Stable static MethodHandle NEW_STRING; - private static MethodHandle newString() { - MethodHandle mh = NEW_STRING; - if (mh == null) { - MethodHandle newString = JLA.stringConcatHelper("newString", - methodType(String.class, byte[].class, long.class)); - NEW_STRING = mh = newString.rebind(); - } - return mh; - } - - private @Stable static MethodHandle NEW_ARRAY_SUFFIX; - private static MethodHandle newArrayWithSuffix(String suffix) { - MethodHandle mh = NEW_ARRAY_SUFFIX; - if (mh == null) { - MethodHandle newArrayWithSuffix = JLA.stringConcatHelper("newArrayWithSuffix", - methodType(byte[].class, String.class, long.class)); - NEW_ARRAY_SUFFIX = mh = newArrayWithSuffix.rebind(); - } - return MethodHandles.insertArguments(mh, 0, suffix); - } - /** * Public gateways to public "stringify" methods. These methods have the * form String apply(T obj), and normally delegate to {@code String.valueOf}, * depending on argument's type. */ - private @Stable static MethodHandle OBJECT_STRINGIFIER; - private static MethodHandle objectStringifier() { - MethodHandle mh = OBJECT_STRINGIFIER; - if (mh == null) { - OBJECT_STRINGIFIER = mh = JLA.stringConcatHelper("stringOf", - methodType(String.class, Object.class)); - } - return mh; - } private @Stable static MethodHandle FLOAT_STRINGIFIER; private static MethodHandle floatStringifier() { MethodHandle mh = FLOAT_STRINGIFIER; @@ -1027,38 +609,6 @@ public final class StringConcatFactory { } } - private static final @Stable MethodHandle[] NO_PREFIX_PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] MIXERS = new MethodHandle[TYPE_COUNT]; - private static final long INITIAL_CODER = JLA.stringConcatInitialCoder(); - - /** - * Promote integral types to int. - */ - private static Class promoteToIntType(Class t) { - // use int for subword integral types; still need special mixers - // and prependers for char, boolean - return t == byte.class || t == short.class ? int.class : t; - } - - /** - * Returns a stringifier for references and floats/doubles only. - * Always returns null for other primitives. - * - * @param t class to stringify - * @return stringifier; null, if not available - */ - private static MethodHandle stringifierFor(Class t) { - if (t == Object.class) { - return objectStringifier(); - } else if (t == float.class) { - return floatStringifier(); - } else if (t == double.class) { - return doubleStringifier(); - } - return null; - } - private static MethodHandle stringValueOf(Class ptype) { try { return MethodHandles.publicLookup() diff --git a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java index c55ccd735df..4f4e35c6727 100644 --- a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java +++ b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java @@ -54,6 +54,7 @@ import static java.util.Objects.*; import jdk.internal.module.Checks; import jdk.internal.module.ModuleInfo; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** @@ -91,6 +92,7 @@ import jdk.internal.module.ModuleInfo; * @since 9 */ +@AOTSafeClassInitializer public final class ModuleDescriptor implements Comparable { diff --git a/src/java.base/share/classes/java/lang/ref/Reference.java b/src/java.base/share/classes/java/lang/ref/Reference.java index 43d9f414b3c..88bdb99dfd6 100644 --- a/src/java.base/share/classes/java/lang/ref/Reference.java +++ b/src/java.base/share/classes/java/lang/ref/Reference.java @@ -25,7 +25,6 @@ package java.lang.ref; -import jdk.internal.vm.annotation.AOTRuntimeSetup; import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.IntrinsicCandidate; @@ -291,11 +290,6 @@ public abstract sealed class Reference<@jdk.internal.RequiresIdentity T> } static { - runtimeSetup(); - } - - @AOTRuntimeSetup - private static void runtimeSetup() { // provide access in SharedSecrets SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override diff --git a/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java b/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java index 24b55600954..18aa6f29f1f 100644 --- a/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java +++ b/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java @@ -25,18 +25,26 @@ package java.lang.runtime; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassHierarchyResolver; +import java.lang.classfile.Opcode; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.invoke.StringConcatFactory; import java.lang.invoke.TypeDescriptor; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Objects; +import static java.lang.classfile.ClassFile.ACC_STATIC; +import static java.lang.constant.ConstantDescs.*; import static java.util.Objects.requireNonNull; /** @@ -58,15 +66,18 @@ public final class ObjectMethods { private static final MethodHandle TRUE = MethodHandles.constant(boolean.class, true); private static final MethodHandle ZERO = MethodHandles.zero(int.class); private static final MethodHandle CLASS_IS_INSTANCE; - private static final MethodHandle OBJECTS_EQUALS; - private static final MethodHandle OBJECTS_HASHCODE; - private static final MethodHandle OBJECTS_TOSTRING; + private static final MethodHandle IS_NULL; + private static final MethodHandle IS_ARG0_NULL; + private static final MethodHandle IS_ARG1_NULL; private static final MethodHandle OBJECT_EQ; private static final MethodHandle HASH_COMBINER; + private static final MethodType MT_OBJECT_BOOLEAN = MethodType.methodType(boolean.class, Object.class); + private static final MethodType MT_INT = MethodType.methodType(int.class); + private static final MethodTypeDesc MTD_OBJECT_BOOLEAN = MethodTypeDesc.of(CD_boolean, CD_Object); + private static final MethodTypeDesc MTD_INT = MethodTypeDesc.of(CD_int); private static final HashMap, MethodHandle> primitiveEquals = new HashMap<>(); private static final HashMap, MethodHandle> primitiveHashers = new HashMap<>(); - private static final HashMap, MethodHandle> primitiveToString = new HashMap<>(); static { try { @@ -76,12 +87,12 @@ public final class ObjectMethods { CLASS_IS_INSTANCE = publicLookup.findVirtual(Class.class, "isInstance", MethodType.methodType(boolean.class, Object.class)); - OBJECTS_EQUALS = publicLookup.findStatic(Objects.class, "equals", - MethodType.methodType(boolean.class, Object.class, Object.class)); - OBJECTS_HASHCODE = publicLookup.findStatic(Objects.class, "hashCode", - MethodType.methodType(int.class, Object.class)); - OBJECTS_TOSTRING = publicLookup.findStatic(Objects.class, "toString", - MethodType.methodType(String.class, Object.class)); + + var objectsIsNull = publicLookup.findStatic(Objects.class, "isNull", + MethodType.methodType(boolean.class, Object.class)); + IS_NULL = objectsIsNull; + IS_ARG0_NULL = MethodHandles.dropArguments(objectsIsNull, 1, Object.class); + IS_ARG1_NULL = MethodHandles.dropArguments(objectsIsNull, 0, Object.class); OBJECT_EQ = lookup.findStatic(OBJECT_METHODS_CLASS, "eq", MethodType.methodType(boolean.class, Object.class, Object.class)); @@ -121,23 +132,6 @@ public final class ObjectMethods { MethodType.methodType(int.class, double.class))); primitiveHashers.put(boolean.class, lookup.findStatic(Boolean.class, "hashCode", MethodType.methodType(int.class, boolean.class))); - - primitiveToString.put(byte.class, lookup.findStatic(Byte.class, "toString", - MethodType.methodType(String.class, byte.class))); - primitiveToString.put(short.class, lookup.findStatic(Short.class, "toString", - MethodType.methodType(String.class, short.class))); - primitiveToString.put(char.class, lookup.findStatic(Character.class, "toString", - MethodType.methodType(String.class, char.class))); - primitiveToString.put(int.class, lookup.findStatic(Integer.class, "toString", - MethodType.methodType(String.class, int.class))); - primitiveToString.put(long.class, lookup.findStatic(Long.class, "toString", - MethodType.methodType(String.class, long.class))); - primitiveToString.put(float.class, lookup.findStatic(Float.class, "toString", - MethodType.methodType(String.class, float.class))); - primitiveToString.put(double.class, lookup.findStatic(Double.class, "toString", - MethodType.methodType(String.class, double.class))); - primitiveToString.put(boolean.class, lookup.findStatic(Boolean.class, "toString", - MethodType.methodType(String.class, boolean.class))); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); @@ -159,24 +153,41 @@ public final class ObjectMethods { private static boolean eq(boolean a, boolean b) { return a == b; } /** Get the method handle for combining two values of a given type */ - private static MethodHandle equalator(Class clazz) { - return (clazz.isPrimitive() - ? primitiveEquals.get(clazz) - : OBJECTS_EQUALS.asType(MethodType.methodType(boolean.class, clazz, clazz))); + private static MethodHandle equalator(MethodHandles.Lookup lookup, Class clazz) throws Throwable { + if (clazz.isPrimitive()) + return primitiveEquals.get(clazz); + MethodType mt = MethodType.methodType(boolean.class, clazz, clazz); + return MethodHandles.guardWithTest(IS_ARG0_NULL.asType(mt), + IS_ARG1_NULL.asType(mt), + lookup.findVirtual(clazz, "equals", MT_OBJECT_BOOLEAN).asType(mt)); } /** Get the hasher for a value of a given type */ - private static MethodHandle hasher(Class clazz) { - return (clazz.isPrimitive() - ? primitiveHashers.get(clazz) - : OBJECTS_HASHCODE.asType(MethodType.methodType(int.class, clazz))); + private static MethodHandle hasher(MethodHandles.Lookup lookup, Class clazz) throws Throwable { + if (clazz.isPrimitive()) + return primitiveHashers.get(clazz); + MethodType mt = MethodType.methodType(int.class, clazz); + return MethodHandles.guardWithTest(IS_NULL.asType(MethodType.methodType(boolean.class, clazz)), + MethodHandles.dropArguments(MethodHandles.zero(int.class), 0, clazz), + lookup.findVirtual(clazz, "hashCode", MT_INT).asType(mt)); } - /** Get the stringifier for a value of a given type */ - private static MethodHandle stringifier(Class clazz) { - return (clazz.isPrimitive() - ? primitiveToString.get(clazz) - : OBJECTS_TOSTRING.asType(MethodType.methodType(String.class, clazz))); + // If this type must be a monomorphic receiver, that is, one that has no + // subtypes in the JVM. For example, Object-typed fields may have a more + // specific one type at runtime and thus need optimizations. + private static boolean isMonomorphic(Class type) { + // Includes primitives and final classes, but not arrays. + // All array classes are reported to be final, but Object[] can have subtypes like String[] + return Modifier.isFinal(type.getModifiers()) && !type.isArray(); + } + + private static String specializerClassName(Class targetClass, String kind) { + String name = targetClass.getName(); + if (targetClass.isHidden()) { + // use the original class name + name = name.replace('/', '_'); + } + return name + "$$" + kind + "Specializer"; } /** @@ -185,8 +196,8 @@ public final class ObjectMethods { * @param getters the list of getters * @return the method handle */ - private static MethodHandle makeEquals(Class receiverClass, - List getters) { + private static MethodHandle makeEquals(MethodHandles.Lookup lookup, Class receiverClass, + List getters) throws Throwable { MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass); MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class); MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z @@ -195,8 +206,70 @@ public final class ObjectMethods { MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z - for (MethodHandle getter : getters) { - MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z + int size = getters.size(); + MethodHandle[] equalators = new MethodHandle[size]; + boolean hasPolymorphism = false; + for (int i = 0; i < size; i++) { + var getter = getters.get(i); + var type = getter.type().returnType(); + if (isMonomorphic(type)) { + equalators[i] = equalator(lookup, type); + } else { + hasPolymorphism = true; + } + } + + // Currently, hotspot does not support polymorphic inlining. + // As a result, if we have a MethodHandle to Object.equals, + // it does not enjoy separate profiles like individual invokevirtuals, + // and we must spin bytecode to accomplish separate profiling. + if (hasPolymorphism) { + String[] names = new String[size]; + + var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup))); + var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Equalator")), clb -> { + for (int i = 0; i < size; i++) { + if (equalators[i] == null) { + var name = "equalator".concat(Integer.toString(i)); + names[i] = name; + var type = getters.get(i).type().returnType(); + boolean isInterface = type.isInterface(); + var typeDesc = type.describeConstable().orElseThrow(); + clb.withMethodBody(name, MethodTypeDesc.of(CD_boolean, typeDesc, typeDesc), ACC_STATIC, cob -> { + var nonNullPath = cob.newLabel(); + var fail = cob.newLabel(); + cob.aload(0) + .ifnonnull(nonNullPath) + .aload(1) + .ifnonnull(fail) + .iconst_1() // arg0 null, arg1 null + .ireturn() + .labelBinding(fail) + .iconst_0() // arg0 null, arg1 non-null + .ireturn() + .labelBinding(nonNullPath) + .aload(0) // arg0.equals(arg1) - bytecode subject to customized profiling + .aload(1) + .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "equals", MTD_OBJECT_BOOLEAN, isInterface) + .ireturn(); + }); + } + } + }); + + var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG); + + for (int i = 0; i < size; i++) { + if (equalators[i] == null) { + var type = getters.get(i).type().returnType(); + equalators[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(boolean.class, type, type)); + } + } + } + + for (int i = 0; i < size; i++) { + var getter = getters.get(i); + MethodHandle equalator = equalators[i]; // (TT)Z MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr)); } @@ -212,13 +285,68 @@ public final class ObjectMethods { * @param getters the list of getters * @return the method handle */ - private static MethodHandle makeHashCode(Class receiverClass, - List getters) { + private static MethodHandle makeHashCode(MethodHandles.Lookup lookup, Class receiverClass, + List getters) throws Throwable { MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I + int size = getters.size(); + MethodHandle[] hashers = new MethodHandle[size]; + boolean hasPolymorphism = false; + for (int i = 0; i < size; i++) { + var getter = getters.get(i); + var type = getter.type().returnType(); + if (isMonomorphic(type)) { + hashers[i] = hasher(lookup, type); + } else { + hasPolymorphism = true; + } + } + + // Currently, hotspot does not support polymorphic inlining. + // As a result, if we have a MethodHandle to Object.hashCode, + // it does not enjoy separate profiles like individual invokevirtuals, + // and we must spin bytecode to accomplish separate profiling. + if (hasPolymorphism) { + String[] names = new String[size]; + + var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup))); + var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Hasher")), clb -> { + for (int i = 0; i < size; i++) { + if (hashers[i] == null) { + var name = "hasher".concat(Integer.toString(i)); + names[i] = name; + var type = getters.get(i).type().returnType(); + boolean isInterface = type.isInterface(); + var typeDesc = type.describeConstable().orElseThrow(); + clb.withMethodBody(name, MethodTypeDesc.of(CD_int, typeDesc), ACC_STATIC, cob -> { + var nonNullPath = cob.newLabel(); + cob.aload(0) + .ifnonnull(nonNullPath) + .iconst_0() // null hash is 0 + .ireturn() + .labelBinding(nonNullPath) + .aload(0) // arg0.hashCode() - bytecode subject to customized profiling + .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "hashCode", MTD_INT, isInterface) + .ireturn(); + }); + } + } + }); + + var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG); + + for (int i = 0; i < size; i++) { + if (hashers[i] == null) { + var type = getters.get(i).type().returnType(); + hashers[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(int.class, type)); + } + } + } + // @@@ Use loop combinator instead? - for (MethodHandle getter : getters) { - MethodHandle hasher = hasher(getter.type().returnType()); // (T)I + for (int i = 0; i < size; i++) { + var getter = getters.get(i); + MethodHandle hasher = hashers[i]; // (T)I MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter); // (R)I MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I @@ -403,12 +531,12 @@ public final class ObjectMethods { case "equals" -> { if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class))) throw new IllegalArgumentException("Bad method type: " + methodType); - yield makeEquals(recordClass, getterList); + yield makeEquals(lookup, recordClass, getterList); } case "hashCode" -> { if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass))) throw new IllegalArgumentException("Bad method type: " + methodType); - yield makeHashCode(recordClass, getterList); + yield makeHashCode(lookup, recordClass, getterList); } case "toString" -> { if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass))) diff --git a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java index f4d82595842..99716baf439 100644 --- a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java +++ b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java @@ -165,22 +165,22 @@ public final class SwitchBootstraps { * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * a reference type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} and {@code Integer} constants * and {@code Class} and {@code EnumDesc} instances, in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise * @throws IllegalArgumentException if any element in the labels array is null * @throws IllegalArgumentException if the invocation type is not a method type of first parameter of a reference type, - * second parameter of type {@code int} and with {@code int} as its return type, + * second parameter of type {@code int} and with {@code int} as its return type * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String}, * {@code Integer}, {@code Long}, {@code Float}, {@code Double}, {@code Boolean}, - * {@code Class} or {@code EnumDesc}. + * {@code Class} or {@code EnumDesc} * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code Boolean} - * when {@code target} is a {@code Boolean.class}. + * when {@code target} is a {@code Boolean.class} * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ @@ -255,29 +255,36 @@ public final class SwitchBootstraps { * enum constant's {@link Enum#name()}. * *

- * If no element in the {@code labels} array matches the target, then - * the method of the call site return the length of the {@code labels} array. + * If for a given {@code target} there is no element in the {@code labels} + * fulfilling one of the above conditions, then the method of the call + * site returns the length of the {@code labels} array. *

* The value of the {@code restart} index must be between {@code 0} (inclusive) and * the length of the {@code labels} array (inclusive), - * both or an {@link IndexOutOfBoundsException} is thrown. + * or an {@link IndexOutOfBoundsException} is thrown. + * + * @apiNote It is permissible for the {@code labels} array to contain {@code String} + * values that do not represent any enum constants at runtime. * * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * an enum type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} constants and {@code Class} instances, * in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} - * @throws IllegalArgumentException if any element in the labels array is null, if the - * invocation type is not a method type whose first parameter type is an enum type, - * second parameter of type {@code int} and whose return type is {@code int}, - * or if {@code labels} contains an element that is not of type {@code String} or - * {@code Class} of the target enum type. + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise + * @throws IllegalArgumentException if any element in the labels array is null + * @throws IllegalArgumentException if any element in the labels array is an empty {@code String} + * @throws IllegalArgumentException if the invocation type is not a method type + * whose first parameter type is an enum type, + * second parameter of type {@code int} and + * whose return type is {@code int} + * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String} or + * {@code Class} equal to the target enum type * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ diff --git a/src/java.base/share/classes/java/math/BigDecimal.java b/src/java.base/share/classes/java/math/BigDecimal.java index c24998344c1..14d81d30c3d 100644 --- a/src/java.base/share/classes/java/math/BigDecimal.java +++ b/src/java.base/share/classes/java/math/BigDecimal.java @@ -154,7 +154,7 @@ import jdk.internal.vm.annotation.Stable; * Subtractmax(minuend.scale(), subtrahend.scale()) * Multiplymultiplier.scale() + multiplicand.scale() * Dividedividend.scale() - divisor.scale() - * Square rootradicand.scale()/2 + * Square rootceil(radicand.scale()/2.0) * * * @@ -327,10 +327,6 @@ import jdk.internal.vm.annotation.Stable; * @spec https://standards.ieee.org/ieee/754/6210/ * IEEE Standard for Floating-Point Arithmetic * - * @author Josh Bloch - * @author Mike Cowlishaw - * @author Joseph D. Darcy - * @author Sergey V. Kuksenko * @since 1.1 */ public class BigDecimal extends Number implements Comparable { @@ -1779,7 +1775,6 @@ public class BigDecimal extends Number implements Comparable { * terminating decimal expansion, including dividing by zero * @return {@code this / divisor} * @since 1.5 - * @author Joseph D. Darcy */ public BigDecimal divide(BigDecimal divisor) { /* @@ -1948,7 +1943,6 @@ public class BigDecimal extends Number implements Comparable { * @throws ArithmeticException if {@code mc.precision} {@literal >} 0 and the result * requires a precision of more than {@code mc.precision} digits. * @since 1.5 - * @author Joseph D. Darcy */ public BigDecimal divideToIntegralValue(BigDecimal divisor, MathContext mc) { if (mc.precision == 0 || // exact result @@ -2119,7 +2113,7 @@ public class BigDecimal extends Number implements Comparable { * with rounding according to the context settings. * *

The preferred scale of the returned result is equal to - * {@code this.scale()/2}. The value of the returned result is + * {@code Math.ceilDiv(this.scale(), 2)}. The value of the returned result is * always within one ulp of the exact decimal value for the * precision in question. If the rounding mode is {@link * RoundingMode#HALF_UP HALF_UP}, {@link RoundingMode#HALF_DOWN @@ -2180,7 +2174,7 @@ public class BigDecimal extends Number implements Comparable { // The code below favors relative simplicity over checking // for special cases that could run faster. - final int preferredScale = this.scale/2; + final int preferredScale = Math.ceilDiv(this.scale, 2); BigDecimal result; if (mc.roundingMode == RoundingMode.UNNECESSARY || mc.precision == 0) { // Exact result requested diff --git a/src/java.base/share/classes/java/math/BigInteger.java b/src/java.base/share/classes/java/math/BigInteger.java index 21f8598266f..ed26f5c1211 100644 --- a/src/java.base/share/classes/java/math/BigInteger.java +++ b/src/java.base/share/classes/java/math/BigInteger.java @@ -2768,9 +2768,9 @@ public class BigInteger extends Number implements Comparable { * @throws ArithmeticException if {@code n} is even and {@code this} is negative. * @see #sqrt() * @since 26 - * @apiNote Note that calling {@code nthRoot(2)} is equivalent to calling {@code sqrt()}. + * @apiNote Note that calling {@code rootn(2)} is equivalent to calling {@code sqrt()}. */ - public BigInteger nthRoot(int n) { + public BigInteger rootn(int n) { if (n == 1) return this; @@ -2778,7 +2778,7 @@ public class BigInteger extends Number implements Comparable { return sqrt(); checkRootDegree(n); - return new MutableBigInteger(this.mag).nthRootRem(n)[0].toBigInteger(signum); + return new MutableBigInteger(this.mag).rootnRem(n)[0].toBigInteger(signum); } /** @@ -2793,12 +2793,12 @@ public class BigInteger extends Number implements Comparable { * @throws ArithmeticException if {@code n} is even and {@code this} is negative. * @see #sqrt() * @see #sqrtAndRemainder() - * @see #nthRoot(int) + * @see #rootn(int) * @since 26 - * @apiNote Note that calling {@code nthRootAndRemainder(2)} is equivalent to calling + * @apiNote Note that calling {@code rootnAndRemainder(2)} is equivalent to calling * {@code sqrtAndRemainder()}. */ - public BigInteger[] nthRootAndRemainder(int n) { + public BigInteger[] rootnAndRemainder(int n) { if (n == 1) return new BigInteger[] { this, ZERO }; @@ -2806,7 +2806,7 @@ public class BigInteger extends Number implements Comparable { return sqrtAndRemainder(); checkRootDegree(n); - MutableBigInteger[] rootRem = new MutableBigInteger(this.mag).nthRootRem(n); + MutableBigInteger[] rootRem = new MutableBigInteger(this.mag).rootnRem(n); return new BigInteger[] { rootRem[0].toBigInteger(signum), rootRem[1].toBigInteger(signum) @@ -4180,6 +4180,10 @@ public class BigInteger extends Number implements Comparable { if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; + if (fitsIntoLong()) { + return Long.toString(longValue(), radix); + } + BigInteger abs = this.abs(); // Ensure buffer capacity sufficient to contain string representation @@ -5121,12 +5125,16 @@ public class BigInteger extends Number implements Comparable { * @since 1.8 */ public long longValueExact() { - if (mag.length <= 2 && bitLength() < Long.SIZE) + if (fitsIntoLong()) return longValue(); throw new ArithmeticException("BigInteger out of long range"); } + private boolean fitsIntoLong() { + return mag.length <= 2 && bitLength() < Long.SIZE; + } + /** * Converts this {@code BigInteger} to an {@code int}, checking * for lost information. If the value of this {@code BigInteger} diff --git a/src/java.base/share/classes/java/math/MathContext.java b/src/java.base/share/classes/java/math/MathContext.java index d0c1cb4a5a9..f80fcc3e076 100644 --- a/src/java.base/share/classes/java/math/MathContext.java +++ b/src/java.base/share/classes/java/math/MathContext.java @@ -51,8 +51,6 @@ import java.io.*; * @spec https://standards.ieee.org/ieee/754/6210/ * IEEE Standard for Floating-Point Arithmetic * - * @author Mike Cowlishaw - * @author Joseph D. Darcy * @since 1.5 */ diff --git a/src/java.base/share/classes/java/math/MutableBigInteger.java b/src/java.base/share/classes/java/math/MutableBigInteger.java index dd1da29ddd2..1ede4cf32f8 100644 --- a/src/java.base/share/classes/java/math/MutableBigInteger.java +++ b/src/java.base/share/classes/java/math/MutableBigInteger.java @@ -1906,7 +1906,7 @@ class MutableBigInteger { * @param n the root degree * @return the integer {@code n}th root of {@code this} and the remainder */ - MutableBigInteger[] nthRootRem(int n) { + MutableBigInteger[] rootnRem(int n) { // Special cases. if (this.isZero() || this.isOne()) return new MutableBigInteger[] { this, new MutableBigInteger() }; @@ -1923,7 +1923,7 @@ class MutableBigInteger { if (bitLength <= Long.SIZE) { // Initial estimate is the root of the unsigned long value. final long x = this.toLong(); - long sLong = (long) nthRootApprox(Math.nextUp(x >= 0 ? x : x + 0x1p64), n) + 1L; + long sLong = (long) rootnApprox(Math.nextUp(x >= 0 ? x : x + 0x1p64), n) + 1L; /* The integer-valued recurrence formula in the algorithm of Brent&Zimmermann * simply discards the fraction part of the real-valued Newton recurrence * on the function f discussed in the referenced work. @@ -1996,7 +1996,7 @@ class MutableBigInteger { // Use the root of the shifted value as an estimate. // rad ≤ 2^ME, so Math.nextUp(rad) < Double.MAX_VALUE rad = Math.nextUp(rad); - approx = nthRootApprox(rad, n); + approx = rootnApprox(rad, n); } else { // fp arithmetic gives too few correct bits // Set the root shift to the root's bit length minus 1 // The initial estimate will be 2^rootLen == 2 << (rootLen - 1) @@ -2050,7 +2050,7 @@ class MutableBigInteger { MutableBigInteger x = new MutableBigInteger(this); x.rightShift(rootSh * n); - newtonRecurrenceNthRoot(x, s, n, s.toBigInteger().pow(n - 1)); + newtonRecurrenceRootn(x, s, n, s.toBigInteger().pow(n - 1)); s.add(ONE); // round up to ensure s is an upper bound of the root } @@ -2060,7 +2060,7 @@ class MutableBigInteger { } // Do the 1st iteration outside the loop to ensure an overestimate - newtonRecurrenceNthRoot(this, s, n, s.toBigInteger().pow(n - 1)); + newtonRecurrenceRootn(this, s, n, s.toBigInteger().pow(n - 1)); // Refine the estimate. do { BigInteger sBig = s.toBigInteger(); @@ -2069,18 +2069,18 @@ class MutableBigInteger { if (rem.subtract(this) <= 0) return new MutableBigInteger[] { s, rem }; - newtonRecurrenceNthRoot(this, s, n, sToN1); + newtonRecurrenceRootn(this, s, n, sToN1); } while (true); } - private static double nthRootApprox(double x, int n) { + private static double rootnApprox(double x, int n) { return Math.nextUp(n == 3 ? Math.cbrt(x) : Math.pow(x, Math.nextUp(1.0 / n))); } /** * Computes {@code ((n-1)*s + x/sToN1)/n} and places the result in {@code s}. */ - private static void newtonRecurrenceNthRoot( + private static void newtonRecurrenceRootn( MutableBigInteger x, MutableBigInteger s, int n, BigInteger sToN1) { MutableBigInteger dividend = new MutableBigInteger(); s.mul(n - 1, dividend); diff --git a/src/java.base/share/classes/java/math/RoundingMode.java b/src/java.base/share/classes/java/math/RoundingMode.java index e66a64e143f..4188c781cab 100644 --- a/src/java.base/share/classes/java/math/RoundingMode.java +++ b/src/java.base/share/classes/java/math/RoundingMode.java @@ -115,9 +115,6 @@ package java.math; * IEEE Standard for Floating-Point Arithmetic * @jls 15.4 Floating-point Expressions * - * @author Josh Bloch - * @author Mike Cowlishaw - * @author Joseph D. Darcy * @since 1.5 */ @SuppressWarnings("deprecation") // Legacy rounding mode constants in BigDecimal diff --git a/src/java.base/share/classes/java/net/DatagramSocket.java b/src/java.base/share/classes/java/net/DatagramSocket.java index 87b52699993..20b4d8a96f1 100644 --- a/src/java.base/share/classes/java/net/DatagramSocket.java +++ b/src/java.base/share/classes/java/net/DatagramSocket.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, 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 @@ -611,13 +611,13 @@ public class DatagramSocket implements java.io.Closeable { * with a {@link DatagramChannel DatagramChannel}. In that case, * interrupting a thread receiving a datagram packet will close the * underlying channel and cause this method to throw {@link - * java.nio.channels.ClosedByInterruptException} with the interrupt - * status set. + * java.nio.channels.ClosedByInterruptException} with the thread's + * interrupted status set. *

  • The datagram socket uses the system-default socket implementation and * a {@linkplain Thread#isVirtual() virtual thread} is receiving a * datagram packet. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. This method will then throw - * {@code SocketException} with the interrupt status set. + * {@code SocketException} with the thread's interrupted status set. * * * @param p the {@code DatagramPacket} into which to place diff --git a/src/java.base/share/classes/java/net/ServerSocket.java b/src/java.base/share/classes/java/net/ServerSocket.java index 945693ef65e..af7cedfd966 100644 --- a/src/java.base/share/classes/java/net/ServerSocket.java +++ b/src/java.base/share/classes/java/net/ServerSocket.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, 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 @@ -406,13 +406,13 @@ public class ServerSocket implements java.io.Closeable { * with a {@link ServerSocketChannel ServerSocketChannel}. In that * case, interrupting a thread accepting a connection will close the * underlying channel and cause this method to throw {@link - * java.nio.channels.ClosedByInterruptException} with the interrupt - * status set. + * java.nio.channels.ClosedByInterruptException} with the thread's + * interrupted status set. *
  • The socket uses the system-default socket implementation and a * {@linkplain Thread#isVirtual() virtual thread} is accepting a * connection. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. This method will then throw - * {@code SocketException} with the interrupt status set. + * {@code SocketException} with the thread's interrupted status set. * * * @implNote diff --git a/src/java.base/share/classes/java/net/Socket.java b/src/java.base/share/classes/java/net/Socket.java index 692c3395f78..a2aee2e45a1 100644 --- a/src/java.base/share/classes/java/net/Socket.java +++ b/src/java.base/share/classes/java/net/Socket.java @@ -573,12 +573,13 @@ public class Socket implements java.io.Closeable { * a {@link SocketChannel SocketChannel}. * In that case, interrupting a thread establishing a connection will * close the underlying channel and cause this method to throw - * {@link ClosedByInterruptException} with the interrupt status set. + * {@link ClosedByInterruptException} with the thread's interrupted + * status set. *
  • The socket uses the system-default socket implementation and a * {@linkplain Thread#isVirtual() virtual thread} is establishing a * connection. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. This method will then throw - * {@code SocketException} with the interrupt status set. + * {@code SocketException} with the thread's interrupted status set. * * * @param endpoint the {@code SocketAddress} @@ -613,12 +614,13 @@ public class Socket implements java.io.Closeable { * a {@link SocketChannel SocketChannel}. * In that case, interrupting a thread establishing a connection will * close the underlying channel and cause this method to throw - * {@link ClosedByInterruptException} with the interrupt status set. + * {@link ClosedByInterruptException} with the thread's interrupted + * status set. *
  • The socket uses the system-default socket implementation and a * {@linkplain Thread#isVirtual() virtual thread} is establishing a * connection. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. This method will then throw - * {@code SocketException} with the interrupt status set. + * {@code SocketException} with the thread's interrupted status set. * * * @apiNote Establishing a TCP/IP connection is subject to connect timeout settings @@ -886,13 +888,14 @@ public class Socket implements java.io.Closeable { * a {@link SocketChannel SocketChannel}. * In that case, interrupting a thread reading from the input stream * will close the underlying channel and cause the read method to - * throw {@link ClosedByInterruptException} with the interrupt - * status set. + * throw {@link ClosedByInterruptException} with the thread's + * interrupted status set. *
  • The socket uses the system-default socket implementation and a * {@linkplain Thread#isVirtual() virtual thread} is reading from the * input stream. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. The read method will then - * throw {@code SocketException} with the interrupt status set. + * throw {@code SocketException} with the thread's interrupted + * status set. * * *

    Under abnormal conditions the underlying connection may be @@ -1026,13 +1029,14 @@ public class Socket implements java.io.Closeable { * a {@link SocketChannel SocketChannel}. * In that case, interrupting a thread writing to the output stream * will close the underlying channel and cause the write method to - * throw {@link ClosedByInterruptException} with the interrupt status - * set. + * throw {@link ClosedByInterruptException} with the thread's + * interrupted status set. *

  • The socket uses the system-default socket implementation and a * {@linkplain Thread#isVirtual() virtual thread} is writing to the * output stream. In that case, interrupting the virtual thread will * cause it to wakeup and close the socket. The write method will then - * throw {@code SocketException} with the interrupt status set. + * throw {@code SocketException} with the thread's interrupted + * status set. * * *

    Closing the returned {@link java.io.OutputStream OutputStream} diff --git a/src/java.base/share/classes/java/net/URI.java b/src/java.base/share/classes/java/net/URI.java index daf63d19032..d568ab1c114 100644 --- a/src/java.base/share/classes/java/net/URI.java +++ b/src/java.base/share/classes/java/net/URI.java @@ -43,6 +43,7 @@ import java.text.Normalizer; import jdk.internal.access.JavaNetUriAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.Exceptions; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.nio.cs.UTF_8; import static jdk.internal.util.Exceptions.filterNonSocketInfo; @@ -516,6 +517,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @see URISyntaxException */ +@AOTSafeClassInitializer public final class URI implements Comparable, Serializable { @@ -3726,6 +3728,7 @@ public final class URI } } + static { SharedSecrets.setJavaNetUriAccess( new JavaNetUriAccess() { diff --git a/src/java.base/share/classes/java/net/URL.java b/src/java.base/share/classes/java/net/URL.java index 9266b6c94f1..1e86f41fd3f 100644 --- a/src/java.base/share/classes/java/net/URL.java +++ b/src/java.base/share/classes/java/net/URL.java @@ -41,8 +41,8 @@ import java.util.ServiceLoader; import jdk.internal.access.JavaNetURLAccess; import jdk.internal.access.SharedSecrets; -import jdk.internal.misc.ThreadTracker; import jdk.internal.misc.VM; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.net.util.IPAddressUtil; import static jdk.internal.util.Exceptions.filterNonSocketInfo; import static jdk.internal.util.Exceptions.formatMsg; @@ -214,6 +214,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @author James Gosling * @since 1.0 */ +@AOTSafeClassInitializer public final class URL implements java.io.Serializable { static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol"; @@ -1391,24 +1392,13 @@ public final class URL implements java.io.Serializable { return handler; } - private static class ThreadTrackHolder { - static final ThreadTracker TRACKER = new ThreadTracker(); - } - - private static Object tryBeginLookup() { - return ThreadTrackHolder.TRACKER.tryBegin(); - } - - private static void endLookup(Object key) { - ThreadTrackHolder.TRACKER.end(key); - } + private static final ScopedValue IN_LOOKUP = ScopedValue.newInstance(); private static URLStreamHandler lookupViaProviders(final String protocol) { - Object key = tryBeginLookup(); - if (key == null) { + if (IN_LOOKUP.isBound()) { throw new Error("Circular loading of URL stream handler providers detected"); } - try { + return ScopedValue.where(IN_LOOKUP, true).call(() -> { final ClassLoader cl = ClassLoader.getSystemClassLoader(); final ServiceLoader sl = ServiceLoader.load(URLStreamHandlerProvider.class, cl); @@ -1420,9 +1410,7 @@ public final class URL implements java.io.Serializable { return h; } return null; - } finally { - endLookup(key); - } + }); } /** diff --git a/src/java.base/share/classes/java/nio/BufferOverflowException.java b/src/java.base/share/classes/java/nio/BufferOverflowException.java new file mode 100644 index 00000000000..22195f8268b --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferOverflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative put operation reaches + * the target buffer's limit. + * + * @since 1.4 + */ + +public class BufferOverflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -5484897634319144535L; + + /** + * Constructs an instance of this class. + */ + public BufferOverflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/BufferUnderflowException.java b/src/java.base/share/classes/java/nio/BufferUnderflowException.java new file mode 100644 index 00000000000..1561f1d4b62 --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferUnderflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative get operation reaches + * the source buffer's limit. + * + * @since 1.4 + */ + +public class BufferUnderflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -1713313658691622206L; + + /** + * Constructs an instance of this class. + */ + public BufferUnderflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/ByteOrder.java b/src/java.base/share/classes/java/nio/ByteOrder.java index 96f2317b956..ab6876448be 100644 --- a/src/java.base/share/classes/java/nio/ByteOrder.java +++ b/src/java.base/share/classes/java/nio/ByteOrder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -35,28 +35,19 @@ import jdk.internal.misc.Unsafe; * @since 1.4 */ -public final class ByteOrder { - - private final String name; - - private ByteOrder(String name) { - this.name = name; - } - - /** - * Constant denoting big-endian byte order. In this order, the bytes of a - * multibyte value are ordered from most significant to least significant. - */ - public static final ByteOrder BIG_ENDIAN - = new ByteOrder("BIG_ENDIAN"); - +public enum ByteOrder { /** * Constant denoting little-endian byte order. In this order, the bytes of * a multibyte value are ordered from least significant to most * significant. */ - public static final ByteOrder LITTLE_ENDIAN - = new ByteOrder("LITTLE_ENDIAN"); + LITTLE_ENDIAN, + /** + * Constant denoting big-endian byte order. In this order, the bytes of a + * multibyte value are ordered from most significant to least significant. + */ + BIG_ENDIAN; + // Retrieve the native byte order. It's used early during bootstrap, and // must be initialized after BIG_ENDIAN and LITTLE_ENDIAN. @@ -78,18 +69,4 @@ public final class ByteOrder { public static ByteOrder nativeOrder() { return NATIVE_ORDER; } - - /** - * Constructs a string describing this object. - * - *

    This method returns the string - * {@code "BIG_ENDIAN"} for {@link #BIG_ENDIAN} and - * {@code "LITTLE_ENDIAN"} for {@link #LITTLE_ENDIAN}. - * - * @return The specified string - */ - public String toString() { - return name; - } - } diff --git a/src/java.base/share/classes/java/nio/InvalidMarkException.java b/src/java.base/share/classes/java/nio/InvalidMarkException.java new file mode 100644 index 00000000000..8ee16693c0d --- /dev/null +++ b/src/java.base/share/classes/java/nio/InvalidMarkException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio; + +/** + * Unchecked exception thrown when an attempt is made to reset a buffer + * when its mark is not defined. + * + * @since 1.4 + */ + +public class InvalidMarkException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1698329710438510774L; + + /** + * Constructs an instance of this class. + */ + public InvalidMarkException() { } +} diff --git a/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java new file mode 100644 index 00000000000..e86123307f3 --- /dev/null +++ b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a content-mutation method such as + * put or compact is invoked upon a read-only buffer. + * + * @since 1.4 + */ + +public class ReadOnlyBufferException + extends UnsupportedOperationException +{ + + @java.io.Serial + private static final long serialVersionUID = -1210063976496234090L; + + /** + * Constructs an instance of this class. + */ + public ReadOnlyBufferException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java new file mode 100644 index 00000000000..d02b3f289b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to initiate an accept + * operation on a channel and a previous accept operation has not completed. + * + * @since 1.7 + */ + +public class AcceptPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2721339977965416421L; + + /** + * Constructs an instance of this class. + */ + public AcceptPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java new file mode 100644 index 00000000000..4602e8d1f46 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind the socket a + * network oriented channel that is already bound. + * + * @since 1.7 + */ + +public class AlreadyBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6796072983322737592L; + + /** + * Constructs an instance of this class. + */ + public AlreadyBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java new file mode 100644 index 00000000000..48fe018a0b7 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} that is already connected. + * + * @since 1.4 + */ + +public class AlreadyConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7331895245053773357L; + + /** + * Constructs an instance of this class. + */ + public AlreadyConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java new file mode 100644 index 00000000000..bc3ee02f50b --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread closes the + * channel or the part of the channel upon which it is blocked in an I/O + * operation. + * + * @since 1.4 + */ + +public class AsynchronousCloseException + extends ClosedChannelException +{ + + @java.io.Serial + private static final long serialVersionUID = 6891178312432313966L; + + /** + * Constructs an instance of this class. + */ + public AsynchronousCloseException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java new file mode 100644 index 00000000000..946ea7205b1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to use + * a selection key that is no longer valid. + * + * @since 1.4 + */ + +public class CancelledKeyException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8438032138028814268L; + + /** + * Constructs an instance of this class. + */ + public CancelledKeyException() { } +} diff --git a/src/hotspot/share/gc/shared/strongRootsScope.cpp b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java similarity index 52% rename from src/hotspot/share/gc/shared/strongRootsScope.cpp rename to src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java index 1316df68e5f..a2f62aaed32 100644 --- a/src/hotspot/share/gc/shared/strongRootsScope.cpp +++ b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,33 +21,28 @@ * 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. - * */ -#include "classfile/stringTable.hpp" -#include "code/nmethod.hpp" -#include "gc/shared/strongRootsScope.hpp" -#include "runtime/threads.hpp" +package java.nio.channels; -MarkScope::MarkScope() { - nmethod::oops_do_marking_prologue(); -} +/** + * Checked exception received by a thread when another thread interrupts it + * while it is blocked in an I/O operation upon a channel. Before this + * exception is thrown the channel will have been closed and the interrupted + * status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ -MarkScope::~MarkScope() { - nmethod::oops_do_marking_epilogue(); -} +public class ClosedByInterruptException + extends AsynchronousCloseException +{ -StrongRootsScope::StrongRootsScope(uint n_threads) : _n_threads(n_threads) { - // No need for thread claim for statically-known sequential case (_n_threads == 0) - // For positive values, clients of this class often unify sequential/parallel - // cases, so they expect the thread claim token to be updated. - if (_n_threads != 0) { - Threads::change_thread_claim_token(); - } -} + @java.io.Serial + private static final long serialVersionUID = -4488191543534286750L; -StrongRootsScope::~StrongRootsScope() { - if (_n_threads != 0) { - Threads::assert_all_threads_claimed(); - } + /** + * Constructs an instance of this class. + */ + public ClosedByInterruptException() { } } diff --git a/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java new file mode 100644 index 00000000000..71452b6d1c2 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Checked exception thrown when an attempt is made to invoke or complete an + * I/O operation upon channel that is closed, or at least closed to that + * operation. That this exception is thrown does not necessarily imply that + * the channel is completely closed. A socket channel whose write half has + * been shut down, for example, may still be open for reading. + * + * @since 1.4 + */ + +public class ClosedChannelException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 882777185433553857L; + + /** + * Constructs an instance of this class. + */ + public ClosedChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java new file mode 100644 index 00000000000..dcefb98d69e --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a closed selector. + * + * @since 1.4 + */ + +public class ClosedSelectorException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6466297122317847835L; + + /** + * Constructs an instance of this class. + */ + public ClosedSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java new file mode 100644 index 00000000000..2a5b80e3b25 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} for which a non-blocking connection operation is already in + * progress. + * + * @since 1.4 + */ + +public class ConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2008393366501760879L; + + /** + * Constructs an instance of this class. + */ + public ConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/DatagramChannel.java b/src/java.base/share/classes/java/nio/channels/DatagramChannel.java index 392d9add37f..5e72efcdea6 100644 --- a/src/java.base/share/classes/java/nio/channels/DatagramChannel.java +++ b/src/java.base/share/classes/java/nio/channels/DatagramChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -290,7 +290,7 @@ public abstract class DatagramChannel * If another thread interrupts the current thread * while the connect operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws UnresolvedAddressException * If the given remote address is not fully resolved @@ -389,7 +389,7 @@ public abstract class DatagramChannel * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs @@ -443,7 +443,7 @@ public abstract class DatagramChannel * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws UnresolvedAddressException * If the given remote address is not fully resolved diff --git a/src/java.base/share/classes/java/nio/channels/FileChannel.java b/src/java.base/share/classes/java/nio/channels/FileChannel.java index 6e78eefcca6..e31d01b34c0 100644 --- a/src/java.base/share/classes/java/nio/channels/FileChannel.java +++ b/src/java.base/share/classes/java/nio/channels/FileChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -662,7 +662,7 @@ public abstract class FileChannel * @throws ClosedByInterruptException * If another thread interrupts the current thread while the * transfer is in progress, thereby closing both channels and - * setting the current thread's interrupt status + * setting the current thread's interrupted status * * @throws IOException * If some other I/O error occurs @@ -732,7 +732,7 @@ public abstract class FileChannel * @throws ClosedByInterruptException * If another thread interrupts the current thread while the * transfer is in progress, thereby closing both channels and - * setting the current thread's interrupt status + * setting the current thread's interrupted status * * @throws IOException * If some other I/O error occurs @@ -780,7 +780,7 @@ public abstract class FileChannel * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs @@ -829,7 +829,7 @@ public abstract class FileChannel * If another thread interrupts the current thread * while the write operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs @@ -1093,10 +1093,10 @@ public abstract class FileChannel * this method then an {@link AsynchronousCloseException} will be thrown. * *

    If the invoking thread is interrupted while waiting to acquire the - * lock then its interrupt status will be set and a {@link + * lock then its interrupted status will be set and a {@link * FileLockInterruptionException} will be thrown. If the invoker's - * interrupt status is set when this method is invoked then that exception - * will be thrown immediately; the thread's interrupt status will not be + * interrupted status is set when this method is invoked then that exception + * will be thrown immediately; the thread's interrupted status will not be * changed. * *

    The region specified by the {@code position} and {@code size} diff --git a/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java new file mode 100644 index 00000000000..ae1f12f15fc --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread interrupts it + * while it is waiting to acquire a file lock. Before this exception is thrown + * the interrupted status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ + +public class FileLockInterruptionException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 7104080643653532383L; + + /** + * Constructs an instance of this class. + */ + public FileLockInterruptionException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/GatheringByteChannel.java b/src/java.base/share/classes/java/nio/channels/GatheringByteChannel.java index 4e3b0cf136d..0b03b9c8196 100644 --- a/src/java.base/share/classes/java/nio/channels/GatheringByteChannel.java +++ b/src/java.base/share/classes/java/nio/channels/GatheringByteChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -76,11 +76,14 @@ public interface GatheringByteChannel * the final position of each updated buffer, except the last updated * buffer, is guaranteed to be equal to that buffer's limit. * - *

    Unless otherwise specified, a write operation will return only after + *

    For many types of channels, a write operation will return only after * writing all of the r requested bytes. Some types of channels, * depending upon their state, may write only some of the bytes or possibly - * none at all. A socket channel in non-blocking mode, for example, cannot - * write any more bytes than are free in the socket's output buffer. + * none at all. A socket channel in {@linkplain + * SelectableChannel#isBlocking non-blocking mode}, for example, cannot + * write any more bytes than are free in the socket's output buffer. The + * write method may need to be invoked more than once to ensure that all + * {@linkplain ByteBuffer#hasRemaining remaining} bytes are written. * *

    This method may be invoked at any time. If another thread has * already initiated a write operation upon this channel, however, then an @@ -120,7 +123,7 @@ public interface GatheringByteChannel * If another thread interrupts the current thread * while the write operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs @@ -158,7 +161,7 @@ public interface GatheringByteChannel * If another thread interrupts the current thread * while the write operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs diff --git a/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java new file mode 100644 index 00000000000..28d1627ef34 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when a blocking-mode-specific operation + * is invoked upon a channel in the incorrect blocking mode. + * + * @since 1.4 + */ + +public class IllegalBlockingModeException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3335774961855590474L; + + /** + * Constructs an instance of this class. + */ + public IllegalBlockingModeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java new file mode 100644 index 00000000000..3e5da2b87c4 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to open a channel + * in a group that was not created by the same provider. + * + * @since 1.7 + */ + +public class IllegalChannelGroupException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2495041211157744253L; + + /** + * Constructs an instance of this class. + */ + public IllegalChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java new file mode 100644 index 00000000000..d1e26c3352f --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to register a channel + * with a selector that was not created by the provider that created the + * channel. + * + * @since 1.4 + */ + +public class IllegalSelectorException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -8406323347253320987L; + + /** + * Constructs an instance of this class. + */ + public IllegalSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java new file mode 100644 index 00000000000..3dc6931e7fb --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when a timeout elapses before an + * asynchronous operation completes. + * + * @since 1.7 + */ + +public class InterruptedByTimeoutException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = -4268008601014042947L; + + /** + * Constructs an instance of this class. + */ + public InterruptedByTimeoutException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/InterruptibleChannel.java b/src/java.base/share/classes/java/nio/channels/InterruptibleChannel.java index d13a37aeae4..c1c3628f7a4 100644 --- a/src/java.base/share/classes/java/nio/channels/InterruptibleChannel.java +++ b/src/java.base/share/classes/java/nio/channels/InterruptibleChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, 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 @@ -45,11 +45,11 @@ import java.io.IOException; * another thread may invoke the blocked thread's {@link Thread#interrupt() * interrupt} method. This will cause the channel to be closed, the blocked * thread to receive a {@link ClosedByInterruptException}, and the blocked - * thread's interrupt status to be set. + * thread's interrupted status to be set. * - *

    If a thread's interrupt status is already set and it invokes a blocking + *

    If a thread's interrupted status is already set and it invokes a blocking * I/O operation upon a channel then the channel will be closed and the thread - * will immediately receive a {@link ClosedByInterruptException}; its interrupt + * will immediately receive a {@link ClosedByInterruptException}; its interrupted * status will remain set. * *

    A channel supports asynchronous closing and interruption if, and only diff --git a/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java new file mode 100644 index 00000000000..954486e4d35 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when the {@link SocketChannel#finishConnect + * finishConnect} method of a {@link SocketChannel} is invoked without first + * successfully invoking its {@link SocketChannel#connect connect} method. + * + * @since 1.4 + */ + +public class NoConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8296561183633134743L; + + /** + * Constructs an instance of this class. + */ + public NoConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java new file mode 100644 index 00000000000..4714ace0b30 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read + * from a channel that was not originally opened for reading. + * + * @since 1.4 + */ + +public class NonReadableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3200915679294993514L; + + /** + * Constructs an instance of this class. + */ + public NonReadableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java new file mode 100644 index 00000000000..70877bb46b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write + * to a channel that was not originally opened for writing. + * + * @since 1.4 + */ + +public class NonWritableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7071230488279011621L; + + /** + * Constructs an instance of this class. + */ + public NonWritableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java new file mode 100644 index 00000000000..12f222b7692 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a server socket channel that is not yet bound. + * + * @since 1.4 + */ + +public class NotYetBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4640999303950202242L; + + /** + * Constructs an instance of this class. + */ + public NotYetBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java new file mode 100644 index 00000000000..8ea6150de7d --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a socket channel that is not yet connected. + * + * @since 1.4 + */ + +public class NotYetConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4697316551909513464L; + + /** + * Constructs an instance of this class. + */ + public NotYetConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java new file mode 100644 index 00000000000..a24019ffcd1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to acquire a lock on a + * region of a file that overlaps a region already locked by the same Java + * virtual machine, or when another thread is already waiting to lock an + * overlapping region of the same file. + * + * @since 1.4 + */ + +public class OverlappingFileLockException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2047812138163068433L; + + /** + * Constructs an instance of this class. + */ + public OverlappingFileLockException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ReadPendingException.java b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java new file mode 100644 index 00000000000..113c3fb27a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read from an + * asynchronous socket channel and a previous read has not completed. + * + * @since 1.7 + */ + +public class ReadPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1986315242191227217L; + + /** + * Constructs an instance of this class. + */ + public ReadPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ReadableByteChannel.java b/src/java.base/share/classes/java/nio/channels/ReadableByteChannel.java index 7d544458390..37b3a5442bc 100644 --- a/src/java.base/share/classes/java/nio/channels/ReadableByteChannel.java +++ b/src/java.base/share/classes/java/nio/channels/ReadableByteChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -101,7 +101,7 @@ public interface ReadableByteChannel extends Channel { * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs diff --git a/src/java.base/share/classes/java/nio/channels/ScatteringByteChannel.java b/src/java.base/share/classes/java/nio/channels/ScatteringByteChannel.java index 27fce3c1e09..66ff5047d70 100644 --- a/src/java.base/share/classes/java/nio/channels/ScatteringByteChannel.java +++ b/src/java.base/share/classes/java/nio/channels/ScatteringByteChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -119,7 +119,7 @@ public interface ScatteringByteChannel * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs @@ -160,7 +160,7 @@ public interface ScatteringByteChannel * If another thread interrupts the current thread * while the read operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs diff --git a/src/java.base/share/classes/java/nio/channels/Selector.java b/src/java.base/share/classes/java/nio/channels/Selector.java index d8c0dc261bd..b90b8929a51 100644 --- a/src/java.base/share/classes/java/nio/channels/Selector.java +++ b/src/java.base/share/classes/java/nio/channels/Selector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -236,7 +236,7 @@ import java.util.function.Consumer; * *

  • By invoking the blocked thread's {@link * java.lang.Thread#interrupt() interrupt} method, in which case its - * interrupt status will be set and the selector's {@link #wakeup wakeup} + * interrupted status will be set and the selector's {@link #wakeup wakeup} * method will be invoked.

  • * * diff --git a/src/java.base/share/classes/java/nio/channels/ServerSocketChannel.java b/src/java.base/share/classes/java/nio/channels/ServerSocketChannel.java index b2ee728cf7f..6cf2520fd78 100644 --- a/src/java.base/share/classes/java/nio/channels/ServerSocketChannel.java +++ b/src/java.base/share/classes/java/nio/channels/ServerSocketChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -328,7 +328,7 @@ public abstract class ServerSocketChannel * If another thread interrupts the current thread * while the accept operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws NotYetBoundException * If this channel's socket has not yet been bound diff --git a/src/hotspot/share/gc/shared/strongRootsScope.hpp b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java similarity index 53% rename from src/hotspot/share/gc/shared/strongRootsScope.hpp rename to src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java index 2d33753e4b0..d25f6f568ae 100644 --- a/src/hotspot/share/gc/shared/strongRootsScope.hpp +++ b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,31 +21,27 @@ * 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. - * */ -#ifndef SHARE_GC_SHARED_STRONGROOTSSCOPE_HPP -#define SHARE_GC_SHARED_STRONGROOTSSCOPE_HPP +package java.nio.channels; -#include "memory/allocation.hpp" +/** + * Unchecked exception thrown when an attempt is made to construct a channel in + * a group that is shutdown or the completion handler for an I/O operation + * cannot be invoked because the channel group has terminated. + * + * @since 1.7 + */ -class MarkScope : public StackObj { - protected: - MarkScope(); - ~MarkScope(); -}; +public class ShutdownChannelGroupException + extends IllegalStateException +{ -// Sets up and tears down the required state for sequential/parallel root processing. -class StrongRootsScope : public MarkScope { - // Number of threads participating in the roots processing. - // 0 means statically-known sequential root processing; used only by Serial GC - const uint _n_threads; + @java.io.Serial + private static final long serialVersionUID = -3903801676350154157L; - public: - StrongRootsScope(uint n_threads); - ~StrongRootsScope(); - - uint n_threads() const { return _n_threads; } -}; - -#endif // SHARE_GC_SHARED_STRONGROOTSSCOPE_HPP + /** + * Constructs an instance of this class. + */ + public ShutdownChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/SocketChannel.java b/src/java.base/share/classes/java/nio/channels/SocketChannel.java index 26878ab4006..493f9e88ebf 100644 --- a/src/java.base/share/classes/java/nio/channels/SocketChannel.java +++ b/src/java.base/share/classes/java/nio/channels/SocketChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -250,7 +250,7 @@ public abstract class SocketChannel * If another thread interrupts the current thread * while the connect operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws UnresolvedAddressException * If the given remote address is an InetSocketAddress that is not fully @@ -485,7 +485,7 @@ public abstract class SocketChannel * If another thread interrupts the current thread * while the connect operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws UnresolvedAddressException * If the given remote address is an InetSocketAddress that is not fully resolved @@ -542,7 +542,7 @@ public abstract class SocketChannel * If another thread interrupts the current thread * while the connect operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs diff --git a/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java new file mode 100644 index 00000000000..86ae3e6d7a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke a network + * operation upon an unresolved socket address. + * + * @since 1.4 + */ + +public class UnresolvedAddressException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 6136959093620794148L; + + /** + * Constructs an instance of this class. + */ + public UnresolvedAddressException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java new file mode 100644 index 00000000000..0b63cd8d418 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind or connect + * to a socket address of a type that is not supported. + * + * @since 1.4 + */ + +public class UnsupportedAddressTypeException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2964323842829700493L; + + /** + * Constructs an instance of this class. + */ + public UnsupportedAddressTypeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/WritableByteChannel.java b/src/java.base/share/classes/java/nio/channels/WritableByteChannel.java index ef8efa5037c..063cd5eb938 100644 --- a/src/java.base/share/classes/java/nio/channels/WritableByteChannel.java +++ b/src/java.base/share/classes/java/nio/channels/WritableByteChannel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -65,11 +65,14 @@ public interface WritableByteChannel * Upon return the buffer's position will be equal to * p {@code +} n; its limit will not have changed. * - *

    Unless otherwise specified, a write operation will return only after + *

    For many types of channels, a write operation will return only after * writing all of the r requested bytes. Some types of channels, * depending upon their state, may write only some of the bytes or possibly - * none at all. A socket channel in non-blocking mode, for example, cannot - * write any more bytes than are free in the socket's output buffer. + * none at all. A socket channel in {@linkplain + * SelectableChannel#isBlocking non-blocking mode}, for example, cannot + * write any more bytes than are free in the socket's output buffer. The + * write method may need to be invoked more than once to ensure that all + * {@linkplain ByteBuffer#hasRemaining remaining} bytes are written. * *

    This method may be invoked at any time. If another thread has * already initiated a write operation upon this channel, however, then an @@ -95,7 +98,7 @@ public interface WritableByteChannel * If another thread interrupts the current thread * while the write operation is in progress, thereby * closing the channel and setting the current thread's - * interrupt status + * interrupted status * * @throws IOException * If some other I/O error occurs diff --git a/src/java.base/share/classes/java/nio/channels/WritePendingException.java b/src/java.base/share/classes/java/nio/channels/WritePendingException.java new file mode 100644 index 00000000000..f4dca987247 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/WritePendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write to an + * asynchronous socket channel and a previous write has not completed. + * + * @since 1.7 + */ + +public class WritePendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 7031871839266032276L; + + /** + * Constructs an instance of this class. + */ + public WritePendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/exceptions b/src/java.base/share/classes/java/nio/channels/exceptions deleted file mode 100644 index 3f75fb3fcd2..00000000000 --- a/src/java.base/share/classes/java/nio/channels/exceptions +++ /dev/null @@ -1,194 +0,0 @@ -# -# Copyright (c) 2000, 2009, 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. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# 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. -# - -# Generated exception classes for java.nio.channels - -SINCE=1.4 -PACKAGE=java.nio.channels -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2007," - - -SUPER=java.io.IOException - -gen ClosedChannelException " - * Checked exception thrown when an attempt is made to invoke or complete an - * I/O operation upon channel that is closed, or at least closed to that - * operation. That this exception is thrown does not necessarily imply that - * the channel is completely closed. A socket channel whose write half has - * been shut down, for example, may still be open for reading." \ - 882777185433553857L - -gen FileLockInterruptionException " - * Checked exception received by a thread when another thread interrupts it - * while it is waiting to acquire a file lock. Before this exception is thrown - * the interrupt status of the previously-blocked thread will have been set." \ - 7104080643653532383L - - -SUPER=ClosedChannelException - -gen AsynchronousCloseException " - * Checked exception received by a thread when another thread closes the - * channel or the part of the channel upon which it is blocked in an I/O - * operation." \ - 6891178312432313966L - - -SUPER=AsynchronousCloseException - -gen ClosedByInterruptException " - * Checked exception received by a thread when another thread interrupts it - * while it is blocked in an I/O operation upon a channel. Before this - * exception is thrown the channel will have been closed and the interrupt - * status of the previously-blocked thread will have been set." \ - -4488191543534286750L - - -SUPER=IllegalArgumentException - -gen IllegalSelectorException " - * Unchecked exception thrown when an attempt is made to register a channel - * with a selector that was not created by the provider that created the - * channel." \ - -8406323347253320987L - -gen UnresolvedAddressException " - * Unchecked exception thrown when an attempt is made to invoke a network - * operation upon an unresolved socket address." \ - 6136959093620794148L - -gen UnsupportedAddressTypeException " - * Unchecked exception thrown when an attempt is made to bind or connect - * to a socket address of a type that is not supported." \ - -2964323842829700493L - - -SUPER=IllegalStateException - -gen AlreadyConnectedException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} that is already connected." \ - -7331895245053773357L - -gen ConnectionPendingException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} for which a non-blocking connection operation is already in - * progress." \ - 2008393366501760879L - -gen ClosedSelectorException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a closed selector." \ - 6466297122317847835L - -gen CancelledKeyException " - * Unchecked exception thrown when an attempt is made to use - * a selection key that is no longer valid." \ - -8438032138028814268L - -gen IllegalBlockingModeException " - * Unchecked exception thrown when a blocking-mode-specific operation - * is invoked upon a channel in the incorrect blocking mode." \ - -3335774961855590474L - -gen NoConnectionPendingException " - * Unchecked exception thrown when the {@link SocketChannel#finishConnect - * finishConnect} method of a {@link SocketChannel} is invoked without first - * successfully invoking its {@link SocketChannel#connect connect} method." \ - -8296561183633134743L - -gen NonReadableChannelException " - * Unchecked exception thrown when an attempt is made to read - * from a channel that was not originally opened for reading." \ - -3200915679294993514L - -gen NonWritableChannelException " - * Unchecked exception thrown when an attempt is made to write - * to a channel that was not originally opened for writing." \ - -7071230488279011621L - -gen NotYetBoundException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a server socket channel that is not yet bound." \ - 4640999303950202242L - -gen NotYetConnectedException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a socket channel that is not yet connected." \ - 4697316551909513464L - -gen OverlappingFileLockException " - * Unchecked exception thrown when an attempt is made to acquire a lock on a - * region of a file that overlaps a region already locked by the same Java - * virtual machine, or when another thread is already waiting to lock an - * overlapping region of the same file." \ - 2047812138163068433L - - -SINCE=1.7 - -SUPER=java.io.IOException - -gen InterruptedByTimeoutException " - * Checked exception received by a thread when a timeout elapses before an - * asynchronous operation completes." \ - -4268008601014042947L - -SUPER=IllegalArgumentException - -gen IllegalChannelGroupException " - * Unchecked exception thrown when an attempt is made to open a channel - * in a group that was not created by the same provider. " \ - -2495041211157744253L - - -SUPER=IllegalStateException - -gen AlreadyBoundException " - * Unchecked exception thrown when an attempt is made to bind the socket a - * network oriented channel that is already bound." \ - 6796072983322737592L - -gen AcceptPendingException " - * Unchecked exception thrown when an attempt is made to initiate an accept - * operation on a channel and a previous accept operation has not completed." \ - 2721339977965416421L - -gen ReadPendingException " - * Unchecked exception thrown when an attempt is made to read from an - * asynchronous socket channel and a previous read has not completed." \ - 1986315242191227217L - -gen WritePendingException " - * Unchecked exception thrown when an attempt is made to write to an - * asynchronous socket channel and a previous write has not completed." \ - 7031871839266032276L - -gen ShutdownChannelGroupException " - * Unchecked exception thrown when an attempt is made to construct a channel in - * a group that is shutdown or the completion handler for an I/O operation - * cannot be invoked because the channel group has terminated." \ - -3903801676350154157L diff --git a/src/hotspot/share/gc/shared/softRefPolicy.hpp b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java similarity index 58% rename from src/hotspot/share/gc/shared/softRefPolicy.hpp rename to src/java.base/share/classes/java/nio/charset/CharacterCodingException.java index 1e8ec356c40..00a8efae4dd 100644 --- a/src/hotspot/share/gc/shared/softRefPolicy.hpp +++ b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,24 +21,26 @@ * 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. - * */ -#ifndef SHARE_GC_SHARED_SOFTREFPOLICY_HPP -#define SHARE_GC_SHARED_SOFTREFPOLICY_HPP +package java.nio.charset; -class SoftRefPolicy { - private: - // Set to true when policy wants soft refs cleared. - // Reset to false by gc after it clears all soft refs. - bool _should_clear_all_soft_refs; +/** + * Checked exception thrown when a character encoding + * or decoding error occurs. + * + * @since 1.4 + */ - public: - SoftRefPolicy() : - _should_clear_all_soft_refs(false) {} +public class CharacterCodingException + extends java.io.IOException +{ - bool should_clear_all_soft_refs() { return _should_clear_all_soft_refs; } - void set_should_clear_all_soft_refs(bool v) { _should_clear_all_soft_refs = v; } -}; + @java.io.Serial + private static final long serialVersionUID = 8421532232154627783L; -#endif // SHARE_GC_SHARED_SOFTREFPOLICY_HPP + /** + * Constructs an instance of this class. + */ + public CharacterCodingException() { } +} diff --git a/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java new file mode 100644 index 00000000000..992168883bb --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when a string that is not a + * legal charset name is used as such. + * + * @since 1.4 + */ + +public class IllegalCharsetNameException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1457525358470002989L; + + /** + * The illegal charset name. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The illegal charset name + */ + public IllegalCharsetNameException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the illegal charset name. + * + * @return The illegal charset name + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java new file mode 100644 index 00000000000..da50e0cfd0c --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when no support is available + * for a requested charset. + * + * @since 1.4 + */ + +public class UnsupportedCharsetException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1490765524727386367L; + + /** + * The name of the unsupported charset. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The name of the unsupported charset + */ + public UnsupportedCharsetException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the name of the unsupported charset. + * + * @return The name of the unsupported charset + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/exceptions b/src/java.base/share/classes/java/nio/charset/exceptions deleted file mode 100644 index c4773090ae0..00000000000 --- a/src/java.base/share/classes/java/nio/charset/exceptions +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright (c) 2000, 2021, 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. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# 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. -# - -# Generated exception classes for java.nio.charset - -SINCE=1.4 -PACKAGE=java.nio.charset -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - -SUPER=java.io.IOException - -gen CharacterCodingException " - * Checked exception thrown when a character encoding - * or decoding error occurs." \ - 8421532232154627783L - - -SUPER=IllegalArgumentException - -gen IllegalCharsetNameException " - * Unchecked exception thrown when a string that is not a - * legal charset name is used as such." \ - 1457525358470002989L \ - String charsetName CharsetName "illegal charset name" - -gen UnsupportedCharsetException " - * Unchecked exception thrown when no support is available - * for a requested charset." \ - 1490765524727386367L \ - String charsetName CharsetName "name of the unsupported charset" diff --git a/src/java.base/share/classes/java/nio/exceptions b/src/java.base/share/classes/java/nio/exceptions deleted file mode 100644 index 7465cdbed78..00000000000 --- a/src/java.base/share/classes/java/nio/exceptions +++ /dev/null @@ -1,60 +0,0 @@ -# -# Copyright (c) 2000, 2021, 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. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# 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. -# - -# Generated exception classes for java.nio - -SINCE=1.4 -PACKAGE=java.nio -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - - -SUPER=RuntimeException - -gen BufferOverflowException " - * Unchecked exception thrown when a relative put operation reaches - * the target buffer's limit." \ - -5484897634319144535L - -gen BufferUnderflowException " - * Unchecked exception thrown when a relative get operation reaches - * the source buffer's limit." \ - -1713313658691622206L - - -SUPER=IllegalStateException - -gen InvalidMarkException " - * Unchecked exception thrown when an attempt is made to reset a buffer - * when its mark is not defined." \ - 1698329710438510774L - - -SUPER=UnsupportedOperationException - -gen ReadOnlyBufferException " - * Unchecked exception thrown when a content-mutation method such as - * put or compact is invoked upon a read-only buffer." \ - -1210063976496234090L diff --git a/src/java.base/share/classes/java/security/SecureClassLoader.java b/src/java.base/share/classes/java/security/SecureClassLoader.java index b398d7332d7..7b0420ec601 100644 --- a/src/java.base/share/classes/java/security/SecureClassLoader.java +++ b/src/java.base/share/classes/java/security/SecureClassLoader.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import jdk.internal.misc.CDS; /** * This class extends {@code ClassLoader} with additional support for defining @@ -243,6 +244,20 @@ public class SecureClassLoader extends ClassLoader { * Called by the VM, during -Xshare:dump */ private void resetArchivedStates() { - pdcache.clear(); + if (CDS.isDumpingAOTLinkedClasses()) { + for (CodeSourceKey key : pdcache.keySet()) { + if (key.cs.getCodeSigners() != null) { + // We don't archive any signed classes, so we don't need to cache their ProtectionDomains. + pdcache.remove(key); + } + } + if (System.getProperty("cds.debug.archived.protection.domains") != null) { + for (CodeSourceKey key : pdcache.keySet()) { + System.out.println("Archiving ProtectionDomain " + key.cs + " for " + this); + } + } + } else { + pdcache.clear(); + } } } diff --git a/src/java.base/share/classes/java/text/CompactNumberFormat.java b/src/java.base/share/classes/java/text/CompactNumberFormat.java index 7163b2dd63b..74c1eabe970 100644 --- a/src/java.base/share/classes/java/text/CompactNumberFormat.java +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java @@ -250,38 +250,43 @@ public final class CompactNumberFormat extends NumberFormat { /** * List of positive prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positivePrefixPatterns; /** * List of negative prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativePrefixPatterns; /** * List of positive suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positiveSuffixPatterns; /** * List of negative suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativeSuffixPatterns; /** * List of divisors of this formatter's compact number patterns. * Divisor can be either Long or BigInteger (if the divisor value goes - * beyond long boundary) + * beyond long boundary). This field is a read-only constant + * once initialized. */ private transient List divisors; /** * List of place holders that represent minimum integer digits at each index - * for each count. + * for each count. This field is a read-only constant once initialized. */ private transient List placeHolderPatterns; @@ -374,7 +379,7 @@ public final class CompactNumberFormat extends NumberFormat { /** * The map for plural rules that maps LDML defined tags (e.g. "one") to - * its rule. + * its rule. This field is a read-only constant once initialized. */ private transient Map rulesMap; @@ -1515,7 +1520,7 @@ public final class CompactNumberFormat extends NumberFormat { } } - private final transient DigitList digitList = new DigitList(); + private transient DigitList digitList = new DigitList(); private static final int STATUS_INFINITE = 0; private static final int STATUS_POSITIVE = 1; private static final int STATUS_LENGTH = 2; @@ -2506,8 +2511,14 @@ public final class CompactNumberFormat extends NumberFormat { @Override public CompactNumberFormat clone() { CompactNumberFormat other = (CompactNumberFormat) super.clone(); + + // Cloning reference fields. Other fields (e.g., "positivePrefixPatterns") + // are not cloned since they are read-only constants after initialization. other.compactPatterns = compactPatterns.clone(); other.symbols = (DecimalFormatSymbols) symbols.clone(); + other.decimalFormat = (DecimalFormat) decimalFormat.clone(); + other.defaultDecimalFormat = (DecimalFormat) defaultDecimalFormat.clone(); + other.digitList = (DigitList) digitList.clone(); return other; } diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index 7ace5e136fe..c803a97ad86 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -2335,9 +2335,10 @@ public class DecimalFormat extends NumberFormat { // (bug 4162852). if (multiplier != 1 && gotDouble) { longResult = (long)doubleResult; - gotDouble = ((doubleResult != (double)longResult) || - (doubleResult == 0.0 && 1/doubleResult < 0.0)) && - !isParseIntegerOnly(); + gotDouble = ((doubleResult >= Long.MAX_VALUE || doubleResult <= Long.MIN_VALUE) || + (doubleResult != (double)longResult) || + (doubleResult == 0.0 && 1/doubleResult < 0.0)) && + !isParseIntegerOnly(); } // cast inside of ?: because of binary numeric promotion, JLS 15.25 diff --git a/src/java.base/share/classes/java/text/DigitList.java b/src/java.base/share/classes/java/text/DigitList.java index 2895126f93b..9ad1203fb62 100644 --- a/src/java.base/share/classes/java/text/DigitList.java +++ b/src/java.base/share/classes/java/text/DigitList.java @@ -46,6 +46,7 @@ import java.nio.charset.StandardCharsets; import jdk.internal.access.SharedSecrets; import jdk.internal.math.FloatingDecimal; import jdk.internal.util.ArraysSupport; +import jdk.internal.vm.annotation.Stable; /** * Digit List. Private to DecimalFormat. @@ -108,7 +109,6 @@ final class DigitList implements Cloneable { public int count = 0; public byte[] digits = new byte[MAX_COUNT]; - private byte[] data; private RoundingMode roundingMode = RoundingMode.HALF_EVEN; private boolean isNegative = false; @@ -320,15 +320,18 @@ final class DigitList implements Cloneable { * fractional digits to be converted. If false, total digits. */ void set(boolean isNegative, double source, int maximumDigits, boolean fixedPoint) { + assert Double.isFinite(source); + FloatingDecimal.BinaryToASCIIConverter fdConverter = FloatingDecimal.getBinaryToASCIIConverter(source, COMPAT); boolean hasBeenRoundedUp = fdConverter.digitsRoundedUp(); boolean valueExactAsDecimal = fdConverter.decimalDigitsExact(); - assert !fdConverter.isExceptional(); - byte[] chars = getDataChars(26); - int len = fdConverter.getChars(chars); - set(isNegative, chars, len, + count = fdConverter.getDigits(digits); + + int exp = fdConverter.getDecimalExponent() - count; + + set(isNegative, exp, hasBeenRoundedUp, valueExactAsDecimal, maximumDigits, fixedPoint); } @@ -340,43 +343,22 @@ final class DigitList implements Cloneable { * @param valueExactAsDecimal whether or not collected digits provide * an exact decimal representation of the value. */ - private void set(boolean isNegative, byte[] source, int len, + private void set(boolean isNegative, int exp, boolean roundedUp, boolean valueExactAsDecimal, int maximumDigits, boolean fixedPoint) { this.isNegative = isNegative; - decimalAt = -1; - count = 0; - int exponent = 0; - // Number of zeros between decimal point and first non-zero digit after - // decimal point, for numbers < 1. - int leadingZerosAfterDecimal = 0; - boolean nonZeroDigitSeen = false; + if (!nonZeroAfterIndex(0)) { + count = 0; + decimalAt = 0; + return; + } + decimalAt = count + exp; - for (int i = 0; i < len; ) { - byte c = source[i++]; - if (c == '.') { - decimalAt = count; - } else if (c == 'e' || c == 'E') { - exponent = parseInt(source, i, len); - break; - } else { - if (!nonZeroDigitSeen) { - nonZeroDigitSeen = (c != '0'); - if (!nonZeroDigitSeen && decimalAt != -1) - ++leadingZerosAfterDecimal; - } - if (nonZeroDigitSeen) { - digits[count++] = c; - } - } - } - if (decimalAt == -1) { - decimalAt = count; - } - if (nonZeroDigitSeen) { - decimalAt += exponent - leadingZerosAfterDecimal; + // Eliminate trailing zeros. + while (count > 1 && digits[count - 1] == '0') { + --count; } if (fixedPoint) { @@ -405,11 +387,6 @@ final class DigitList implements Cloneable { // else fall through } - // Eliminate trailing zeros. - while (count > 1 && digits[count - 1] == '0') { - --count; - } - // Eliminate digits beyond maximum digits to be displayed. // Round up if appropriate. round(fixedPoint ? (maximumDigits + decimalAt) : maximumDigits, @@ -669,13 +646,13 @@ final class DigitList implements Cloneable { */ @SuppressWarnings("deprecation") void set(boolean isNegative, BigDecimal source, int maximumDigits, boolean fixedPoint) { - String s = source.toString(); - extendDigits(s.length()); - + String s = source.unscaledValue().toString(); int len = s.length(); - byte[] chars = getDataChars(len); - s.getBytes(0, len, chars, 0); - set(isNegative, chars, len, + + extendDigits(len); + s.getBytes(0, len, digits, 0); + count = len; + set(isNegative, -source.scale(), false, true, maximumDigits, fixedPoint); } @@ -745,14 +722,7 @@ final class DigitList implements Cloneable { public Object clone() { try { DigitList other = (DigitList) super.clone(); - byte[] newDigits = new byte[digits.length]; - System.arraycopy(digits, 0, newDigits, 0, digits.length); - other.digits = newDigits; - - // Data does not need to be copied because it does - // not carry significant information. It will be recreated on demand. - // Setting it to null is needed to avoid sharing across clones. - other.data = null; + other.digits = digits.clone(); return other; } catch (CloneNotSupportedException e) { @@ -760,29 +730,8 @@ final class DigitList implements Cloneable { } } - private static int parseInt(byte[] str, int offset, int strLen) { - byte c; - boolean positive = true; - if ((c = str[offset]) == '-') { - positive = false; - offset++; - } else if (c == '+') { - offset++; - } - - int value = 0; - while (offset < strLen) { - c = str[offset++]; - if (c >= '0' && c <= '9') { - value = value * 10 + (c - '0'); - } else { - break; - } - } - return positive ? value : -value; - } - // The digit part of -9223372036854775808L + @Stable private static final byte[] LONG_MIN_REP = "9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); public String toString() { @@ -798,11 +747,4 @@ final class DigitList implements Cloneable { digits = new byte[len]; } } - - private byte[] getDataChars(int length) { - if (data == null || data.length < length) { - data = new byte[length]; - } - return data; - } } diff --git a/src/java.base/share/classes/java/time/Duration.java b/src/java.base/share/classes/java/time/Duration.java index 88d49fa9e45..7b3289a1f59 100644 --- a/src/java.base/share/classes/java/time/Duration.java +++ b/src/java.base/share/classes/java/time/Duration.java @@ -138,6 +138,37 @@ public final class Duration * Constant for a duration of zero. */ public static final Duration ZERO = new Duration(0, 0); + /** + * The minimum supported {@code Duration}, which is {@link Long#MIN_VALUE} + * seconds. + * + * @apiNote This constant represents the smallest possible instance of + * {@code Duration}. Since {@code Duration} is directed, the smallest + * possible duration is negative. + * + * The constant is intended to be used as a sentinel value or in tests. + * Care should be taken when performing arithmetic on {@code MIN} as there + * is a high risk that {@link ArithmeticException} or {@link DateTimeException} + * will be thrown. + * + * @since 26 + */ + public static final Duration MIN = new Duration(Long.MIN_VALUE, 0); + /** + * The maximum supported {@code Duration}, which is {@link Long#MAX_VALUE} + * seconds and {@code 999,999,999} nanoseconds. + * + * @apiNote This constant represents the largest possible instance of + * {@code Duration}. + * + * The constant is intended to be used as a sentinel value or in tests. + * Care should be taken when performing arithmetic on {@code MAX} as there + * is a high risk that {@link ArithmeticException} or {@link DateTimeException} + * will be thrown. + * + * @since 26 + */ + public static final Duration MAX = new Duration(Long.MAX_VALUE, 999_999_999); /** * Serialization version. */ @@ -172,7 +203,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard 24 hour days. *

    * The seconds are calculated based on the standard definition of a day, - * where each day is 86400 seconds which implies a 24 hour day. + * where each day is 86,400 seconds which implies a 24 hour day. * The nanosecond in second field is set to zero. * * @param days the number of days, positive or negative @@ -187,7 +218,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard hours. *

    * The seconds are calculated based on the standard definition of an hour, - * where each hour is 3600 seconds. + * where each hour is 3,600 seconds. * The nanosecond in second field is set to zero. * * @param hours the number of hours, positive or negative @@ -375,8 +406,8 @@ public final class Duration *

          *    "PT20.345S" -- parses as "20.345 seconds"
          *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
    -     *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
    -     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
    +     *    "PT10H"     -- parses as "10 hours" (where an hour is 3,600 seconds)
    +     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86,400 seconds)
          *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
          *    "PT-6H3M"    -- parses as "-6 hours and +3 minutes"
          *    "-PT6H3M"    -- parses as "-6 hours and -3 minutes"
    @@ -477,7 +508,7 @@ public final class Duration
          * {@link ChronoField#NANO_OF_SECOND NANO_OF_SECOND} field should be supported.
          * 

    * The result of this method can be a negative duration if the end is before the start. - * To guarantee to obtain a positive duration call {@link #abs()} on the result. + * To guarantee a positive duration, call {@link #abs()} on the result. * * @param startInclusive the start instant, inclusive, not null * @param endExclusive the end instant, exclusive, not null @@ -752,7 +783,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days added. *

    - * The number of days is multiplied by 86400 to obtain the number of seconds to add. + * The number of days is multiplied by 86,400 to obtain the number of seconds to add. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -893,7 +924,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days subtracted. *

    - * The number of days is multiplied by 86400 to obtain the number of seconds to subtract. + * The number of days is multiplied by 86,400 to obtain the number of seconds to subtract. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -909,7 +940,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in hours subtracted. *

    - * The number of hours is multiplied by 3600 to obtain the number of seconds to subtract. + * The number of hours is multiplied by 3,600 to obtain the number of seconds to subtract. *

    * This instance is immutable and unaffected by this method call. * @@ -924,7 +955,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in minutes subtracted. *

    - * The number of hours is multiplied by 60 to obtain the number of seconds to subtract. + * The number of minutes is multiplied by 60 to obtain the number of seconds to subtract. *

    * This instance is immutable and unaffected by this method call. * @@ -1165,7 +1196,7 @@ public final class Duration * Gets the number of days in this duration. *

    * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -1180,7 +1211,7 @@ public final class Duration * Gets the number of hours in this duration. *

    * This returns the total number of hours in the duration by dividing the - * number of seconds by 3600. + * number of seconds by 3,600. *

    * This instance is immutable and unaffected by this method call. * @@ -1272,7 +1303,7 @@ public final class Duration * Extracts the number of days in the duration. *

    * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

    * This instance is immutable and unaffected by this method call. @@ -1476,10 +1507,10 @@ public final class Duration *

    * Examples: *

    -     *    "20.345 seconds"                 -- "PT20.345S
    +     *    "20.345 seconds"                 -- "PT20.345S"
          *    "15 minutes" (15 * 60 seconds)   -- "PT15M"
    -     *    "10 hours" (10 * 3600 seconds)   -- "PT10H"
    -     *    "2 days" (2 * 86400 seconds)     -- "PT48H"
    +     *    "10 hours" (10 * 3,600 seconds)  -- "PT10H"
    +     *    "2 days" (2 * 86,400 seconds)    -- "PT48H"
          * 
    * Note that multiples of 24 hours are not output as days to avoid confusion * with {@code Period}. diff --git a/src/java.base/share/classes/java/time/Instant.java b/src/java.base/share/classes/java/time/Instant.java index 1c7a41d7f0e..640743f0fb7 100644 --- a/src/java.base/share/classes/java/time/Instant.java +++ b/src/java.base/share/classes/java/time/Instant.java @@ -114,15 +114,15 @@ import java.util.Objects; *

    * The length of the solar day is the standard way that humans measure time. * This has traditionally been subdivided into 24 hours of 60 minutes of 60 seconds, - * forming a 86400 second day. + * forming an 86,400 second day. *

    * Modern timekeeping is based on atomic clocks which precisely define an SI second * relative to the transitions of a Caesium atom. The length of an SI second was defined - * to be very close to the 86400th fraction of a day. + * to be very close to the 86,400th fraction of a day. *

    * Unfortunately, as the Earth rotates the length of the day varies. * In addition, over time the average length of the day is getting longer as the Earth slows. - * As a result, the length of a solar day in 2012 is slightly longer than 86400 SI seconds. + * As a result, the length of a solar day in 2012 is slightly longer than 86,400 SI seconds. * The actual length of any given day and the amount by which the Earth is slowing * are not predictable and can only be determined by measurement. * The UT1 time-scale captures the accurate length of day, but is only available some @@ -131,7 +131,7 @@ import java.util.Objects; * The UTC time-scale is a standard approach to bundle up all the additional fractions * of a second from UT1 into whole seconds, known as leap-seconds. * A leap-second may be added or removed depending on the Earth's rotational changes. - * As such, UTC permits a day to have 86399 SI seconds or 86401 SI seconds where + * As such, UTC permits a day to have 86,399 SI seconds or 86,401 SI seconds where * necessary in order to keep the day aligned with the Sun. *

    * The modern UTC time-scale was introduced in 1972, introducing the concept of whole leap-seconds. @@ -143,7 +143,7 @@ import java.util.Objects; * Given the complexity of accurate timekeeping described above, this Java API defines * its own time-scale, the Java Time-Scale. *

    - * The Java Time-Scale divides each calendar day into exactly 86400 + * The Java Time-Scale divides each calendar day into exactly 86,400 * subdivisions, known as seconds. These seconds may differ from the * SI second. It closely matches the de facto international civil time * scale, the definition of which changes from time to time. @@ -171,7 +171,7 @@ import java.util.Objects; * This is identical to UTC on days that do not have a leap second. * On days that do have a leap second, the leap second is spread equally * over the last 1000 seconds of the day, maintaining the appearance of - * exactly 86400 seconds per day. + * exactly 86,400 seconds per day. *

    * For the segment prior to 1972-11-03, extending back arbitrarily far, * the consensus international time scale is defined to be UT1, applied @@ -788,6 +788,32 @@ public final class Instant return (Instant) amountToAdd.addTo(this); } + /** + * Returns a copy of this instant with the specified duration added, with + * saturated semantics. + *

    + * If the result is "earlier" than {@link Instant#MIN}, this method returns + * {@code MIN}. If the result is "later" than {@link Instant#MAX}, it + * returns {@code MAX}. Otherwise it returns {@link #plus(TemporalAmount) plus(duration)}. + * + * @apiNote This method can be used to calculate a deadline from + * this instant and a timeout. Unlike {@code plus(duration)}, + * this method never throws {@link ArithmeticException} or {@link DateTimeException} + * due to numeric overflow or {@code Instant} range violation. + * + * @param duration the duration to add, not null + * @return an {@code Instant} based on this instant with the addition made, not null + * + * @since 26 + */ + public Instant plusSaturating(Duration duration) { + if (duration.isNegative()) { + return until(Instant.MIN).compareTo(duration) >= 0 ? Instant.MIN : plus(duration); + } else { + return until(Instant.MAX).compareTo(duration) <= 0 ? Instant.MAX : plus(duration); + } + } + /** * Returns a copy of this instant with the specified amount added. *

    diff --git a/src/java.base/share/classes/java/time/LocalDate.java b/src/java.base/share/classes/java/time/LocalDate.java index 6724410da2b..016bdab5394 100644 --- a/src/java.base/share/classes/java/time/LocalDate.java +++ b/src/java.base/share/classes/java/time/LocalDate.java @@ -182,11 +182,11 @@ public final class LocalDate /** * @serial The month-of-year. */ - private final byte month; + private final short month; /** * @serial The day-of-month. */ - private final byte day; + private final short day; //----------------------------------------------------------------------- /** @@ -490,8 +490,8 @@ public final class LocalDate */ private LocalDate(int year, int month, int dayOfMonth) { this.year = year; - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = (short) month; + this.day = (short) dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/MonthDay.java b/src/java.base/share/classes/java/time/MonthDay.java index 6244c14e6e1..1de4fa84d3e 100644 --- a/src/java.base/share/classes/java/time/MonthDay.java +++ b/src/java.base/share/classes/java/time/MonthDay.java @@ -146,11 +146,11 @@ public final class MonthDay /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; /** * @serial The day-of-month. */ - private final byte day; + private final int day; //----------------------------------------------------------------------- /** @@ -319,8 +319,8 @@ public final class MonthDay * @param dayOfMonth the day-of-month to represent, validated from 1 to 29-31 */ private MonthDay(int month, int dayOfMonth) { - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = month; + this.day = dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/Period.java b/src/java.base/share/classes/java/time/Period.java index 5ee80710edb..745714788c3 100644 --- a/src/java.base/share/classes/java/time/Period.java +++ b/src/java.base/share/classes/java/time/Period.java @@ -765,7 +765,7 @@ public final class Period *

    * This instance is immutable and unaffected by this method call. * - * @param daysToSubtract the months to subtract, positive or negative + * @param daysToSubtract the days to subtract, positive or negative * @return a {@code Period} based on this period with the specified days subtracted, not null * @throws ArithmeticException if numeric overflow occurs */ diff --git a/src/java.base/share/classes/java/time/YearMonth.java b/src/java.base/share/classes/java/time/YearMonth.java index b24151de3f0..8ad1172811f 100644 --- a/src/java.base/share/classes/java/time/YearMonth.java +++ b/src/java.base/share/classes/java/time/YearMonth.java @@ -153,7 +153,7 @@ public final class YearMonth /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; //----------------------------------------------------------------------- /** @@ -306,7 +306,7 @@ public final class YearMonth */ private YearMonth(int year, int month) { this.year = year; - this.month = (byte) month; + this.month = month; } /** diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index d93c6e2d46d..4199d17735c 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -417,9 +417,9 @@ public final class ZoneOffset /** * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds *

    - * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800. + * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64,800 to +64,800. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 * @return the ZoneOffset, not null * @throws DateTimeException if the offset is not in the required range */ @@ -450,7 +450,7 @@ public final class ZoneOffset /** * Constructor. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 */ private ZoneOffset(int totalSeconds) { this.totalSeconds = totalSeconds; diff --git a/src/java.base/share/classes/java/time/ZonedDateTime.java b/src/java.base/share/classes/java/time/ZonedDateTime.java index 962469b3225..57dc98d5c68 100644 --- a/src/java.base/share/classes/java/time/ZonedDateTime.java +++ b/src/java.base/share/classes/java/time/ZonedDateTime.java @@ -136,7 +136,7 @@ import jdk.internal.util.DateTimeHelper; * For Overlaps, the general strategy is that if the local date-time falls in the * middle of an Overlap, then the previous offset will be retained. If there is no * previous offset, or the previous offset is invalid, then the earlier offset is - * used, typically "summer" time.. Two additional methods, + * used, typically "summer" time. Two additional methods, * {@link #withEarlierOffsetAtOverlap()} and {@link #withLaterOffsetAtOverlap()}, * help manage the case of an overlap. *

    @@ -246,7 +246,7 @@ public final class ZonedDateTime * Time-zone rules, such as daylight savings, mean that not every local date-time * is valid for the specified zone, thus the local date-time may be adjusted. *

    - * The local date time and first combined to form a local date-time. + * The local date and time are first combined to form a local date-time. * The local date-time is then resolved to a single instant on the time-line. * This is achieved by finding a valid offset from UTC/Greenwich for the local * date-time as defined by the {@link ZoneRules rules} of the zone ID. @@ -263,7 +263,7 @@ public final class ZonedDateTime * @param date the local date, not null * @param time the local time, not null * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null */ public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { return of(LocalDateTime.of(date, time), zone); @@ -333,7 +333,7 @@ public final class ZonedDateTime * @param second the second-of-minute to represent, from 0 to 59 * @param nanoOfSecond the nano-of-second to represent, from 0 to 999,999,999 * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null * @throws DateTimeException if the value of any field is out of range, or * if the day-of-month is invalid for the month-year */ diff --git a/src/java.base/share/classes/java/time/chrono/HijrahDate.java b/src/java.base/share/classes/java/time/chrono/HijrahDate.java index 2d3e4f93e69..114a47e4797 100644 --- a/src/java.base/share/classes/java/time/chrono/HijrahDate.java +++ b/src/java.base/share/classes/java/time/chrono/HijrahDate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2019, 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 @@ -137,11 +137,11 @@ public final class HijrahDate /** * The month-of-year. */ - private final transient byte monthOfYear; + private final transient int monthOfYear; /** * The day-of-month. */ - private final transient byte dayOfMonth; + private final transient int dayOfMonth; //------------------------------------------------------------------------- /** @@ -273,8 +273,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = prolepticYear; - this.monthOfYear = (byte) monthOfYear; - this.dayOfMonth = (byte) dayOfMonth; + this.monthOfYear = monthOfYear; + this.dayOfMonth = dayOfMonth; } /** @@ -287,8 +287,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = dateInfo[0]; - this.monthOfYear = (byte) dateInfo[1]; - this.dayOfMonth = (byte) dateInfo[2]; + this.monthOfYear = dateInfo[1]; + this.dayOfMonth = dateInfo[2]; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java index 26192b8e178..16d7193c556 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -1904,11 +1904,11 @@ public final class DateTimeFormatter { try { DateTimePrintContext context = new DateTimePrintContext(temporal, this); if (appendable instanceof StringBuilder) { - printerParser.format(context, (StringBuilder) appendable); + printerParser.format(context, (StringBuilder) appendable, false); } else { // buffer output to avoid writing to appendable in case of error StringBuilder buf = new StringBuilder(32); - printerParser.format(context, buf); + printerParser.format(context, buf, false); appendable.append(buf); } } catch (IOException ex) { diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 7a2142e5113..4708094effb 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Alibaba Group Holding Limited. 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 @@ -1937,7 +1938,7 @@ public final class DateTimeFormatterBuilder { padNext(pad); // pad and continue parsing } // main rules - TemporalField field = FIELD_MAP.get(cur); + TemporalField field = getField(cur); if (field != null) { parseField(cur, count, field); } else if (cur == 'z') { @@ -2185,48 +2186,55 @@ public final class DateTimeFormatterBuilder { } } - /** Map of letters to fields. */ - private static final Map FIELD_MAP = new HashMap<>(); - static { + /** + * Returns the TemporalField for the given pattern character. + * + * @param ch the pattern character + * @return the TemporalField for the given pattern character, or null if not applicable + */ + private static TemporalField getField(char ch) { // SDF = SimpleDateFormat - FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML - FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF) - FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310) - FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone) - FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML - FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone) - FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML - FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML - FIELD_MAP.put('F', ChronoField.ALIGNED_WEEK_OF_MONTH); // SDF, LDML - FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone) - FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number) - FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML - FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML - FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML - FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number) - FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML - FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML) - FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML) - FIELD_MAP.put('g', JulianFields.MODIFIED_JULIAN_DAY); - // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 - // 310 - Z - matches SimpleDateFormat and LDML - // 310 - V - time-zone id, matches LDML - // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back - // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data - // 310 - p - prefix for padding - // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 - // 310 - x - matches LDML - // 310 - w, W, and Y are localized forms matching LDML - // LDML - B - day periods - // LDML - U - cycle year name, not supported by 310 yet - // LDML - l - deprecated - // LDML - j - not relevant + return switch (ch) { + case 'G' -> ChronoField.ERA; // SDF, LDML (different to both for 1/2 chars) + case 'y' -> ChronoField.YEAR_OF_ERA; // SDF, LDML + case 'u' -> ChronoField.YEAR; // LDML (different in SDF) + case 'Q' -> IsoFields.QUARTER_OF_YEAR; // LDML (removed quarter from 310) + case 'q' -> IsoFields.QUARTER_OF_YEAR; // LDML (stand-alone) + case 'M' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML + case 'L' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML (stand-alone) + case 'D' -> ChronoField.DAY_OF_YEAR; // SDF, LDML + case 'd' -> ChronoField.DAY_OF_MONTH; // SDF, LDML + case 'F' -> ChronoField.ALIGNED_WEEK_OF_MONTH; // SDF, LDML + case 'E' -> ChronoField.DAY_OF_WEEK; // SDF, LDML (different to both for 1/2 chars) + case 'c' -> ChronoField.DAY_OF_WEEK; // LDML (stand-alone) + case 'e' -> ChronoField.DAY_OF_WEEK; // LDML (needs localized week number) + case 'a' -> ChronoField.AMPM_OF_DAY; // SDF, LDML + case 'H' -> ChronoField.HOUR_OF_DAY; // SDF, LDML + case 'k' -> ChronoField.CLOCK_HOUR_OF_DAY; // SDF, LDML + case 'K' -> ChronoField.HOUR_OF_AMPM; // SDF, LDML + case 'h' -> ChronoField.CLOCK_HOUR_OF_AMPM; // SDF, LDML + case 'm' -> ChronoField.MINUTE_OF_HOUR; // SDF, LDML + case 's' -> ChronoField.SECOND_OF_MINUTE; // SDF, LDML + case 'S' -> ChronoField.NANO_OF_SECOND; // LDML (SDF uses milli-of-second number) + case 'A' -> ChronoField.MILLI_OF_DAY; // LDML + case 'n' -> ChronoField.NANO_OF_SECOND; // 310 (proposed for LDML) + case 'N' -> ChronoField.NANO_OF_DAY; // 310 (proposed for LDML) + case 'g' -> JulianFields.MODIFIED_JULIAN_DAY; + default -> null; + // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 + // 310 - Z - matches SimpleDateFormat and LDML + // 310 - V - time-zone id, matches LDML + // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back + // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data + // 310 - p - prefix for padding + // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 + // 310 - x - matches LDML + // 310 - w, W, and Y are localized forms matching LDML + // LDML - B - day periods + // LDML - U - cycle year name, not supported by 310 yet + // LDML - l - deprecated + // LDML - j - not relevant + }; } //----------------------------------------------------------------------- @@ -2477,10 +2485,15 @@ public final class DateTimeFormatterBuilder { * * @param context the context to format using, not null * @param buf the buffer to append to, not null + * @param optional whether the enclosing formatter is optional. + * If true and this formatter is nested in an optional formatter + * and the data is not available, then no error is returned and + * nothing is appended to the buffer. If false and the data is not available + * then an exception is thrown or false is returned as appropriate. * @return false if unable to query the value from the date-time, true otherwise * @throws DateTimeException if the date-time cannot be printed successfully */ - boolean format(DateTimePrintContext context, StringBuilder buf); + boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional); /** * Parses text into date-time information. @@ -2530,21 +2543,13 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { int length = buf.length(); - if (optional) { - context.startOptional(); - } - try { - for (DateTimePrinterParser pp : printerParsers) { - if (pp.format(context, buf) == false) { - buf.setLength(length); // reset buffer - return true; - } - } - } finally { - if (optional) { - context.endOptional(); + boolean effectiveOptional = optional | this.optional; + for (DateTimePrinterParser pp : printerParsers) { + if (!pp.format(context, buf, effectiveOptional)) { + buf.setLength(length); // reset buffer + return true; } } return true; @@ -2613,9 +2618,9 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { int preLen = buf.length(); - if (printerParser.format(context, buf) == false) { + if (printerParser.format(context, buf, optional) == false) { return false; } int len = buf.length() - preLen; @@ -2682,7 +2687,7 @@ public final class DateTimeFormatterBuilder { LENIENT; @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { return true; // nothing to do here } @@ -2724,7 +2729,7 @@ public final class DateTimeFormatterBuilder { this.value = value; } - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { return true; } @@ -2750,7 +2755,7 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { buf.append(literal); return true; } @@ -2800,7 +2805,7 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { buf.append(literal); return true; } @@ -2912,8 +2917,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long valueLong = context.getValue(field); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long valueLong = context.getValue(field, optional); if (valueLong == null) { return false; } @@ -3367,8 +3372,8 @@ public final class DateTimeFormatterBuilder { }; @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long value = context.getValue(field); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long value = context.getValue(field, optional); if (value == null) { return false; } @@ -3556,8 +3561,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long value = context.getValue(field); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long value = context.getValue(field, optional); if (value == null) { return false; } @@ -3704,8 +3709,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long value = context.getValue(field); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long value = context.getValue(field, optional); if (value == null) { return false; } @@ -3717,7 +3722,7 @@ public final class DateTimeFormatterBuilder { text = provider.getText(chrono, field, value, textStyle, context.getLocale()); } if (text == null) { - return numberPrinterParser().format(context, buf); + return numberPrinterParser().format(context, buf, optional); } buf.append(text); return true; @@ -3799,9 +3804,9 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { // use INSTANT_SECONDS, thus this code is not bound by Instant.MAX - Long inSecs = context.getValue(INSTANT_SECONDS); + Long inSecs = context.getValue(INSTANT_SECONDS, optional); Long inNanos = null; if (context.getTemporal().isSupported(NANO_OF_SECOND)) { inNanos = context.getTemporal().getLong(NANO_OF_SECOND); @@ -3985,8 +3990,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long offsetSecs = context.getValue(OFFSET_SECONDS); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long offsetSecs = context.getValue(OFFSET_SECONDS, optional); if (offsetSecs == null) { return false; } @@ -4284,8 +4289,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long offsetSecs = context.getValue(OFFSET_SECONDS); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long offsetSecs = context.getValue(OFFSET_SECONDS, optional); if (offsetSecs == null) { return false; } @@ -4498,8 +4503,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - ZoneId zone = context.getValue(TemporalQueries.zoneId()); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + ZoneId zone = context.getValue(TemporalQueries.zoneId(), optional); if (zone == null) { return false; } @@ -4620,8 +4625,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - ZoneId zone = context.getValue(query); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + ZoneId zone = context.getValue(query, optional); if (zone == null) { return false; } @@ -5045,8 +5050,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Chronology chrono = context.getValue(TemporalQueries.chronology()); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Chronology chrono = context.getValue(TemporalQueries.chronology(), optional); if (chrono == null) { return false; } @@ -5142,9 +5147,9 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { Chronology chrono = Chronology.from(context.getTemporal()); - return formatter(context.getLocale(), chrono).toPrinterParser(false).format(context, buf); + return formatter(context.getLocale(), chrono).toPrinterParser(false).format(context, buf, optional); } @Override @@ -5253,8 +5258,8 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - return printerParser(context.getLocale()).format(context, buf); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + return printerParser(context.getLocale()).format(context, buf, optional); } @Override @@ -5357,12 +5362,12 @@ public final class DateTimeFormatterBuilder { } @Override - public boolean format(DateTimePrintContext context, StringBuilder buf) { - Long hod = context.getValue(HOUR_OF_DAY); + public boolean format(DateTimePrintContext context, StringBuilder buf, boolean optional) { + Long hod = context.getValue(HOUR_OF_DAY, optional); if (hod == null) { return false; } - Long moh = context.getValue(MINUTE_OF_HOUR); + Long moh = context.getValue(MINUTE_OF_HOUR, optional); long value = Math.floorMod(hod, 24) * 60 + (moh != null ? Math.floorMod(moh, 60) : 0); Locale locale = context.getLocale(); LocaleStore store = findDayPeriodStore(locale); diff --git a/src/java.base/share/classes/java/time/format/DateTimePrintContext.java b/src/java.base/share/classes/java/time/format/DateTimePrintContext.java index d755ba3ee78..051796b6a9c 100644 --- a/src/java.base/share/classes/java/time/format/DateTimePrintContext.java +++ b/src/java.base/share/classes/java/time/format/DateTimePrintContext.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Alibaba Group Holding Limited. 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 @@ -86,11 +87,6 @@ import java.util.Objects; *

    * This class provides a single wrapper to items used in the format. * - * @implSpec - * This class is a mutable context intended for use from a single thread. - * Usage of the class is thread-safe within standard printing as the framework creates - * a new instance of the class for each format and printing is single-threaded. - * * @since 1.8 */ final class DateTimePrintContext { @@ -103,10 +99,6 @@ final class DateTimePrintContext { * The formatter, not null. */ private final DateTimeFormatter formatter; - /** - * Whether the current formatter is optional. - */ - private int optional; /** * Creates a new instance of the context. @@ -115,7 +107,6 @@ final class DateTimePrintContext { * @param formatter the formatter controlling the format, not null */ DateTimePrintContext(TemporalAccessor temporal, DateTimeFormatter formatter) { - super(); this.temporal = adjust(temporal, formatter); this.formatter = formatter; } @@ -348,30 +339,17 @@ final class DateTimePrintContext { } //----------------------------------------------------------------------- - /** - * Starts the printing of an optional segment of the input. - */ - void startOptional() { - this.optional++; - } - - /** - * Ends the printing of an optional segment of the input. - */ - void endOptional() { - this.optional--; - } - /** * Gets a value using a query. * * @param query the query to use, not null + * @param optional whether the query is optional, true if the query may be missing * @return the result, null if not found and optional is true * @throws DateTimeException if the type is not available and the section is not optional */ - R getValue(TemporalQuery query) { + R getValue(TemporalQuery query, boolean optional) { R result = temporal.query(query); - if (result == null && optional == 0) { + if (result == null && !optional) { throw new DateTimeException("Unable to extract " + query + " from temporal " + temporal); } @@ -384,11 +362,12 @@ final class DateTimePrintContext { * This will return the value for the specified field. * * @param field the field to find, not null + * @param optional whether the field is optional, true if the field may be missing * @return the value, null if not found and optional is true * @throws DateTimeException if the field is not available and the section is not optional */ - Long getValue(TemporalField field) { - if (optional > 0 && !temporal.isSupported(field)) { + Long getValue(TemporalField field, boolean optional) { + if (optional && !temporal.isSupported(field)) { return null; } return temporal.getLong(field); diff --git a/src/java.base/share/classes/java/time/package-info.java b/src/java.base/share/classes/java/time/package-info.java index 8a4fbe44d8b..d0fb7a6c2bc 100644 --- a/src/java.base/share/classes/java/time/package-info.java +++ b/src/java.base/share/classes/java/time/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -65,7 +65,7 @@ * The main API for dates, times, instants, and durations. *

    *

    - * The classes defined here represent the principle date-time concepts, + * The classes defined here represent the principal date-time concepts, * including instants, durations, dates, times, time-zones and periods. * They are based on the ISO calendar system, which is the de facto world * calendar following the proleptic Gregorian rules. @@ -150,7 +150,7 @@ *

    *

    * {@link java.time.OffsetTime} stores a time and offset from UTC without a date. - * This stores a date like '11:30+01:00'. + * This stores a time like '11:30+01:00'. * The {@link java.time.ZoneOffset ZoneOffset} is of the form '+01:00'. *

    *

    @@ -249,7 +249,7 @@ *

    * Multiple calendar systems is an awkward addition to the design challenges. * The first principle is that most users want the standard ISO calendar system. - * As such, the main classes are ISO-only. The second principle is that most of those that want a + * As such, the main classes are ISO-only. The second principle is that most of those who want a * non-ISO calendar system want it for user interaction, thus it is a UI localization issue. * As such, date and time objects should be held as ISO objects in the data model and persistent * storage, only being converted to and from a local calendar for display. diff --git a/src/java.base/share/classes/java/time/temporal/ChronoField.java b/src/java.base/share/classes/java/time/temporal/ChronoField.java index 9e5de367b0e..506737ff268 100644 --- a/src/java.base/share/classes/java/time/temporal/ChronoField.java +++ b/src/java.base/share/classes/java/time/temporal/ChronoField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -599,7 +599,7 @@ public enum ChronoField implements TemporalField { * A {@link ZoneOffset} represents the period of time that local time differs from UTC/Greenwich. * This is usually a fixed number of hours and minutes. * It is equivalent to the {@link ZoneOffset#getTotalSeconds() total amount} of the offset in seconds. - * For example, during the winter Paris has an offset of {@code +01:00}, which is 3600 seconds. + * For example, during the winter, Paris has an offset of {@code +01:00}, which is 3,600 seconds. *

    * This field is strictly defined to have the same meaning in all calendar systems. * This is necessary to ensure interoperation between calendars. diff --git a/src/java.base/share/classes/java/time/temporal/ChronoUnit.java b/src/java.base/share/classes/java/time/temporal/ChronoUnit.java index 8f94e061d4d..6e944b296da 100644 --- a/src/java.base/share/classes/java/time/temporal/ChronoUnit.java +++ b/src/java.base/share/classes/java/time/temporal/ChronoUnit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -184,10 +184,9 @@ public enum ChronoUnit implements TemporalUnit { * Artificial unit that represents the concept of forever. * This is primarily used with {@link TemporalField} to represent unbounded fields * such as the year or era. - * The estimated duration of this unit is artificially defined as the largest duration - * supported by {@link Duration}. + * The estimated duration of this unit is artificially defined as {@link Duration#MAX}. */ - FOREVER("Forever", Duration.ofSeconds(Long.MAX_VALUE, 999_999_999)); + FOREVER("Forever", Duration.MAX); private final String name; private final Duration duration; diff --git a/src/java.base/share/classes/java/time/temporal/ValueRange.java b/src/java.base/share/classes/java/time/temporal/ValueRange.java index 442cf0a2509..27e71e77d66 100644 --- a/src/java.base/share/classes/java/time/temporal/ValueRange.java +++ b/src/java.base/share/classes/java/time/temporal/ValueRange.java @@ -78,7 +78,7 @@ import java.time.DateTimeException; * Only the minimum and maximum values are provided. * It is possible for there to be invalid values within the outer range. * For example, a weird field may have valid values of 1, 2, 4, 6, 7, thus - * have a range of '1 - 7', despite that fact that values 3 and 5 are invalid. + * have a range of '1 - 7', despite the fact that values 3 and 5 are invalid. *

    * Instances of this class are not tied to a specific field. * diff --git a/src/java.base/share/classes/java/util/Comparator.java b/src/java.base/share/classes/java/util/Comparator.java index 6e0420d26e8..ad48dc94ed6 100644 --- a/src/java.base/share/classes/java/util/Comparator.java +++ b/src/java.base/share/classes/java/util/Comparator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -30,7 +30,6 @@ import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; import java.util.function.ToDoubleFunction; -import java.util.Comparators; /** * A comparison function, which imposes a total ordering on @@ -189,6 +188,52 @@ public interface Comparator { return Collections.reverseOrder(this); } + /** + * Returns the greater of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) >= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the larger of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U max(U o1, U o2) { + return compare(o1, o2) >= 0 ? o1 : o2; + } + + /** + * Returns the smaller of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) <= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the smaller of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U min(U o1, U o2) { + return compare(o1, o2) <= 0 ? o1 : o2; + } + /** * Returns a lexicographic-order comparator with another comparator. * If this {@code Comparator} considers two elements equal, i.e. diff --git a/src/java.base/share/classes/java/util/GregorianCalendar.java b/src/java.base/share/classes/java/util/GregorianCalendar.java index 9c75cbc5732..26b0c7c1f2f 100644 --- a/src/java.base/share/classes/java/util/GregorianCalendar.java +++ b/src/java.base/share/classes/java/util/GregorianCalendar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -1214,8 +1214,10 @@ public class GregorianCalendar extends Calendar { d.setHours(hourOfDay); time = calsys.getTime(d); - // If we stay on the same wall-clock time, try the next or previous hour. - if (internalGet(HOUR_OF_DAY) == d.getHours()) { + // If the rolled amount is not a full HOUR/HOUR_OF_DAY (12/24-hour) cycle and + // if we stay on the same wall-clock time, try the next or previous hour. + if (((field == HOUR_OF_DAY && amount % 24 != 0) || (field == HOUR && amount % 12 != 0)) + && internalGet(HOUR_OF_DAY) == d.getHours()) { hourOfDay = getRolledValue(rolledValue, amount > 0 ? +1 : -1, min, max); if (field == HOUR && internalGet(AM_PM) == PM) { hourOfDay += 12; diff --git a/src/java.base/share/classes/java/util/Locale.java b/src/java.base/share/classes/java/util/Locale.java index eb69d1dfc2f..452b621ea05 100644 --- a/src/java.base/share/classes/java/util/Locale.java +++ b/src/java.base/share/classes/java/util/Locale.java @@ -204,15 +204,18 @@ import sun.util.locale.provider.TimeZoneNameUtility; * key="x"/value="java-1-7" * * - * BCP 47 deviation: Although BCP 47 requires field values to be registered - * in the IANA Language Subtag Registry, the {@code Locale} class - * does not validate this requirement. For example, the variant code "foobar" - * is well-formed since it is composed of 5 to 8 alphanumerics, but is not defined - * the IANA Language Subtag Registry. The {@link Builder} - * only checks if an individual field satisfies the syntactic - * requirement (is well-formed), but does not validate the value - * itself. Conversely, {@link #of(String, String, String) Locale::of} and its - * overloads do not make any syntactic checks on the input. + * BCP 47 deviation: BCP47 defines the following two levels of + * conformance, + * "valid" and "well-formed". A valid tag requires that it is well-formed, its + * subtag values are registered in the IANA Language Subtag Registry, and it does not + * contain duplicate variant or extension singleton subtags. The {@code Locale} + * class does not enforce that subtags are registered in the Subtag Registry. + * {@link Builder} only checks if an individual field satisfies the syntactic + * requirement (is well-formed). When passed duplicate variants, {@code Builder} + * accepts and includes them. When passed duplicate extension singletons, {@code + * Builder} accepts but ignores the duplicate key and its associated value. + * Conversely, {@link #of(String, String, String) Locale::of} and its + * overloads do not check if the input is well-formed at all. * *

    Unicode BCP 47 U Extension

    * @@ -246,7 +249,11 @@ import sun.util.locale.provider.TimeZoneNameUtility; * can be empty, or a series of subtags 3-8 alphanums in length). A * well-formed locale attribute has the form * {@code [0-9a-zA-Z]{3,8}} (it is a single subtag with the same - * form as a locale type subtag). + * form as a locale type subtag). Duplicate locale attributes as well + * as locale keys do not convey meaning. For methods in {@code Locale} and + * {@code Locale.Builder} that accept extensions, occurrences of duplicate + * locale attributes as well as locale keys and their associated type are accepted + * but ignored. * *

    The Unicode locale extension specifies optional behavior in * locale-sensitive services. Although the LDML specification defines @@ -561,6 +568,8 @@ import sun.util.locale.provider.TimeZoneNameUtility; * RFC 4647: Matching of Language Tags * @spec https://www.rfc-editor.org/info/rfc5646 * RFC 5646: Tags for Identifying Languages + * @spec https://www.rfc-editor.org/info/rfc6067 + * RFC 6067: BCP 47 Extension U * @spec https://www.unicode.org/reports/tr35 * Unicode Locale Data Markup Language (LDML) * @see Builder @@ -1743,6 +1752,12 @@ public final class Locale implements Cloneable, Serializable { * to {@link Locale.Builder#setLanguageTag(String)} which throws an exception * in this case. * + *

    Duplicate variants are accepted and included by the builder. + * However, duplicate extension singleton keys and their associated type + * are accepted but ignored. The same behavior applies to duplicate locale + * keys and attributes within a U extension. Note that subsequent subtags after + * the occurrence of a duplicate are not ignored. + * *

    The following conversions are performed:

      * *
    • The language code "und" is mapped to language "". @@ -2717,6 +2732,12 @@ public final class Locale implements Cloneable, Serializable { * just discards ill-formed and following portions of the * tag). * + *

      Duplicate variants are accepted and included by the builder. + * However, duplicate extension singleton keys and their associated type + * are accepted but ignored. The same behavior applies to duplicate locale + * keys and attributes within a U extension. Note that subsequent subtags after + * the occurrence of a duplicate are not ignored. + * *

      See {@link Locale##langtag_conversions converions} for a full list * of conversions that are performed on {@code languageTag}. * @@ -2726,9 +2747,13 @@ public final class Locale implements Cloneable, Serializable { * @see Locale#forLanguageTag(String) */ public Builder setLanguageTag(String languageTag) { - LanguageTag tag = LanguageTag.parse( - languageTag, new ParsePosition(0), false); - localeBuilder.setLanguageTag(tag); + if (LocaleUtils.isEmpty(languageTag)) { + localeBuilder.clear(); + } else { + LanguageTag tag = LanguageTag.parse( + languageTag, new ParsePosition(0), false); + localeBuilder.setLanguageTag(tag); + } return this; } @@ -2804,7 +2829,8 @@ public final class Locale implements Cloneable, Serializable { * Sets the variant. If variant is null or the empty string, the * variant in this {@code Builder} is removed. Otherwise, it * must consist of one or more {@linkplain Locale##def_variant well-formed} - * subtags, or an exception is thrown. + * subtags, or an exception is thrown. Duplicate variants are + * accepted and included by the builder. * *

      Note: This method checks if {@code variant} * satisfies the IETF BCP 47 variant subtag's syntax requirements, @@ -2837,7 +2863,8 @@ public final class Locale implements Cloneable, Serializable { *

      Note: The key {@link #UNICODE_LOCALE_EXTENSION * UNICODE_LOCALE_EXTENSION} ('u') is used for the Unicode locale extension. * Setting a value for this key replaces any existing Unicode locale key/type - * pairs with those defined in the extension. + * pairs with those defined in the extension. Duplicate locale attributes + * as well as locale keys and their associated type are accepted but ignored. * *

      Note: The key {@link #PRIVATE_USE_EXTENSION * PRIVATE_USE_EXTENSION} ('x') is used for the private use code. To be diff --git a/src/java.base/share/classes/java/util/SimpleTimeZone.java b/src/java.base/share/classes/java/util/SimpleTimeZone.java index 58d43692ee0..f047ce26e58 100644 --- a/src/java.base/share/classes/java/util/SimpleTimeZone.java +++ b/src/java.base/share/classes/java/util/SimpleTimeZone.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -112,15 +112,15 @@ import sun.util.calendar.Gregorian; *

      
        *      // Base GMT offset: -8:00
        *      // DST starts:      at 2:00am in standard time
      - *      //                  on the first Sunday in April
      + *      //                  on the second Sunday in March
        *      // DST ends:        at 2:00am in daylight time
      - *      //                  on the last Sunday in October
      + *      //                  on the first Sunday in November
        *      // Save:            1 hour
        *      SimpleTimeZone(-28800000,
        *                     "America/Los_Angeles",
      - *                     Calendar.APRIL, 1, -Calendar.SUNDAY,
      + *                     Calendar.MARCH, 8, -Calendar.SUNDAY,
        *                     7200000,
      - *                     Calendar.OCTOBER, -1, Calendar.SUNDAY,
      + *                     Calendar.NOVEMBER, 1, -Calendar.SUNDAY,
        *                     7200000,
        *                     3600000)
        *
      @@ -863,13 +863,24 @@ public class SimpleTimeZone extends TimeZone {
           }
       
           /**
      -     * Generates the hash code for the SimpleDateFormat object.
      +     * Generates the hash code for the SimpleTimeZone object.
            * @return the hash code for this object
            */
           public int hashCode()
           {
      -        return startMonth ^ startDay ^ startDayOfWeek ^ startTime ^
      -            endMonth ^ endDay ^ endDayOfWeek ^ endTime ^ rawOffset;
      +        int hash = 31 * getID().hashCode() + rawOffset;
      +        hash = 31 * hash + Boolean.hashCode(useDaylight);
      +        if (useDaylight) {
      +            hash = 31 * hash + startMonth;
      +            hash = 31 * hash + startDay;
      +            hash = 31 * hash + startDayOfWeek;
      +            hash = 31 * hash + startTime;
      +            hash = 31 * hash + endMonth;
      +            hash = 31 * hash + endDay;
      +            hash = 31 * hash + endDayOfWeek;
      +            hash = 31 * hash + endTime;
      +        }
      +        return hash;
           }
       
           /**
      diff --git a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
      index 7503c154ddb..1338f2fd804 100644
      --- a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
      +++ b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java
      @@ -1904,8 +1904,8 @@ public class CompletableFuture implements Future, CompletionStage {
               while ((r = result) == null) {
                   if (q == null) {
                       q = new Signaller(interruptible, 0L, 0L);
      -                if (Thread.currentThread() instanceof ForkJoinWorkerThread)
      -                    ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q);
      +                if (Thread.currentThread() instanceof ForkJoinWorkerThread wt)
      +                    ForkJoinPool.helpAsyncBlocker(wt.pool, q);
                   }
                   else if (!queued)
                       queued = tryPushStack(q);
      @@ -1950,8 +1950,8 @@ public class CompletableFuture implements Future, CompletionStage {
                       break;
                   else if (q == null) {
                       q = new Signaller(true, nanos, deadline);
      -                if (Thread.currentThread() instanceof ForkJoinWorkerThread)
      -                    ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q);
      +                if (Thread.currentThread() instanceof ForkJoinWorkerThread wt)
      +                    ForkJoinPool.helpAsyncBlocker(wt.pool, q);
                   }
                   else if (!queued)
                       queued = tryPushStack(q);
      diff --git a/src/java.base/share/classes/java/util/concurrent/ExecutorService.java b/src/java.base/share/classes/java/util/concurrent/ExecutorService.java
      index f899b56b288..7b5ca34ac0d 100644
      --- a/src/java.base/share/classes/java/util/concurrent/ExecutorService.java
      +++ b/src/java.base/share/classes/java/util/concurrent/ExecutorService.java
      @@ -133,7 +133,7 @@ import java.util.List;
        *   } catch (InterruptedException ex) {
        *     // (Re-)Cancel if current thread also interrupted
        *     pool.shutdownNow();
      - *     // Preserve interrupt status
      + *     // Preserve interrupted status
        *     Thread.currentThread().interrupt();
        *   }
        * }}
      @@ -375,7 +375,7 @@ public interface ExecutorService extends Executor, AutoCloseable { *

      If interrupted while waiting, this method stops all executing tasks as * if by invoking {@link #shutdownNow()}. It then continues to wait until all * actively executing tasks have completed. Tasks that were awaiting - * execution are not executed. The interrupt status will be re-asserted + * execution are not executed. The interrupted status will be re-asserted * before this method returns. * *

      If already terminated, invoking this method has no effect. diff --git a/src/java.base/share/classes/java/util/concurrent/Executors.java b/src/java.base/share/classes/java/util/concurrent/Executors.java index ef3d6348010..8b339cf83ef 100644 --- a/src/java.base/share/classes/java/util/concurrent/Executors.java +++ b/src/java.base/share/classes/java/util/concurrent/Executors.java @@ -755,6 +755,13 @@ public final class Executors { super.shutdown(); cleanable.clean(); // unregisters the cleanable } + + @Override + public List shutdownNow() { + List unexecuted = super.shutdownNow(); + cleanable.clean(); // unregisters the cleanable + return unexecuted; + } } /** diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java index 482fe3cf801..1f2c8d2ffa6 100644 --- a/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java +++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java @@ -875,7 +875,7 @@ public class ForkJoinPool extends AbstractExecutorService * ==================== * * Regular ForkJoinTasks manage task cancellation (method cancel) - * independently from the interrupt status of threads running + * independently from the interrupted status of threads running * tasks. Interrupts are issued internally only while * terminating, to wake up workers and cancel queued tasks. By * default, interrupts are cleared only when necessary to ensure @@ -900,7 +900,7 @@ public class ForkJoinPool extends AbstractExecutorService * with results accessed via join() differ from those via get(), * which differ from those invoked using pool submit methods by * non-workers (which comply with Future.get() specs). Internal - * usages of ForkJoinTasks ignore interrupt status when executing + * usages of ForkJoinTasks ignore interrupted status when executing * or awaiting completion. Otherwise, reporting task results or * exceptions is preferred to throwing InterruptedExceptions, * which are in turn preferred to timeouts. Similarly, completion @@ -4171,7 +4171,7 @@ public class ForkJoinPool extends AbstractExecutorService * method stops all executing tasks as if by invoking {@link * #shutdownNow()}. It then continues to wait until all actively * executing tasks have completed. Tasks that were awaiting - * execution are not executed. The interrupt status will be + * execution are not executed. The interrupted status will be * re-asserted before this method returns. * * @since 19 diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java index 51b2488264e..137cac45ed0 100644 --- a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java +++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java @@ -531,19 +531,22 @@ public abstract class ForkJoinTask implements Future, Serializable { * still correct, although it may contain a misleading stack * trace. * - * @param asExecutionException true if wrap as ExecutionException + * @param asExecutionException true if wrap the result as an + * ExecutionException. This applies only to actual exceptions, not + * implicit CancellationExceptions issued when not THROWN or + * available, which are not wrapped because by default they are + * issued separately from ExecutionExceptions by callers. Which + * may require further handling when this is not true (currently + * only in InvokeAnyTask). * @return the exception, or null if none */ private Throwable getException(boolean asExecutionException) { int s; Throwable ex; Aux a; if ((s = status) >= 0 || (s & ABNORMAL) == 0) return null; - else if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null) { - ex = new CancellationException(); - if (!asExecutionException || !(this instanceof InterruptibleTask)) - return ex; // else wrap below - } - else if (a.thread != Thread.currentThread()) { + if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null) + return new CancellationException(); + if (a.thread != Thread.currentThread()) { try { Constructor noArgCtor = null, oneArgCtor = null; for (Constructor c : ex.getClass().getConstructors()) { @@ -1814,6 +1817,8 @@ public abstract class ForkJoinTask implements Future, Serializable { (t = new InvokeAnyTask(c, this, t))); } return timed ? get(nanos, TimeUnit.NANOSECONDS) : get(); + } catch (CancellationException ce) { + throw new ExecutionException(ce); } finally { for (; t != null; t = t.pred) t.onRootCompletion(); diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinWorkerThread.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinWorkerThread.java index b942d3ecd09..566fc417952 100644 --- a/src/java.base/share/classes/java/util/concurrent/ForkJoinWorkerThread.java +++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinWorkerThread.java @@ -267,10 +267,8 @@ public class ForkJoinWorkerThread extends Thread { @Override // to record changes public void setContextClassLoader(ClassLoader cl) { - if (ClassLoader.getSystemClassLoader() != cl) { - resetCCL = true; - super.setContextClassLoader(cl); - } + resetCCL = ClassLoader.getSystemClassLoader() != cl; + super.setContextClassLoader(cl); } @Override // to re-establish CCL if necessary diff --git a/src/java.base/share/classes/java/util/concurrent/FutureTask.java b/src/java.base/share/classes/java/util/concurrent/FutureTask.java index 2ec97629105..a571cb77cce 100644 --- a/src/java.base/share/classes/java/util/concurrent/FutureTask.java +++ b/src/java.base/share/classes/java/util/concurrent/FutureTask.java @@ -69,7 +69,7 @@ public class FutureTask implements RunnableFuture { /* * Revision notes: This differs from previous versions of this * class that relied on AbstractQueuedSynchronizer, mainly to - * avoid surprising users about retaining interrupt status during + * avoid surprising users about retaining interrupted status during * cancellation races. Sync control in the current design relies * on a "state" field updated via CAS to track completion, along * with a simple Treiber stack to hold waiting threads. diff --git a/src/java.base/share/classes/java/util/concurrent/Semaphore.java b/src/java.base/share/classes/java/util/concurrent/Semaphore.java index 0e7a9ccc0b3..fce0c39cb78 100644 --- a/src/java.base/share/classes/java/util/concurrent/Semaphore.java +++ b/src/java.base/share/classes/java/util/concurrent/Semaphore.java @@ -334,7 +334,7 @@ public class Semaphore implements java.io.Serializable { * while waiting for a permit then it will continue to wait, but the * time at which the thread is assigned a permit may change compared to * the time it would have received the permit had no interruption - * occurred. When the thread does return from this method its interrupt + * occurred. When the thread does return from this method its interrupted * status will be set. */ public void acquireUninterruptibly() { @@ -494,7 +494,7 @@ public class Semaphore implements java.io.Serializable { *

      If the current thread is {@linkplain Thread#interrupt interrupted} * while waiting for permits then it will continue to wait and its * position in the queue is not affected. When the thread does return - * from this method its interrupt status will be set. + * from this method its interrupted status will be set. * * @param permits the number of permits to acquire * @throws IllegalArgumentException if {@code permits} is negative diff --git a/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedLongSynchronizer.java b/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedLongSynchronizer.java index c660af6a0ba..ba81123fc35 100644 --- a/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedLongSynchronizer.java +++ b/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedLongSynchronizer.java @@ -652,7 +652,7 @@ public abstract class AbstractQueuedLongSynchronizer /** * Acquires in exclusive mode, aborting if interrupted. - * Implemented by first checking interrupt status, then invoking + * Implemented by first checking interrupted status, then invoking * at least once {@link #tryAcquire}, returning on * success. Otherwise the thread is queued, possibly repeatedly * blocking and unblocking, invoking {@link #tryAcquire} @@ -674,7 +674,7 @@ public abstract class AbstractQueuedLongSynchronizer /** * Attempts to acquire in exclusive mode, aborting if interrupted, * and failing if the given timeout elapses. Implemented by first - * checking interrupt status, then invoking at least once {@link + * checking interrupted status, then invoking at least once {@link * #tryAcquire}, returning on success. Otherwise, the thread is * queued, possibly repeatedly blocking and unblocking, invoking * {@link #tryAcquire} until success or the thread is interrupted @@ -741,7 +741,7 @@ public abstract class AbstractQueuedLongSynchronizer /** * Acquires in shared mode, aborting if interrupted. Implemented - * by first checking interrupt status, then invoking at least once + * by first checking interrupted status, then invoking at least once * {@link #tryAcquireShared}, returning on success. Otherwise the * thread is queued, possibly repeatedly blocking and unblocking, * invoking {@link #tryAcquireShared} until success or the thread @@ -763,7 +763,7 @@ public abstract class AbstractQueuedLongSynchronizer /** * Attempts to acquire in shared mode, aborting if interrupted, and * failing if the given timeout elapses. Implemented by first - * checking interrupt status, then invoking at least once {@link + * checking interrupted status, then invoking at least once {@link * #tryAcquireShared}, returning on success. Otherwise, the * thread is queued, possibly repeatedly blocking and unblocking, * invoking {@link #tryAcquireShared} until success or the thread diff --git a/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java b/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java index 0ff216c80a0..c0779545083 100644 --- a/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java +++ b/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java @@ -1032,7 +1032,7 @@ public abstract class AbstractQueuedSynchronizer /** * Acquires in exclusive mode, aborting if interrupted. - * Implemented by first checking interrupt status, then invoking + * Implemented by first checking interrupted status, then invoking * at least once {@link #tryAcquire}, returning on * success. Otherwise the thread is queued, possibly repeatedly * blocking and unblocking, invoking {@link #tryAcquire} @@ -1054,7 +1054,7 @@ public abstract class AbstractQueuedSynchronizer /** * Attempts to acquire in exclusive mode, aborting if interrupted, * and failing if the given timeout elapses. Implemented by first - * checking interrupt status, then invoking at least once {@link + * checking interrupted status, then invoking at least once {@link * #tryAcquire}, returning on success. Otherwise, the thread is * queued, possibly repeatedly blocking and unblocking, invoking * {@link #tryAcquire} until success or the thread is interrupted @@ -1121,7 +1121,7 @@ public abstract class AbstractQueuedSynchronizer /** * Acquires in shared mode, aborting if interrupted. Implemented - * by first checking interrupt status, then invoking at least once + * by first checking interrupted status, then invoking at least once * {@link #tryAcquireShared}, returning on success. Otherwise the * thread is queued, possibly repeatedly blocking and unblocking, * invoking {@link #tryAcquireShared} until success or the thread @@ -1143,7 +1143,7 @@ public abstract class AbstractQueuedSynchronizer /** * Attempts to acquire in shared mode, aborting if interrupted, and * failing if the given timeout elapses. Implemented by first - * checking interrupt status, then invoking at least once {@link + * checking interrupted status, then invoking at least once {@link * #tryAcquireShared}, returning on success. Otherwise, the * thread is queued, possibly repeatedly blocking and unblocking, * invoking {@link #tryAcquireShared} until success or the thread diff --git a/src/java.base/share/classes/java/util/concurrent/locks/LockSupport.java b/src/java.base/share/classes/java/util/concurrent/locks/LockSupport.java index 917678b5f1e..38531c80a30 100644 --- a/src/java.base/share/classes/java/util/concurrent/locks/LockSupport.java +++ b/src/java.base/share/classes/java/util/concurrent/locks/LockSupport.java @@ -121,7 +121,7 @@ import jdk.internal.misc.Unsafe; * } * * waiters.remove(); - * // ensure correct interrupt status on return + * // ensure correct interrupted status on return * if (wasInterrupted) * Thread.currentThread().interrupt(); * } @@ -207,7 +207,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread upon return. + * for example, the interrupted status of the thread upon return. * * @param blocker the synchronization object responsible for this * thread parking @@ -252,7 +252,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread, or the elapsed time + * for example, the interrupted status of the thread, or the elapsed time * upon return. * * @param blocker the synchronization object responsible for this @@ -300,7 +300,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread, or the current time + * for example, the interrupted status of the thread, or the current time * upon return. * * @param blocker the synchronization object responsible for this @@ -360,7 +360,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread upon return. + * for example, the interrupted status of the thread upon return. */ public static void park() { if (Thread.currentThread().isVirtual()) { @@ -395,7 +395,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread, or the elapsed time + * for example, the interrupted status of the thread, or the elapsed time * upon return. * * @param nanos the maximum number of nanoseconds to wait @@ -434,7 +434,7 @@ public final class LockSupport { *

      This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, - * for example, the interrupt status of the thread, or the current time + * for example, the interrupted status of the thread, or the current time * upon return. * * @param deadline the absolute time, in milliseconds from the Epoch, diff --git a/src/java.base/share/classes/javax/crypto/Cipher.java b/src/java.base/share/classes/javax/crypto/Cipher.java index 902c3998fbe..f95917b5c86 100644 --- a/src/java.base/share/classes/javax/crypto/Cipher.java +++ b/src/java.base/share/classes/javax/crypto/Cipher.java @@ -286,7 +286,32 @@ public class Cipher { this.lock = new Object(); } - private static final String SHA512TRUNCATED = "SHA512/2"; + // for special handling SHA-512/224, SHA-512/256, SHA512/224, SHA512/256 + static int indexOfRealSlash(String s, int fromIndex) { + while (true) { + int pos = s.indexOf('/', fromIndex); + // 512/2 + if (pos > 3 && pos + 1 < s.length() + && s.charAt(pos - 3) == '5' + && s.charAt(pos - 2) == '1' + && s.charAt(pos - 1) == '2' + && s.charAt(pos + 1) == '2') { + fromIndex = pos + 1; + // see 512/2, find next + } else { + return pos; + } + } + } + + static String reqNonEmpty(String in, String msg) + throws NoSuchAlgorithmException { + in = in.trim(); + if (in.isEmpty()) { + throw new NoSuchAlgorithmException(msg); + } + return in; + } // Parse the specified cipher transformation for algorithm and the // optional mode and padding. If the transformation contains only @@ -305,42 +330,34 @@ public class Cipher { * 2) feedback component (e.g., CFB) - optional * 3) padding component (e.g., PKCS5Padding) - optional */ - - // check if the transformation contains algorithms with "/" in their - // name which can cause the parsing logic to go wrong - int sha512Idx = transformation.toUpperCase(Locale.ENGLISH) - .indexOf(SHA512TRUNCATED); - int startIdx = (sha512Idx == -1 ? 0 : - sha512Idx + SHA512TRUNCATED.length()); - int endIdx = transformation.indexOf('/', startIdx); - - boolean algorithmOnly = (endIdx == -1); - String algo = (algorithmOnly ? transformation.trim() : - transformation.substring(0, endIdx).trim()); - if (algo.isEmpty()) { - throw new NoSuchAlgorithmException("Invalid transformation: " + - "algorithm not specified-" - + transformation); + int endIdx = indexOfRealSlash(transformation, 0); + if (endIdx == -1) { // algo only, done + return new String[] { reqNonEmpty(transformation, + "Invalid transformation: algorithm not specified") + }; } - if (algorithmOnly) { // done - return new String[] { algo }; + // must be algo/mode/padding + String algo = reqNonEmpty(transformation.substring(0, endIdx), + "Invalid transformation: algorithm not specified"); + + int startIdx = endIdx + 1; + endIdx = indexOfRealSlash(transformation, startIdx); + if (endIdx == -1) { + throw new NoSuchAlgorithmException( + "Invalid transformation format: " + transformation); + } + String mode = reqNonEmpty(transformation.substring(startIdx, + endIdx), "Invalid transformation: missing mode"); + + startIdx = endIdx + 1; + endIdx = indexOfRealSlash(transformation, startIdx); + if (endIdx == -1) { + return new String[] { algo, mode, + reqNonEmpty(transformation.substring(startIdx), + "Invalid transformation: missing padding") }; } else { - // continue parsing mode and padding - startIdx = endIdx+1; - endIdx = transformation.indexOf('/', startIdx); - if (endIdx == -1) { - throw new NoSuchAlgorithmException("Invalid transformation" - + " format:" + transformation); - } - String mode = transformation.substring(startIdx, endIdx).trim(); - String padding = transformation.substring(endIdx+1).trim(); - // ensure mode and padding are specified - if (mode.isEmpty() || padding.isEmpty()) { - throw new NoSuchAlgorithmException("Invalid transformation: " + - "missing mode and/or padding-" - + transformation); - } - return new String[] { algo, mode, padding }; + throw new NoSuchAlgorithmException( + "Invalid transformation format: " + transformation); } } @@ -515,12 +532,12 @@ public class Cipher { * transformation * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if no provider supports a {@code CipherSpi} - * implementation for the specified algorithm + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation is not found or + * is found but does not support the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * is found but does not support the padding scheme * * @see java.security.Provider */ @@ -573,8 +590,12 @@ public class Cipher { failure = e; } } + if (failure instanceof NoSuchPaddingException nspe) { + throw nspe; + } throw new NoSuchAlgorithmException - ("Cannot find any provider supporting " + transformation, failure); + ("Cannot find any provider supporting " + transformation, + failure); } /** @@ -582,8 +603,8 @@ public class Cipher { * transformation. * *

      A new {@code Cipher} object encapsulating the - * {@code CipherSpi} implementation from the specified provider - * is returned. The specified provider must be registered + * {@code CipherSpi} implementation from the specified {@code provider} + * is returned. The specified {@code provider} must be registered * in the security provider list. * *

      Note that the list of registered providers may be retrieved via @@ -625,15 +646,16 @@ public class Cipher { * is {@code null} or empty * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if a {@code CipherSpi} implementation for the - * specified algorithm is not available from the specified - * provider + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation from the specified + * {@code provider} is not found or is found but does not support + * the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * from the specified {@code provider} is found but does not + * support the padding scheme * - * @throws NoSuchProviderException if the specified provider is not + * @throws NoSuchProviderException if the specified {@code provider} is not * registered in the security provider list * * @see java.security.Provider @@ -706,13 +728,14 @@ public class Cipher { * is {@code null} * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if a {@code CipherSpi} implementation for the - * specified algorithm is not available from the specified - * {@code provider} object + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation from the specified + * {@code provider} is not found or is found but does not support + * the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * from the specified {@code provider} is found but does not + * support the padding scheme * * @see java.security.Provider */ diff --git a/src/java.base/share/classes/javax/crypto/Mac.java b/src/java.base/share/classes/javax/crypto/Mac.java index 82874693cf2..4405e39d7a0 100644 --- a/src/java.base/share/classes/javax/crypto/Mac.java +++ b/src/java.base/share/classes/javax/crypto/Mac.java @@ -627,6 +627,7 @@ public class Mac implements Cloneable { } byte[] mac = doFinal(); System.arraycopy(mac, 0, output, outOffset, macLen); + Arrays.fill(mac, (byte)0); } /** diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index e529e8ba350..fa6e5b4aac3 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -452,21 +452,6 @@ public interface JavaLangAccess { */ MethodHandle stringConcatHelper(String name, MethodType methodType); - /** - * Get the string concat initial coder - */ - long stringConcatInitialCoder(); - - /** - * Update lengthCoder for constant - */ - long stringConcatMix(long lengthCoder, String constant); - - /** - * Mix value length and coder into current length and coder. - */ - long stringConcatMix(long lengthCoder, char value); - /** * Creates helper for string concatenation. *

      diff --git a/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java b/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java index e20a1e77423..b0a71529fa7 100644 --- a/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java +++ b/src/java.base/share/classes/jdk/internal/access/SharedSecrets.java @@ -25,6 +25,7 @@ package jdk.internal.access; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.Stable; import javax.crypto.SealedObject; @@ -58,8 +59,20 @@ import javax.security.auth.x500.X500Principal; * increased complexity and lack of sustainability. * Use this only as a last resort! * + * + *

      Notes on the @AOTSafeClassInitializer annotation: + * + *

      All static fields in SharedSecrets that are initialized in the AOT + * assembly phase must be stateless (as checked by the HotSpot C++ class + * CDSHeapVerifier::SharedSecretsAccessorFinder) so they can be safely + * stored in the AOT cache. + * + *

      Static fields such as javaObjectInputFilterAccess point to a Lambda + * which is not stateless. The AOT assembly phase must not execute any Java + * code that would lead to the initialization of such fields, or else the AOT + * cache creation will fail. */ - +@AOTSafeClassInitializer public class SharedSecrets { // This field is not necessarily stable private static JavaAWTFontAccess javaAWTFontAccess; diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java index dda9accd8b9..b30592a4ebd 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java @@ -30,6 +30,7 @@ import java.lang.classfile.constantpool.ClassEntry; import java.lang.classfile.constantpool.ConstantPool; import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.classfile.constantpool.PoolEntry; +import java.lang.runtime.ExactConversionsSupport; import java.util.Arrays; import jdk.internal.access.JavaLangAccess; @@ -275,8 +276,11 @@ public final class BufWriterImpl implements BufWriter { void writeUtfEntry(String str) { int strlen = str.length(); int countNonZeroAscii = JLA.countNonZeroAscii(str); - int utflen = utfLen(str, countNonZeroAscii); - Util.checkU2(utflen, "utf8 length"); + long utflenLong = utfLen(str, countNonZeroAscii); + if (!ExactConversionsSupport.isLongToCharExact(utflenLong)) { + throw new IllegalArgumentException("utf8 length out of range of u2: " + utflenLong); + } + int utflen = (int)utflenLong; reserveSpace(utflen + 3); int offset = this.offset; diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java index 6dbde898ee5..921255d5398 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, 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 @@ -309,7 +309,7 @@ public record ClassRemapperImpl(Function mapFunction) impl case Signature.ClassTypeSig cts -> Signature.ClassTypeSig.of( cts.outerType().map(this::mapSignature).orElse(null), - map(cts.classDesc()), + Util.toInternalName(map(cts.classDesc())), cts.typeArgs().stream().map(ta -> switch (ta) { case Signature.TypeArg.Unbounded u -> u; case Signature.TypeArg.Bounded bta -> Signature.TypeArg.bounded( diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java index 5486b58f04e..06c5abb95a8 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, 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 @@ -73,7 +73,7 @@ public final class SignaturesImpl { while (!match(')')) { if (paramTypes == null) paramTypes = new ArrayList<>(); - paramTypes.add(typeSig()); + paramTypes.add(validateNonVoid(typeSig())); } Signature returnType = typeSig(); ArrayList throwsTypes = null; @@ -113,8 +113,22 @@ public final class SignaturesImpl { RefTypeSig classBound = null; ArrayList interfaceBounds = null; require(':'); - if (sig.charAt(sigp) != ':') - classBound = referenceTypeSig(); + if (sig.charAt(sigp) != ':') { + int p = nextIdentifierEnd(sig, sigp); + // For non-identifier chars: + // . / < indicates class type (inner, package, type arg) + // [ indicates array type + // ; indicates class/type var type + // > and : are illegal, such as in + if (p < sig.length()) { + char limit = sig.charAt(p); + if (limit != '>' && limit != ':') { + classBound = referenceTypeSig(); + } + } + // If classBound is absent here, we start tokenizing + // next type parameter, which can trigger failures + } while (match(':')) { if (interfaceBounds == null) interfaceBounds = new ArrayList<>(); @@ -226,13 +240,9 @@ public final class SignaturesImpl { */ private int requireIdentifier() { int start = sigp; - l: while (sigp < sig.length()) { - switch (sig.charAt(sigp)) { - case '.', ';', '[', '/', '<', '>', ':' -> { - break l; - } - } + if (isNonIdentifierChar(sig.charAt(sigp))) + break; sigp++; } if (start == sigp) { @@ -241,6 +251,77 @@ public final class SignaturesImpl { return sigp; } + // Non-identifier chars in ascii 0 to 63, note [ is larger + private static final long SMALL_NON_IDENTIFIER_CHARS_SET = (1L << '.') + | (1L << ';') + | (1L << '/') + | (1L << '<') + | (1L << '>') + | (1L << ':'); + + private static boolean isNonIdentifierChar(char c) { + return c < Long.SIZE ? (SMALL_NON_IDENTIFIER_CHARS_SET & (1L << c)) != 0 : c == '['; + } + + /// {@return exclusive end of the next identifier} + public static int nextIdentifierEnd(String st, int start) { + int end = st.length(); + for (int i = start; i < end; i++) { + if (isNonIdentifierChar(st.charAt(i))) { + return i; + } + } + return end; + } + + /// Validates this string as a simple identifier. + public static String validateIdentifier(String st) { + var len = st.length(); // implicit null check + if (len == 0 || nextIdentifierEnd(st, 0) != len) { + throw new IllegalArgumentException("Not a valid identifier: " + st); + } + return st; + } + + /// Validates this string as slash-separated one or more identifiers. + public static String validatePackageSpecifierPlusIdentifier(String st) { + int nextIdentifierStart = 0; + int len = st.length(); + while (nextIdentifierStart < len) { + int end = nextIdentifierEnd(st, nextIdentifierStart); + if (end == len) + return st; + if (end == nextIdentifierStart || st.charAt(end) != '/') + throw new IllegalArgumentException("Not a class name: " + st); + nextIdentifierStart = end + 1; + } + // Couldn't get an identifier initially or after a separator. + throw new IllegalArgumentException("Not a class name: " + st); + } + + /// Validates the signature to be non-void (a valid field type). + public static Signature validateNonVoid(Signature incoming) { + Objects.requireNonNull(incoming); + if (incoming instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + return incoming; + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(Signature[] signatures) { + return validateArgumentList(List.of(signatures)); + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(List signatures) { + var res = List.copyOf(signatures); // deep null checks + for (var sig : signatures) { + if (sig instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + } + return res; + } + public static record BaseTypeSigImpl(char baseType) implements Signature.BaseTypeSig { @Override @@ -316,13 +397,13 @@ public final class SignaturesImpl { private static StringBuilder printTypeParameters(List typeParameters) { var sb = new StringBuilder(); - if (typeParameters != null && !typeParameters.isEmpty()) { + if (!typeParameters.isEmpty()) { sb.append('<'); for (var tp : typeParameters) { sb.append(tp.identifier()).append(':'); if (tp.classBound().isPresent()) sb.append(tp.classBound().get().signatureString()); - if (tp.interfaceBounds() != null) for (var is : tp.interfaceBounds()) + for (var is : tp.interfaceBounds()) sb.append(':').append(is.signatureString()); } sb.append('>'); @@ -337,7 +418,7 @@ public final class SignaturesImpl { public String signatureString() { var sb = printTypeParameters(typeParameters); sb.append(superclassSignature.signatureString()); - if (superinterfaceSignatures != null) for (var in : superinterfaceSignatures) + for (var in : superinterfaceSignatures) sb.append(in.signatureString()); return sb.toString(); } diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java index bb862578b34..07406b2ee7f 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java @@ -1513,13 +1513,6 @@ public final class VerifierImpl { } } - // Return TRUE if all code paths starting with start_bc_offset end in - // bytecode athrow or loop. - boolean ends_in_athrow(int start_bc_offset) { - log_info("unimplemented VerifierImpl.ends_in_athrow"); - return true; - } - boolean verify_invoke_init(RawBytecodeHelper bcs, int ref_class_index, VerificationType ref_class_type, VerificationFrame current_frame, int code_length, boolean in_try_block, boolean this_uninit, ConstantPoolWrapper cp, VerificationTable stackmap_table) { @@ -1532,16 +1525,6 @@ public final class VerifierImpl { verifyError("Bad method call"); } if (in_try_block) { - for(var exhandler : _method.exceptionTable()) { - int start_pc = exhandler[0]; - int end_pc = exhandler[1]; - - if (bci >= start_pc && bci < end_pc) { - if (!ends_in_athrow(exhandler[2])) { - verifyError("Bad method call from after the start of a try block"); - } - } - } verify_exception_handler_targets(bci, true, current_frame, stackmap_table); } current_frame.initialize_object(type, current_type()); diff --git a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java index c9c6b53fcda..820dfe90073 100644 --- a/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java +++ b/src/java.base/share/classes/jdk/internal/io/JdkConsoleImpl.java @@ -174,7 +174,9 @@ public final class JdkConsoleImpl implements JdkConsole { ioe.addSuppressed(x); } if (ioe != null) { - Arrays.fill(passwd, ' '); + if (passwd != null) { + Arrays.fill(passwd, ' '); + } try { if (reader instanceof LineReader lr) { lr.zeroOut(); diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 79e718c76e5..e062e1629ff 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -137,6 +137,45 @@ public final class ImageReader implements AutoCloseable { return reader.findNode(name); } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

      This is equivalent to: + *

      {@code
      +     * findNode("/modules/" + moduleName + "/" + resourcePath)
      +     * }
      + * but more performant, and returns {@code null} for directories. + * + * @param moduleName The module name of the requested resource. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public Node findResourceNode(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.findResourceNode(moduleName, resourcePath); + } + + /** + * Returns whether a resource exists in the given module. + * + *

      This is equivalent to: + *

      {@code
      +     * findResourceNode(moduleName, resourcePath) != null
      +     * }
      + * but more performant, and will not create or cache new nodes. + * + * @param moduleName The module name of the resource being tested for. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public boolean containsResource(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.containsResource(moduleName, resourcePath); + } + /** * Returns a copy of the content of a resource node. The buffer returned by * this method is not cached by the node, and each call returns a new array @@ -276,10 +315,7 @@ public final class ImageReader implements AutoCloseable { * Returns a node with the given name, or null if no resource or directory of * that name exists. * - *

      This is the only public API by which anything outside this class can access - * {@code Node} instances either directly, or by resolving symbolic links. - * - *

      Note also that there is no reentrant calling back to this method from within + *

      Note that there is no reentrant calling back to this method from within * the node handling code. * * @param name an absolute, {@code /}-separated path string, prefixed with either @@ -291,6 +327,9 @@ public final class ImageReader implements AutoCloseable { // We cannot get the root paths ("/modules" or "/packages") here // because those nodes are already in the nodes cache. if (name.startsWith(MODULES_ROOT + "/")) { + // This may perform two lookups, one for a directory (in + // "/modules/...") and one for a non-prefixed resource + // (with "/modules" removed). node = buildModulesNode(name); } else if (name.startsWith(PACKAGES_ROOT + "/")) { node = buildPackagesNode(name); @@ -307,6 +346,55 @@ public final class ImageReader implements AutoCloseable { return node; } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

      Note that there is no reentrant calling back to this method from within + * the node handling code. + */ + Node findResourceNode(String moduleName, String resourcePath) { + // Unlike findNode(), this method makes only one lookup in the + // underlying jimage, but can only reliably return resource nodes. + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath; + // Synchronize as tightly as possible to reduce locking contention. + synchronized (this) { + Node node = nodes.get(nodeName); + if (node == null) { + ImageLocation loc = findLocation(moduleName, resourcePath); + if (loc != null && isResource(loc)) { + node = newResource(nodeName, loc); + nodes.put(node.getName(), node); + } + return node; + } else { + return node.isResource() ? node : null; + } + } + } + + /** + * Returns whether a resource exists in the given module. + * + *

      This method is expected to be called frequently for resources + * which do not exist in the given module (e.g. as part of classpath + * search). As such, it skips checking the nodes cache and only checks + * for an entry in the jimage file, as this is faster if the resource + * is not present. This also means it doesn't need synchronization. + */ + boolean containsResource(String moduleName, String resourcePath) { + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + // If the given module name is 'modules', then 'isResource()' + // returns false to prevent false positives. + ImageLocation loc = findLocation(moduleName, resourcePath); + return loc != null && isResource(loc); + } + /** * Builds a node in the "/modules/..." namespace. * diff --git a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java index bc5bd9d4265..72c7e7e7451 100644 --- a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java +++ b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java @@ -75,9 +75,13 @@ public class BootLoader { private static final ConcurrentHashMap CLASS_LOADER_VALUE_MAP = new ConcurrentHashMap<>(); - // native libraries loaded by the boot class loader - private static final NativeLibraries NATIVE_LIBS - = NativeLibraries.newInstance(null); + // Holder has the field(s) that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + private static class Holder { + // native libraries loaded by the boot class loader + private static final NativeLibraries NATIVE_LIBS + = NativeLibraries.newInstance(null); + } /** * Returns the unnamed module for the boot loader. @@ -104,7 +108,7 @@ public class BootLoader { * Returns NativeLibraries for the boot class loader. */ public static NativeLibraries getNativeLibraries() { - return NATIVE_LIBS; + return Holder.NATIVE_LIBS; } /** diff --git a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java index 44eaab0e83a..98cedb0b3bf 100644 --- a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java +++ b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -153,7 +153,7 @@ public final class NativeLibraries { } // cannot be loaded by other class loaders - if (loadedLibraryNames.contains(name)) { + if (Holder.loadedLibraryNames.contains(name)) { throw new UnsatisfiedLinkError("Native Library " + name + " already loaded in another classloader"); } @@ -203,7 +203,7 @@ public final class NativeLibraries { NativeLibraryContext.pop(); } // register the loaded native library - loadedLibraryNames.add(name); + Holder.loadedLibraryNames.add(name); libraries.put(name, lib); return lib; } finally { @@ -243,6 +243,11 @@ public final class NativeLibraries { return lib; } + // Called at the end of AOTCache assembly phase. + public void clear() { + libraries.clear(); + } + private NativeLibrary findFromPaths(String[] paths, Class fromClass, String name) { for (String path : paths) { File libfile = new File(path, System.mapLibraryName(name)); @@ -368,7 +373,7 @@ public final class NativeLibraries { acquireNativeLibraryLock(name); try { /* remove the native library name */ - if (!loadedLibraryNames.remove(name)) { + if (!Holder.loadedLibraryNames.remove(name)) { throw new IllegalStateException(name + " has already been unloaded"); } NativeLibraryContext.push(UNLOADER); @@ -395,9 +400,13 @@ public final class NativeLibraries { static final String[] USER_PATHS = ClassLoaderHelper.parsePath(StaticProperty.javaLibraryPath()); } - // All native libraries we've loaded. - private static final Set loadedLibraryNames = + // Holder has the fields that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + static class Holder { + // All native libraries we've loaded. + private static final Set loadedLibraryNames = ConcurrentHashMap.newKeySet(); + } // reentrant lock class that allows exact counting (with external synchronization) @SuppressWarnings("serial") diff --git a/src/java.base/share/classes/jdk/internal/loader/Resource.java b/src/java.base/share/classes/jdk/internal/loader/Resource.java index b72f4df7d52..312dfc859aa 100644 --- a/src/java.base/share/classes/jdk/internal/loader/Resource.java +++ b/src/java.base/share/classes/jdk/internal/loader/Resource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -33,7 +33,6 @@ import java.security.CodeSigner; import java.util.jar.Manifest; import java.nio.ByteBuffer; import java.util.Arrays; -import sun.nio.ByteBuffered; /** * This class is used to represent a Resource that has been loaded @@ -130,10 +129,6 @@ public abstract class Resource { * @return Resource data or null. */ public ByteBuffer getByteBuffer() throws IOException { - InputStream in = cachedInputStream(); - if (in instanceof ByteBuffered) { - return ((ByteBuffered)in).getByteBuffer(); - } return null; } diff --git a/src/java.base/share/classes/jdk/internal/math/DoubleConsts.java b/src/java.base/share/classes/jdk/internal/math/DoubleConsts.java index d3a271fdd07..168e99d4ef5 100644 --- a/src/java.base/share/classes/jdk/internal/math/DoubleConsts.java +++ b/src/java.base/share/classes/jdk/internal/math/DoubleConsts.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -32,8 +32,6 @@ import static java.lang.Double.SIZE; /** * This class contains additional constants documenting limits of the * {@code double} type. - * - * @author Joseph D. Darcy */ public class DoubleConsts { diff --git a/src/java.base/share/classes/jdk/internal/math/FloatConsts.java b/src/java.base/share/classes/jdk/internal/math/FloatConsts.java index fd304c7871a..2bd484e99f3 100644 --- a/src/java.base/share/classes/jdk/internal/math/FloatConsts.java +++ b/src/java.base/share/classes/jdk/internal/math/FloatConsts.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -32,8 +32,6 @@ import static java.lang.Float.SIZE; /** * This class contains additional constants documenting limits of the * {@code float} type. - * - * @author Joseph D. Darcy */ public class FloatConsts { diff --git a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java index 32399310bd3..e90ca4d75f1 100644 --- a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java +++ b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java @@ -27,8 +27,6 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; -import java.util.Arrays; - /** * A class for converting between ASCII and decimal representations of a single * or double precision floating point number. Most conversions are provided via @@ -102,7 +100,6 @@ public class FloatingDecimal{ * values into an ASCII String representation. */ public interface BinaryToASCIIConverter { - int getChars(byte[] result); /** * Retrieves the decimal exponent most closely corresponding to this value. @@ -115,21 +112,7 @@ public class FloatingDecimal{ * @param digits The digit array. * @return The number of valid digits copied into the array. */ - int getDigits(char[] digits); - - /** - * Indicates the sign of the value. - * @return {@code value < 0.0}. - */ - boolean isNegative(); - - /** - * Indicates whether the value is either infinite or not a number. - * - * @return true if and only if the value is NaN - * or infinite. - */ - boolean isExceptional(); + int getDigits(byte[] digits); /** * Indicates whether the value was rounded up during the binary to ASCII @@ -147,63 +130,9 @@ public class FloatingDecimal{ boolean decimalDigitsExact(); } - /** - * A BinaryToASCIIConverter which represents NaN - * and infinite values. - */ - private static class ExceptionalBinaryToASCIIBuffer implements BinaryToASCIIConverter { - private final String image; - private final boolean isNegative; - - public ExceptionalBinaryToASCIIBuffer(String image, boolean isNegative) { - this.image = image; - this.isNegative = isNegative; - } - - @Override - @SuppressWarnings("deprecation") - public int getChars(byte[] chars) { - image.getBytes(0, image.length(), chars, 0); - return image.length(); - } - - @Override - public int getDecimalExponent() { - throw new IllegalArgumentException("Exceptional value does not have an exponent"); - } - - @Override - public int getDigits(char[] digits) { - throw new IllegalArgumentException("Exceptional value does not have digits"); - } - - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return true; - } - - @Override - public boolean digitsRoundedUp() { - throw new IllegalArgumentException("Exceptional value is not rounded"); - } - - @Override - public boolean decimalDigitsExact() { - throw new IllegalArgumentException("Exceptional value is not exact"); - } - } - private static final String INFINITY_REP = "Infinity"; private static final String NAN_REP = "NaN"; - private static final BinaryToASCIIConverter B2AC_POSITIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer(INFINITY_REP, false); - private static final BinaryToASCIIConverter B2AC_NEGATIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer("-" + INFINITY_REP, true); - private static final BinaryToASCIIConverter B2AC_NOT_A_NUMBER = new ExceptionalBinaryToASCIIBuffer(NAN_REP, false); private static final BinaryToASCIIConverter B2AC_POSITIVE_ZERO = new BinaryToASCIIBuffer(false, new byte[]{'0'}); private static final BinaryToASCIIConverter B2AC_NEGATIVE_ZERO = new BinaryToASCIIBuffer(true, new byte[]{'0'}); @@ -255,21 +184,11 @@ public class FloatingDecimal{ } @Override - public int getDigits(char[] digits) { + public int getDigits(byte[] digits) { System.arraycopy(this.digits, firstDigitIndex, digits, 0, this.nDigits); return this.nDigits; } - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return false; - } - @Override public boolean digitsRoundedUp() { return decimalDigitsRoundedUp; @@ -826,83 +745,6 @@ public class FloatingDecimal{ 61, }; - /** - * Converts the decimal representation of a floating-point number into its - * ASCII character representation and stores it in the provided byte array. - * - * @param result the byte array to store the ASCII representation, must have length at least 26 - * @return the number of characters written to the result array - */ - public int getChars(byte[] result) { - assert nDigits <= 19 : nDigits; // generous bound on size of nDigits - int i = 0; - if (isNegative) { - result[0] = '-'; - i = 1; - } - if (decExponent > 0 && decExponent < 8) { - // print digits.digits. - int charLength = Math.min(nDigits, decExponent); - System.arraycopy(digits, firstDigitIndex, result, i, charLength); - i += charLength; - if (charLength < decExponent) { - charLength = decExponent - charLength; - Arrays.fill(result, i, i + charLength, (byte) '0'); - i += charLength; - result[i++] = '.'; - result[i++] = '0'; - } else { - result[i++] = '.'; - if (charLength < nDigits) { - int t = nDigits - charLength; - System.arraycopy(digits, firstDigitIndex + charLength, result, i, t); - i += t; - } else { - result[i++] = '0'; - } - } - } else if (decExponent <= 0 && decExponent > -3) { - result[i++] = '0'; - result[i++] = '.'; - if (decExponent != 0) { - Arrays.fill(result, i, i-decExponent, (byte) '0'); - i -= decExponent; - } - System.arraycopy(digits, firstDigitIndex, result, i, nDigits); - i += nDigits; - } else { - result[i++] = digits[firstDigitIndex]; - result[i++] = '.'; - if (nDigits > 1) { - System.arraycopy(digits, firstDigitIndex+1, result, i, nDigits - 1); - i += nDigits - 1; - } else { - result[i++] = '0'; - } - result[i++] = 'E'; - int e; - if (decExponent <= 0) { - result[i++] = '-'; - e = -decExponent + 1; - } else { - e = decExponent - 1; - } - // decExponent has 1, 2, or 3, digits - if (e <= 9) { - result[i++] = (byte) (e + '0'); - } else if (e <= 99) { - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } else { - result[i++] = (byte) (e / 100 + '0'); - e %= 100; - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } - } - return i; - } - } private static final ThreadLocal threadLocalBinaryToASCIIBuffer = @@ -1707,9 +1549,9 @@ public class FloatingDecimal{ // Discover obvious special cases of NaN and Infinity. if ( binExp == (int)(DoubleConsts.EXP_BIT_MASK>>EXP_SHIFT) ) { if ( fractBits == 0L ){ - return isNegative ? B2AC_NEGATIVE_INFINITY : B2AC_POSITIVE_INFINITY; + throw new IllegalArgumentException((isNegative ? "-" : "") + INFINITY_REP); } else { - return B2AC_NOT_A_NUMBER; + throw new IllegalArgumentException(NAN_REP); } } // Finish unpacking diff --git a/src/java.base/share/classes/jdk/internal/math/MathUtils.java b/src/java.base/share/classes/jdk/internal/math/MathUtils.java index 0de07f8ed60..172090facc0 100644 --- a/src/java.base/share/classes/jdk/internal/math/MathUtils.java +++ b/src/java.base/share/classes/jdk/internal/math/MathUtils.java @@ -26,12 +26,14 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** * This class exposes package private utilities for other classes. * Thus, all methods are assumed to be invoked with correct arguments, * so these are not checked at all. */ +@AOTSafeClassInitializer final class MathUtils { /* * For full details about this code see the following reference: diff --git a/src/java.base/share/classes/jdk/internal/misc/CDS.java b/src/java.base/share/classes/jdk/internal/misc/CDS.java index 72b8479de9a..b61743c1fb3 100644 --- a/src/java.base/share/classes/jdk/internal/misc/CDS.java +++ b/src/java.base/share/classes/jdk/internal/misc/CDS.java @@ -47,11 +47,13 @@ import jdk.internal.util.StaticProperty; public class CDS { // Must be in sync with cdsConfig.hpp - private static final int IS_DUMPING_ARCHIVE = 1 << 0; - private static final int IS_DUMPING_METHOD_HANDLES = 1 << 1; - private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 2; - private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 3; - private static final int IS_USING_ARCHIVE = 1 << 4; + private static final int IS_DUMPING_AOT_LINKED_CLASSES = 1 << 0; + private static final int IS_DUMPING_ARCHIVE = 1 << 1; + private static final int IS_DUMPING_METHOD_HANDLES = 1 << 2; + private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 3; + private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4; + private static final int IS_USING_ARCHIVE = 1 << 5; + private static final int configStatus = getCDSConfigStatus(); /** @@ -82,6 +84,10 @@ public class CDS { return (configStatus & IS_DUMPING_STATIC_ARCHIVE) != 0; } + public static boolean isDumpingAOTLinkedClasses() { + return (configStatus & IS_DUMPING_AOT_LINKED_CLASSES) != 0; + } + public static boolean isSingleThreadVM() { return isDumpingStaticArchive(); } diff --git a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java index 423ffa03d31..32f6d5d4905 100644 --- a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java +++ b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java @@ -379,7 +379,7 @@ public class ThreadFlock implements AutoCloseable { *

      This method may only be invoked by the flock owner. * *

      If interrupted then this method continues to wait until all threads - * finish, before completing with the interrupt status set. + * finish, before completing with the interrupted status set. * *

      A ThreadFlock is intended to be used in a structured manner. If * this method is called to close a flock before nested flocks are closed then it diff --git a/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java b/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java index ce837027faa..d24cc77600c 100644 --- a/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java +++ b/src/java.base/share/classes/jdk/internal/module/ModulePatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -467,8 +467,10 @@ public final class ModulePatcher { } @Override public ByteBuffer getByteBuffer() throws IOException { - byte[] bytes = getInputStream().readAllBytes(); - return ByteBuffer.wrap(bytes); + try (InputStream in = getInputStream()) { + byte[] bytes = in.readAllBytes(); + return ByteBuffer.wrap(bytes); + } } @Override public InputStream getInputStream() throws IOException { diff --git a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java index 39f433d4041..370c151af84 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -414,26 +414,18 @@ public final class SystemModuleFinders { * Returns {@code true} if the given resource exists, {@code false} * if not found. */ - private boolean containsResource(String resourcePath) throws IOException { - Objects.requireNonNull(resourcePath); + private boolean containsResource(String module, String name) throws IOException { + Objects.requireNonNull(name); if (closed) throw new IOException("ModuleReader is closed"); ImageReader imageReader = SystemImage.reader(); - if (imageReader != null) { - ImageReader.Node node = imageReader.findNode("/modules" + resourcePath); - return node != null && node.isResource(); - } else { - // not an images build - return false; - } + return imageReader != null && imageReader.containsResource(module, name); } @Override public Optional find(String name) throws IOException { - Objects.requireNonNull(name); - String resourcePath = "/" + module + "/" + name; - if (containsResource(resourcePath)) { - URI u = JNUA.create("jrt", resourcePath); + if (containsResource(module, name)) { + URI u = JNUA.create("jrt", "/" + module + "/" + name); return Optional.of(u); } else { return Optional.empty(); @@ -465,9 +457,7 @@ public final class SystemModuleFinders { if (closed) { throw new IOException("ModuleReader is closed"); } - String nodeName = "/modules/" + module + "/" + name; - ImageReader.Node node = reader.findNode(nodeName); - return (node != null && node.isResource()) ? node : null; + return reader.findResourceNode(module, name); } @Override diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java new file mode 100644 index 00000000000..89d15eb3439 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + + +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * Thrown when an operation on {@link QuicTLSEngine} doesn't have the necessary + * QUIC keys for encrypting or decrypting packets. This can either be because + * the keys aren't available for a particular {@linkplain KeySpace keyspace} or + * the keys for the {@code keyspace} have been discarded. + */ +public final class QuicKeyUnavailableException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 8553365136999153478L; + + public QuicKeyUnavailableException(final String message, final KeySpace keySpace) { + super(Objects.requireNonNull(keySpace) + " keyspace: " + message); + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java new file mode 100644 index 00000000000..fd0b405069c --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +/** + * Supplies contextual 1-RTT information that's available in the QUIC implementation of the + * {@code java.net.http} module, to the QUIC TLS layer in the {@code java.base} module. + */ +public interface QuicOneRttContext { + + /** + * {@return the largest packet number that was acknowledged by + * the peer in the 1-RTT packet space} + */ + long getLargestPeerAckedPN(); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java new file mode 100644 index 00000000000..5cf0c999fb9 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Objects; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.SSLParameters; + +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.SSLContextImpl; + +/** + * Instances of this class act as a factory for creation + * of {@link QuicTLSEngine QUIC TLS engine}. + */ +public final class QuicTLSContext { + + // In this implementation, we have a dependency on + // sun.security.ssl.SSLContextImpl. We can only support + // Quic on SSLContext instances created by the default + // SunJSSE Provider + private final SSLContextImpl sslCtxImpl; + + /** + * {@return {@code true} if the given {@code sslContext} supports QUIC TLS, {@code false} otherwise} + * @param sslContext an {@link SSLContext} + */ + public static boolean isQuicCompatible(final SSLContext sslContext) { + boolean parametersSupported = isQuicCompatible(sslContext.getSupportedSSLParameters()); + if (!parametersSupported) { + return false; + } + // horrible hack - what we do here is try and get hold of a SSLContext + // that has already been initialised and configured with the HttpClient. + // We see if that SSLContext is created using an implementation of + // sun.security.ssl.SSLContextImpl. Since there's no API + // available to get hold of that underlying implementation, we use + // MethodHandle lookup to get access to the field which holds that + // detail. + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + if (!(underlyingImpl instanceof SSLContextImpl ssci)) { + return false; + } + return ssci.isUsableWithQuic(); + } + + /** + * {@return {@code true} if protocols of the given {@code parameters} support QUIC TLS, {@code false} otherwise} + */ + public static boolean isQuicCompatible(SSLParameters parameters) { + String[] protocols = parameters.getProtocols(); + return protocols != null && Arrays.asList(protocols).contains("TLSv1.3"); + } + + private static SSLContextImpl getSSLContextImpl( + final SSLContext sslContext) { + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + assert underlyingImpl instanceof SSLContextImpl; + return (SSLContextImpl) underlyingImpl; + } + + /** + * Constructs a QuicTLSContext for the given {@code sslContext} + * + * @param sslContext The SSLContext + * @throws IllegalArgumentException If the passed {@code sslContext} isn't + * supported by the QuicTLSContext + * @see #isQuicCompatible(SSLContext) + */ + public QuicTLSContext(final SSLContext sslContext) { + Objects.requireNonNull(sslContext); + if (!isQuicCompatible(sslContext)) { + throw new IllegalArgumentException( + "Cannot construct a QUIC TLS context with the given SSLContext"); + } + this.sslCtxImpl = getSSLContextImpl(sslContext); + } + + /** + * Creates a {@link QuicTLSEngine} using this context + *

      + * This method does not provide hints for session caching. + * + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine() { + return createEngine(null, -1); + } + + /** + * Creates a {@link QuicTLSEngine} using this context using + * advisory peer information. + *

      + * The provided parameters will be used as hints for session caching. + * The {@code peerHost} parameter will be used in the server_name extension, + * unless overridden later. + * + * @param peerHost The peer hostname or IP address. Can be null. + * @param peerPort The peer port, can be -1 if the port is unknown + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine(final String peerHost, final int peerPort) { + return new QuicTLSEngineImpl(this.sslCtxImpl, peerHost, peerPort); + } + + // This VarHandle is used to access the SSLContext::contextSpi + // field which is not publicly accessible. + // In this implementation, Quic is only supported for SSLContext + // instances whose underlying implementation is provided by a + // sun.security.ssl.SSLContextImpl + private static final VarHandle CONTEXT_SPI; + static { + try { + final MethodHandles.Lookup lookup = + MethodHandles.privateLookupIn(SSLContext.class, + MethodHandles.lookup()); + final VarHandle vh = lookup.findVarHandle(SSLContext.class, + "contextSpi", SSLContextSpi.class); + CONTEXT_SPI = vh; + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} + diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java new file mode 100644 index 00000000000..70ed86bbf01 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.function.IntFunction; + +/** + * One instance of these per QUIC connection. Configuration methods not shown + * but would be similar to SSLEngine. + */ +public interface QuicTLSEngine { + + /** + * Represents the encryption level associated with a packet encryption or + * decryption. A QUIC connection has a current keyspace for sending and + * receiving which can be queried. + */ + enum KeySpace { + INITIAL, + HANDSHAKE, + RETRY, // Special algorithm used for this packet + ZERO_RTT, + ONE_RTT + } + + enum HandshakeState { + /** + * Need to receive a CRYPTO frame + */ + NEED_RECV_CRYPTO, + /** + * Need to receive a HANDSHAKE_DONE frame from server to complete the + * handshake, but application data can be sent in this state (client + * only state). + */ + NEED_RECV_HANDSHAKE_DONE, + /** + * Need to send a CRYPTO frame + */ + NEED_SEND_CRYPTO, + /** + * Need to send a HANDSHAKE_DONE frame to complete the handshake, but + * application data can be sent in this state (server only state) + */ + NEED_SEND_HANDSHAKE_DONE, + /** + * Need to execute a task + */ + NEED_TASK, + /** + * Handshake is confirmed, as specified in section 4.1.2 of RFC-9001 + */ + // On client side this happens when client receives HANDSHAKE_DONE + // frame. On server side this happens when the TLS stack has both + // sent a Finished message and verified the peer's Finished message. + HANDSHAKE_CONFIRMED, + } + + /** + * {@return the QUIC versions supported by this engine} + */ + Set getSupportedQuicVersions(); + + /** + * If {@code mode} is {@code true} then configures this QuicTLSEngine to + * operate in client mode. If {@code false}, then this QuicTLSEngine + * operates in server mode. + * + * @param mode true to make this QuicTLSEngine operate in client + * mode, false otherwise + */ + void setUseClientMode(boolean mode); + + /** + * {@return true if this QuicTLSEngine is operating in client mode, false + * otherwise} + */ + boolean getUseClientMode(); + + /** + * {@return the SSLParameters in effect for this engine.} + */ + SSLParameters getSSLParameters(); + + /** + * Sets the {@code SSLParameters} to be used by this engine + * + * @param sslParameters the SSLParameters + * @throws IllegalArgumentException if + * {@linkplain SSLParameters#getProtocols() TLS protocol versions} on the + * {@code sslParameters} is either empty or contains anything other + * than {@code TLSv1.3} + * @throws NullPointerException if {@code sslParameters} is null + */ + void setSSLParameters(SSLParameters sslParameters); + + /** + * {@return the most recent application protocol value negotiated by the + * engine. Returns null if no application protocol has yet been negotiated + * by the engine} + */ + String getApplicationProtocol(); + + /** + * {@return the SSLSession} + * + * @see SSLEngine#getSession() + */ + SSLSession getSession(); + + /** + * Returns the SSLSession being constructed during a QUIC handshake. + * + * @return null if this instance is not currently handshaking, or if the + * current handshake has not progressed far enough to create + * a basic SSLSession. Otherwise, this method returns the + * {@code SSLSession} currently being negotiated. + * + * @see SSLEngine#getHandshakeSession() + */ + SSLSession getHandshakeSession(); + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + HandshakeState getHandshakeState(); + + /** + * Returns true if the TLS handshake is considered complete. + *

      + * The TLS handshake is considered complete when the TLS stack + * has reported that the handshake is complete. This happens when + * the TLS stack has both sent a {@code Finished} message and verified + * the peer's {@code Finished} message. + * + * @return true if TLS handshake is complete, false otherwise. + */ + boolean isTLSHandshakeComplete(); + + /** + * {@return the current sending key space (encryption level)} + */ + KeySpace getCurrentSendKeySpace(); + + /** + * Checks whether the keys for the given key space are available. + *

      + * Keys are available when they are already computed and not discarded yet. + * + * @param keySpace key space to check + * @return true if the given keys are available + */ + boolean keysAvailable(KeySpace keySpace); + + /** + * Discard the keys used by the {@code keySpace}. + *

      + * Once the keys for a particular {@code keySpace} have been discarded, the + * keySpace will no longer be able to + * {@linkplain #encryptPacket(KeySpace, long, IntFunction, + * ByteBuffer, ByteBuffer) encrypt} or + * {@linkplain #decryptPacket(KeySpace, long, int, ByteBuffer, int, ByteBuffer) + * decrypt} packets. + * + * @param keySpace The keyspace whose current keys should be discarded + */ + void discardKeys(KeySpace keySpace); + + /** + * Provide quic_transport_parameters for inclusion in handshake message. + * + * @param params encoded quic_transport_parameters + */ + void setLocalQuicTransportParameters(ByteBuffer params); + + /** + * Reset the handshake state and produce a new ClientHello message. + * + * When a Quic client receives a Version Negotiation packet, + * it restarts the handshake by calling this method after updating the + * {@linkplain #setLocalQuicTransportParameters(ByteBuffer) transport parameters} + * with the new version information. + */ + void restartHandshake() throws IOException; + + /** + * Set consumer for quic_transport_parameters sent by the remote side. + * Consumer will receive a byte buffer containing the value of + * quic_transport_parameters extension sent by the remote endpoint. + * + * @param consumer consumer for remote quic transport parameters + */ + void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer); + + /** + * Derive initial keys for the given QUIC version and connection ID + * @param quicVersion QUIC protocol version + * @param connectionId initial destination connection ID + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this + * {@code QuicTLSEngine} + */ + void deriveInitialKeys(QuicVersion quicVersion, ByteBuffer connectionId) throws IOException; + + /** + * Get the sample size for header protection algorithm + * + * @param keySpace Packet key space + * @return required sample size for header protection + * @throws IllegalArgumentException when keySpace does not require + * header protection + */ + int getHeaderProtectionSampleSize(KeySpace keySpace); + + /** + * Compute the header protection mask for the given sample, + * packet key space and direction (incoming/outgoing). + * + * @param keySpace Packet key space + * @param incoming true for incoming packets, false for outgoing + * @param sample sampled data + * @return mask bytes, at least 5. + * @throws IllegalArgumentException when keySpace does not require + * header protection or sample length is different from required + * @see #getHeaderProtectionSampleSize(KeySpace) + * @spec https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati + * RFC 9001, Section 5.4.1 Header Protection Application + */ + ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Get the authentication tag size. Encryption adds this number of bytes. + * + * @return authentication tag size + */ + int getAuthTagSize(); + + /** + * Encrypt into {@code output}, the given {@code packetPayload} bytes using the + * keys for the given {@code keySpace}. + *

      + * Before encrypting the {@code packetPayload}, this method invokes the {@code headerGenerator} + * passing it the key phase corresponding to the encryption key that's in use. + * For {@code KeySpace}s where key phase isn't applicable, the {@code headerGenerator} will + * be invoked with a value of {@code 0} for the key phase. + *

      + * The {@code headerGenerator} is expected to return a {@code ByteBuffer} representing the + * packet header and where applicable, the returned header must contain the key phase + * that was passed to the {@code headerGenerator}. The packet header will be used as + * the Additional Authentication Data (AAD) for encrypting the {@code packetPayload}. + *

      + * Upon return, the {@code output} will contain the encrypted packet payload bytes + * and the authentication tag. The {@code packetPayload} and the packet header, returned + * by the {@code headerGenerator}, will have their {@code position} equal to their + * {@code limit}. The limit of either of those buffers will not have changed. + *

      + * It is recommended to do the encryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

      +     *          +--------+-------------------+
      +     * input:   | header | plaintext payload |
      +     *          +--------+-------------------+----------+
      +     * output:           | encrypted payload | AEAD tag |
      +     *                   +-------------------+----------+
      +     * 
      + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param headerGenerator an {@link IntFunction} which takes a key phase and returns + * the packet header + * @param packetPayload buffer containing unencrypted packet payload + * @param output buffer into which the encrypted packet payload will be written + * @throws QuicKeyUnavailableException if keys are not available + * @throws QuicTransportException if encrypting the packet would result + * in exceeding the AEAD cipher confidentiality limit + */ + void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, + ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException; + + /** + * Decrypt the given packet bytes using keys for the given packet key space. + * Header protection must be removed before calling this method. + *

      + * The input buffer contains the packet header and the encrypted packet payload. + * The packet header (first {@code headerLength} bytes of the input buffer) + * is consumed by this method, but is not decrypted. + * The packet payload (bytes following the packet header) is decrypted + * by this method. This method consumes the entire input buffer. + *

      + * The decrypted payload bytes are written + * to the output buffer. + *

      + * It is recommended to do the decryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

      +     *          +--------+-------------------+----------+
      +     * input:   | header | encrypted payload | AEAD tag |
      +     *          +--------+-------------------+----------+
      +     * output:           | decrypted payload |
      +     *                   +-------------------+
      +     * 
      + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param keyPhase key phase bit (0 or 1) found on the packet, or -1 + * if the packet does not have a key phase bit + * @param packet buffer containing encrypted packet bytes + * @param headerLength length of the packet header + * @param output buffer where decrypted packet bytes will be stored + * @throws IllegalArgumentException if keyPhase bit is invalid + * @throws QuicKeyUnavailableException if keys are not available + * @throws AEADBadTagException if the provided packet's authentication tag + * is incorrect + * @throws QuicTransportException if decrypting the invalid packet resulted + * in exceeding the AEAD cipher integrity limit + */ + void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws IllegalArgumentException, QuicKeyUnavailableException, + AEADBadTagException, QuicTransportException, ShortBufferException; + + /** + * Sign the provided retry packet. Input buffer contains the retry packet + * payload. Integrity tag is stored in the output buffer. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes without tag + * @param output buffer where integrity tag will be stored + * @throws ShortBufferException if output buffer is too short to + * hold the tag + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void signRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet, ByteBuffer output) throws ShortBufferException, QuicTransportException; + + /** + * Verify the provided retry packet. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes with tag + * @throws AEADBadTagException if integrity tag is invalid + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void verifyRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet) throws AEADBadTagException, QuicTransportException; + + /** + * If the current handshake state is {@link HandshakeState#NEED_SEND_CRYPTO} + * meaning that a CRYPTO frame needs to be sent then this method is called + * to obtain the contents of the frame. Current handshake state + * can be obtained from {@link #getHandshakeState()}, and the current + * key space can be obtained with {@link #getCurrentSendKeySpace()} + * The bytes returned by this call are used to build a CRYPTO frame. + * + * @param keySpace the key space of the packet in which the + * requested data will be placed + * @return buffer containing data that will be put by caller in a CRYPTO + * frame, or null if there are no more handshake bytes to send in + * this key space at this time. + */ + ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException; + + /** + * This method consumes crypto stream. + * + * @param keySpace the key space of the packet in which the provided + * crypto data was encountered. + * @param payload contents of the next CRYPTO frame + * @throws IllegalArgumentException if keySpace is ZERORTT or + * payload is empty + * @throws QuicTransportException if the handshake failed + */ + void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException; + + /** + * Returns a delegated {@code Runnable} task for + * this {@code QuicTLSEngine}. + *

      + * {@code QuicTLSEngine} operations may require the results of + * operations that block, or may take an extended period of time to + * complete. This method is used to obtain an outstanding {@link + * java.lang.Runnable} operation (task). Each task must be assigned + * a thread (possibly the current) to perform the {@link + * java.lang.Runnable#run() run} operation. Once the + * {@code run} method returns, the {@code Runnable} object + * is no longer needed and may be discarded. + *

      + * A call to this method will return each outstanding task + * exactly once. + *

      + * Multiple delegated tasks can be run in parallel. + * + * @return a delegated {@code Runnable} task, or null + * if none are available. + */ + Runnable getDelegatedTask(); + + /** + * Called to check if a {@code HANDSHAKE_DONE} frame needs to be sent by the + * server. This method will only be called for a {@code QuicTLSEngine} which + * is in {@linkplain #getUseClientMode() server mode}. If the current TLS handshake + * state is + * {@link HandshakeState#NEED_SEND_HANDSHAKE_DONE + * NEED_SEND_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_SEND_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException If this {@code QuicTLSEngine} is + * not in server mode + */ + boolean tryMarkHandshakeDone() throws IllegalStateException; + + /** + * Called when HANDSHAKE_DONE message is received from the server. This + * method will only be called for a {@code QuicTLSEngine} which is in + * {@linkplain #getUseClientMode() client mode}. If the current TLS handshake state + * is + * {@link HandshakeState#NEED_RECV_HANDSHAKE_DONE + * NEED_RECV_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_RECV_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException if this {@code QuicTLSEngine} is + * not in client mode + */ + boolean tryReceiveHandshakeDone() throws IllegalStateException; + + /** + * Called when the client and the server, during the connection creation + * handshake, have settled on a Quic version to use for the connection. This + * can happen either due to an explicit version negotiation (as outlined in + * Quic RFC) or the server accepting the Quic version that the client chose + * in its first INITIAL packet. In either of those cases, this method will + * be called. + * + * @param quicVersion the negotiated {@code QuicVersion} + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this engine + */ + void versionNegotiated(QuicVersion quicVersion); + + /** + * Sets the {@link QuicOneRttContext} on the {@code QuicTLSEngine}. + *

      The {@code ctx} will be used by the {@code QuicTLSEngine} to access contextual 1-RTT + * data that might be required for the TLS operations. + * + * @param ctx the 1-RTT context to set + * @throws NullPointerException if {@code ctx} is null + */ + void setOneRttContext(QuicOneRttContext ctx); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java new file mode 100644 index 00000000000..d081458d40e --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +import sun.security.ssl.Alert; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * An enum to model Quic transport errors. + * Some errors have a single possible code value, some, like + * {@link #CRYPTO_ERROR} have a range of possible values. + * Usually, the value (a long) would be used instead of the + * enum, but the enum itself can be useful - for instance in + * switch statements. + * This enum models QUIC transport error codes as defined in + * RFC 9000, section 20.1. + */ +public enum QuicTransportErrors { + /** + * No error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint uses this with CONNECTION_CLOSE to signal that
      +     * the connection is being closed abruptly in the absence
      +     * of any error.
      +     * }
      + */ + NO_ERROR(0x00), + + /** + * Internal Error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * The endpoint encountered an internal error and cannot
      +     * continue with the connection.
      +     * }
      + */ + INTERNAL_ERROR(0x01), + + /** + * Connection refused error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * The server refused to accept a new connection.
      +     * }
      + */ + CONNECTION_REFUSED(0x02), + + /** + * Flow control error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint received more data than it permitted in its advertised data limits;
      +     * see Section 4.
      +     * }
      + * @see + * RFC 9000, Section 20.1: + *
      {@code
      +     * An endpoint received a frame for a stream identifier that exceeded its advertised
      +     * stream limit for the corresponding stream type.
      +     * }
      + */ + STREAM_LIMIT_ERROR(0x04), + + /** + * Stream state error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint received a frame for a stream that was not in a state that permitted
      +     * that frame; see Section 3.
      +     * }
      + * @see + * RFC 9000, Section 20.1: + *
      {@code
      +     * (1) An endpoint received a STREAM frame containing data that exceeded the previously
      +     *     established final size,
      +     * (2) an endpoint received a STREAM frame or a RESET_STREAM frame containing a final
      +     *     size that was lower than the size of stream data that was already received, or
      +     * (3) an endpoint received a STREAM frame or a RESET_STREAM frame containing a
      +     *     different final size to the one already established.
      +     * }
      + */ + FINAL_SIZE_ERROR(0x06), + + /** + * Frame encoding error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint received a frame that was badly formatted -- for instance,
      +     * a frame of an unknown type or an ACK frame that has more
      +     * acknowledgment ranges than the remainder of the packet could carry.
      +     * }
      + */ + FRAME_ENCODING_ERROR(0x07), + + /** + * Transport parameter error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint received transport parameters that were badly
      +     * formatted, included an invalid value, omitted a mandatory
      +     * transport parameter, included a forbidden transport
      +     * parameter, or were otherwise in error.
      +     * }
      + */ + TRANSPORT_PARAMETER_ERROR(0x08), + + /** + * Connection id limit error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * The number of connection IDs provided by the peer exceeds
      +     * the advertised active_connection_id_limit.
      +     * }
      + */ + CONNECTION_ID_LIMIT_ERROR(0x09), + + /** + * Protocol violiation error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint detected an error with protocol compliance that
      +     * was not covered by more specific error codes.
      +     * }
      + */ + PROTOCOL_VIOLATION(0x0a), + + /** + * Invalid token error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * A server received a client Initial that contained an invalid Token field.
      +     * }
      + */ + INVALID_TOKEN(0x0b), + + /** + * Application error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * The application or application protocol caused the connection to be closed.
      +     * }
      + */ + APPLICATION_ERROR(0x0c), + + /** + * Crypto buffer exceeded error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint has received more data in CRYPTO frames than it can buffer.
      +     * }
      + */ + CRYPTO_BUFFER_EXCEEDED(0x0d), + + /** + * Key update error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint detected errors in performing key updates; see Section 6 of [QUIC-TLS].
      +     * }
      + * @see Section 6 of RFC 9001 [QUIC-TLS] + */ + KEY_UPDATE_ERROR(0x0e), + + /** + * AEAD limit reached error + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint has reached the confidentiality or integrity limit
      +     * for the AEAD algorithm used by the given connection.
      +     * }
      + */ + AEAD_LIMIT_REACHED(0x0f), + + /** + * No viable path error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * An endpoint has determined that the network path is incapable of
      +     * supporting QUIC. An endpoint is unlikely to receive a
      +     * CONNECTION_CLOSE frame carrying this code except when the
      +     * path does not support a large enough MTU.
      +     * }
      + */ + NO_VIABLE_PATH(0x10), + + /** + * Error negotiating version. + * @spec https://www.rfc-editor.org/rfc/rfc9368#name-version-downgrade-preventio + * RFC 9368, Section 4 + */ + VERSION_NEGOTIATION_ERROR(0x11), + + /** + * Crypto error. + *

      + * From + * RFC 9000, Section 20.1: + *

      {@code
      +     * The cryptographic handshake failed. A range of 256 values is
      +     * reserved for carrying error codes specific to the cryptographic
      +     * handshake that is used. Codes for errors occurring when
      +     * TLS is used for the cryptographic handshake are described
      +     * in Section 4.8 of [QUIC-TLS].
      +     * }
      + * @see Section 4.8 of RFC 9001 [QUIC-TLS] + */ + CRYPTO_ERROR(0x0100, 0x01ff); + + private final long from; + private final long to; + + QuicTransportErrors(long code) { + this(code, code); + } + + QuicTransportErrors(long from, long to) { + assert from <= to; + this.from = from; + this.to = to; + } + + /** + * {@return the code for this transport error, if this error + * {@linkplain #hasCode() has a single possible code value}, + * {@code -1} otherwise} + */ + public long code() { return hasCode() ? from : -1;} + + /** + * {@return true if this error has a single possible code value} + */ + public boolean hasCode() { return from == to; } + + /** + * {@return true if this error has a range of possible code values} + */ + public boolean hasRange() { return from < to;} + + /** + * {@return the first possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long from() {return from;} + + /** + * {@return the last possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long to() { return to; } + + /** + * Tells whether the given {@code code} value corresponds to + * this error. + * @param code an error code value + * @return true if the given {@code code} value corresponds to + * this error. + */ + boolean isFor(long code) { + return code >= from && code <= to; + } + + /** + * {@return the {@link QuicTransportErrors} instance corresponding + * to the given {@code code} value, if any} + * @param code a {@code code} value + */ + public static Optional ofCode(long code) { + return Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + } + + public static String toString(long code) { + Optional c = Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + if (c.isEmpty()) return "Unknown [0x"+Long.toHexString(code) + "]"; + if (c.get().hasCode()) return c.get().toString(); + if (c.get() == CRYPTO_ERROR) + return c.get() + "|" + Alert.nameOf((byte)code); + return c.get() + " [0x" + Long.toHexString(code) + "]"; + + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java new file mode 100644 index 00000000000..3341cc527f2 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +/** + * Exception that wraps QUIC transport error codes. + * Thrown in response to packets or frames that violate QUIC protocol. + * This is a fatal exception; connection is always closed when this exception is caught. + * + *

      For a list of errors see: + * https://www.rfc-editor.org/rfc/rfc9000.html#name-transport-error-codes + */ +public final class QuicTransportException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 5259674758792412464L; + + private final QuicTLSEngine.KeySpace keySpace; + private final long frameType; + private final long errorCode; + + /** + * Constructs a new {@code QuicTransportException}. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, QuicTransportErrors errorCode) { + super(reason); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode.code(); + } + + /** + * Constructs a new {@code QuicTransportException}. For use with TLS alerts. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error code + * @param cause the cause + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, long errorCode, Throwable cause) { + super(reason, cause); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode; + } + + /** + * {@return the reason to include in the {@code ConnectionCloseFrame}} + */ + public String getReason() { + return getMessage(); + } + + /** + * {@return the key space for which the error occurred, or {@code null}} + */ + public QuicTLSEngine.KeySpace getKeySpace() { + return keySpace; + } + + /** + * {@return the frame type for which the error occurred, or 0} + */ + public long getFrameType() { + return frameType; + } + + /** + * {@return the transport error that occurred} + */ + public long getErrorCode() { + return errorCode; + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java new file mode 100644 index 00000000000..9429f6bf26f --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +import java.nio.ByteBuffer; + +/** + * Interface for consumer of QUIC transport parameters, in wire-encoded format + */ +public interface QuicTransportParametersConsumer { + /** + * Consumes the provided QUIC transport parameters + * @param buffer byte buffer containing encoded quic transport parameters + * @throws QuicTransportException if buffer does not represent valid parameters + */ + void accept(ByteBuffer buffer) throws QuicTransportException; +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java new file mode 100644 index 00000000000..14bbaba816d --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.quic; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the Quic versions defined in their corresponding RFCs + */ +public enum QuicVersion { + // the version numbers are defined in their respective RFCs + QUIC_V1(1), // RFC-9000 + QUIC_V2(0x6b3343cf); // RFC 9369 + + // 32 bits unsigned integer representing the version as + // defined in RFC. This is the version number as sent + // in long headers packets (see RFC 9000). + private final int versionNumber; + + private QuicVersion(final int versionNumber) { + this.versionNumber = versionNumber; + } + + /** + * {@return the version number} + */ + public int versionNumber() { + return this.versionNumber; + } + + /** + * {@return the QuicVersion corresponding to the {@code versionNumber} or + * {@link Optional#empty() an empty Optional} if the {@code versionNumber} + * doesn't correspond to a Quic version} + * + * @param versionNumber The version number + */ + public static Optional of(int versionNumber) { + for (QuicVersion qv : QuicVersion.values()) { + if (qv.versionNumber == versionNumber) { + return Optional.of(qv); + } + } + return Optional.empty(); + } + + /** + * From among the {@code quicVersions}, selects a {@code QuicVersion} to be used in the + * first packet during connection initiation. + * + * @param quicVersions the available QUIC versions + * @return the QUIC version to use in the first packet + * @throws NullPointerException if {@code quicVersions} is null or any element + * in it is null + * @throws IllegalArgumentException if {@code quicVersions} is empty + */ + public static QuicVersion firstFlightVersion(final Collection quicVersions) { + Objects.requireNonNull(quicVersions); + if (quicVersions.isEmpty()) { + throw new IllegalArgumentException("Empty quic versions"); + } + if (quicVersions.size() == 1) { + return quicVersions.iterator().next(); + } + for (final QuicVersion version : quicVersions) { + if (version == QUIC_V1) { + // we always prefer QUIC v1 for first flight version + return QUIC_V1; + } + } + // the given versions did not have QUIC v1, which implies the + // only available first flight version is QUIC v2 + return QUIC_V2; + } +} diff --git a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java index e8a4f27796f..46885e12adf 100644 --- a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java +++ b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -63,12 +64,12 @@ public abstract class ModifiedUtf { * @param countNonZeroAscii the number of non-zero ascii characters in the prefix calculated by JLA.countNonZeroAscii(str) */ @ForceInline - public static int utfLen(String str, int countNonZeroAscii) { - int utflen = str.length(); - for (int i = utflen - 1; i >= countNonZeroAscii; i--) { + public static long utfLen(String str, int countNonZeroAscii) { + long utflen = str.length(); + for (int i = (int)utflen - 1; i >= countNonZeroAscii; i--) { int c = str.charAt(i); if (c >= 0x80 || c == 0) - utflen += (c >= 0x800) ? 2 : 1; + utflen += (c >= 0x800) ? 2L : 1L; } return utflen; } @@ -90,8 +91,7 @@ public abstract class ModifiedUtf { return false; } // Check exact Modified UTF-8 length. - // The check strLen > CONSTANT_POOL_UTF8_MAX_BYTES above ensures that utfLen can't overflow here. - int utfLen = utfLen(str, 0); + long utfLen = utfLen(str, 0); return utfLen <= CONSTANT_POOL_UTF8_MAX_BYTES; } } diff --git a/src/java.base/share/classes/jdk/internal/vm/Continuation.java b/src/java.base/share/classes/jdk/internal/vm/Continuation.java index a97f9ac9ea4..a7eb3ea6a9f 100644 --- a/src/java.base/share/classes/jdk/internal/vm/Continuation.java +++ b/src/java.base/share/classes/jdk/internal/vm/Continuation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -57,7 +57,6 @@ public class Continuation { /** Reason for pinning */ public enum Pinned { /** Native frame on stack */ NATIVE, - /** Monitor held */ MONITOR, /** In critical section */ CRITICAL_SECTION, /** Exception (OOME/SOE) */ EXCEPTION } @@ -69,8 +68,7 @@ public class Continuation { /** Permanent failure: continuation already yielding */ PERM_FAIL_YIELDING(null), /** Permanent failure: continuation not mounted on the thread */ PERM_FAIL_NOT_MOUNTED(null), /** Transient failure: continuation pinned due to a held CS */ TRANSIENT_FAIL_PINNED_CRITICAL_SECTION(Pinned.CRITICAL_SECTION), - /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE), - /** Transient failure: continuation pinned due to a held monitor */ TRANSIENT_FAIL_PINNED_MONITOR(Pinned.MONITOR); + /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE); final Pinned pinned; private PreemptStatus(Pinned reason) { this.pinned = reason; } @@ -85,8 +83,7 @@ public class Continuation { return switch (reason) { case 2 -> Pinned.CRITICAL_SECTION; case 3 -> Pinned.NATIVE; - case 4 -> Pinned.MONITOR; - case 5 -> Pinned.EXCEPTION; + case 4 -> Pinned.EXCEPTION; default -> throw new AssertionError("Unknown pinned reason: " + reason); }; } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index a26003a3afb..276c379a564 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -205,7 +205,10 @@ public class ThreadDumper { // park blocker Object parkBlocker = snapshot.parkBlocker(); if (parkBlocker != null) { - writer.println(" - parking to wait for " + decorateObject(parkBlocker)); + String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner) + ? ", owner #" + owner.threadId() + : ""; + writer.println(" - parking to wait for " + decorateObject(parkBlocker) + suffix); } // blocked on monitor enter or Object.wait @@ -335,6 +338,9 @@ public class ThreadDumper { // parkBlocker is an object to allow for exclusiveOwnerThread in the future jsonWriter.startObject("parkBlocker"); jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); + if (snapshot.parkBlockerOwner() instanceof Thread owner) { + jsonWriter.writeProperty("owner", owner.threadId()); + } jsonWriter.endObject(); } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java index 4fcbaf24d2e..357d38008d1 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java @@ -44,6 +44,8 @@ class ThreadSnapshot { // an object the thread is blocked/waiting on, converted to ThreadBlocker by ThreadSnapshot.of() private int blockerTypeOrdinal; private Object blockerObject; + // the owner of the blockerObject when the object is park blocker and is AbstractOwnableSynchronizer + private Thread parkBlockerOwner; // set by ThreadSnapshot.of() private ThreadBlocker blocker; @@ -70,8 +72,11 @@ class ThreadSnapshot { snapshot.locks = EMPTY_LOCKS; } if (snapshot.blockerObject != null) { - snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, snapshot.blockerObject); + snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, + snapshot.blockerObject, + snapshot.parkBlockerOwner); snapshot.blockerObject = null; // release + snapshot.parkBlockerOwner = null; } return snapshot; } @@ -104,6 +109,13 @@ class ThreadSnapshot { return getBlocker(BlockerLockType.PARK_BLOCKER); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + Thread parkBlockerOwner() { + return (blocker != null && blocker.type == BlockerLockType.PARK_BLOCKER) ? blocker.owner : null; + } + /** * Returns the object that the thread is blocked on. * @throws IllegalStateException if not in the blocked state @@ -211,11 +223,11 @@ class ThreadSnapshot { } } - private record ThreadBlocker(BlockerLockType type, Object obj) { + private record ThreadBlocker(BlockerLockType type, Object obj, Thread owner) { private static final BlockerLockType[] lockTypeValues = BlockerLockType.values(); // cache - ThreadBlocker(int typeOrdinal, Object obj) { - this(lockTypeValues[typeOrdinal], obj); + ThreadBlocker(int typeOrdinal, Object obj, Thread owner) { + this(lockTypeValues[typeOrdinal], obj, owner); } } diff --git a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java index 3c2dab3209a..1305a7b6b88 100644 --- a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java +++ b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java @@ -30,25 +30,34 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/// Indicates that the static initializer of this class or interface -/// (its `` method) is allowed to be _AOT-initialized_, -/// because its author considers it safe to execute during the AOT -/// assembly phase. +/// Indicates that the annotated class or interface is allowed to be _AOT-initialized_, +/// because its author considers it safe to execute the static initializer of +/// the class or interface during the AOT assembly phase. /// -/// This annotation directs the VM to expect that normal execution of Java code -/// during the assembly phase could trigger initialization of this class, -/// and if that happens, to store the resulting static field values in the -/// AOT cache. (These fields happen to be allocated in the `Class` mirror.) +/// For a class or interface _X_ annotated with `@AOTSafeClassInitializer`, it will +/// be initialized in the AOT assembly phase under two circumstances: /// -/// During the production run, the static initializer (``) of -/// this class or interface will not be executed, if it was already -/// executed during the assembling of the AOT being used to start the -/// production run. In that case the resulting static field states -/// (within the `Class` mirror) were already stored in the AOT cache. +/// 1. If _X_ was initialized during the AOT training run, the JVM will proactively +/// initialize _X_ in the assembly phase. +/// 2. If _X_ was not initialized during the AOT training run, the initialization of +/// _X_ can still be triggered by normal execution of Java code in the assembly +/// phase. At present this is usually the result of performing AOT optimizations for +/// the `java.lang.invoke` package but it may include other cases as well. /// -/// Currently, this annotation is used mainly for supporting AOT -/// linking of APIs, including bootstrap methods, in the -/// `java.lang.invoke` package. +/// If _X_ is initialized during the AOT assembly phase, the VM will store +/// the values of the static fields of _X_ in the AOT cache. Consequently, +/// during the production run that uses this AOT cache, the static initializer +/// (``) of _X_ will not be executed. _X_ will appear to be in the +/// "initialized" state and all the cached values of the static field of _X_ +/// will be available immediately upon the start of the prodcution run. +/// +/// Currently, this annotation is used mainly for two purposes: +/// +/// - To AOT-initialize complex static fields whose values are always the same +/// across JVM lifetimes. One example is the tables of constant values +/// in the `jdk.internal.math.MathUtils` class. +/// - To support AOT linking of APIs, including bootstrap methods, in the +/// `java.lang.invoke` package. /// /// In more detail, the AOT assembly phase performs the following: /// @@ -62,6 +71,8 @@ import java.lang.annotation.Target; /// along with every relevant superclass and implemented interface, along /// with classes for every object created during the course of static /// initialization (running `` for each such class or interface). +/// 5. In addition, any class/interface annotated with `@AOTSafeClassInitializer` +/// that was initialized during the training run is proactively initialized. /// /// Thus, in order to determine that a class or interface _X_ is safe to /// AOT-initialize requires evaluating every other class or interface _Y_ that @@ -112,21 +123,18 @@ import java.lang.annotation.Target; /// remotely) if the execution of such an API touches _X_ for initialization, /// or even if such an API request is in any way sensitive to values stored in /// the fields of _X_, even if the sensitivity is a simple reference identity -/// test. As noted above, all supertypes of _X_ must also have the -/// `@AOTSafeClassInitializer` annotation, and must also be safe for AOT -/// initialization. +/// test. /// /// The author of an AOT-initialized class may elect to patch some states at /// production startup, using an [AOTRuntimeSetup] method, as long as the /// pre-patched field values (present during AOT assembly) are determined to be /// compatible with the post-patched values that apply to the production run. /// -/// In the assembly phase, `classFileParser.cpp` performs checks on the annotated -/// classes, to ensure all supertypes of this class that must be initialized when -/// this class is initialized have the `@AOTSafeClassInitializer` annotation. -/// Otherwise, a [ClassFormatError] will be thrown. (This assembly phase restriction -/// allows module patching and instrumentation to work on annotated classes when -/// AOT is not enabled) +/// Before adding this annotation to a class _X_, the author must determine +/// that it's safe to execute the static initializer of _X_ during the AOT +/// assembly phase. In addition, all supertypes of _X_ must also have this +/// annotation. If a supertype of _X_ is found to be missing this annotation, +/// the AOT assembly phase will fail. /// /// This annotation is only recognized on privileged code and is ignored elsewhere. /// diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index 2a51a0af38d..3ae84fdf198 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -190,6 +190,8 @@ module java.base { jdk.jlink; exports jdk.internal.logger to java.logging; + exports jdk.internal.net.quic to + java.net.http; exports jdk.internal.org.xml.sax to jdk.jfr; exports jdk.internal.org.xml.sax.helpers to @@ -260,6 +262,7 @@ module java.base { jdk.jfr; exports jdk.internal.util to java.desktop, + java.net.http, java.prefs, java.security.jgss, java.smartcardio, diff --git a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java index 76bbff2a610..bd5ea6d7635 100644 --- a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java +++ b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java @@ -37,12 +37,33 @@ public class BytecodeDescriptor { private BytecodeDescriptor() { } // cannot instantiate - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ - public static List> parseMethod(String bytecodeSignature, ClassLoader loader) { - return parseMethod(bytecodeSignature, 0, bytecodeSignature.length(), loader); + /// Parses and validates a field descriptor string in the {@code loader} context. + /// + /// @param descriptor a field descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if the descriptor is valid, but + /// the class cannot be found by the loader + public static Class parseClass(String descriptor, ClassLoader loader) { + int[] i = {0}; + var ret = parseSig(descriptor, i, descriptor.length(), loader); + if (i[0] != descriptor.length() || ret == null) { + parseError("not a class descriptor", descriptor); + } + return ret; + } + + /// Parses and validates a method descriptor string in the {@code loader} context. + /// + /// @param descriptor a method descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if a reference type cannot be found by + /// the loader (before the descriptor is found invalid) + public static List> parseMethod(String descriptor, ClassLoader loader) { + return parseMethod(descriptor, 0, descriptor.length(), loader); } /** @@ -77,10 +98,19 @@ public class BytecodeDescriptor { throw new IllegalArgumentException("bad signature: "+str+": "+msg); } - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ + /// Parse a single type in a descriptor. Results can be: + /// + /// - A `Class` for successful parsing + /// - `null` for malformed descriptor format + /// - Throwing a [TypeNotPresentException] for valid class name, + /// but class cannot be found + /// + /// @param str contains the string to parse + /// @param i cursor for the next token in the string, modified in-place + /// @param end the limit for parsing + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// private static Class parseSig(String str, int[] i, int end, ClassLoader loader) { if (i[0] == end) return null; char c = str.charAt(i[0]++); @@ -107,7 +137,14 @@ public class BytecodeDescriptor { } return t; } else { - return Wrapper.forBasicType(c).primitiveType(); + Wrapper w; + try { + w = Wrapper.forBasicType(c); + } catch (IllegalArgumentException ex) { + // Our reporting has better error message + return null; + } + return w.primitiveType(); } } diff --git a/src/java.base/share/classes/sun/launcher/SecuritySettings.java b/src/java.base/share/classes/sun/launcher/SecuritySettings.java index afd5ead4914..e7ec70afcc3 100644 --- a/src/java.base/share/classes/sun/launcher/SecuritySettings.java +++ b/src/java.base/share/classes/sun/launcher/SecuritySettings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, 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 @@ -136,7 +136,18 @@ public final class SecuritySettings { for (String s : ssls.getEnabledCipherSuites()) { ostream.println(THREEINDENT + s); } + + ostream.println("\n" + TWOINDENT + "Enabled Named Groups:"); + String [] groups = ssls.getSSLParameters().getNamedGroups(); + if (groups == null) { + ostream.println(THREEINDENT + ""); + } else { + for (String s : groups) { + ostream.println(THREEINDENT + s); + } + } } + ostream.println(); } diff --git a/src/java.base/share/classes/sun/net/www/URLConnection.java b/src/java.base/share/classes/sun/net/www/URLConnection.java index 66005ab9b2a..becbf88da73 100644 --- a/src/java.base/share/classes/sun/net/www/URLConnection.java +++ b/src/java.base/share/classes/sun/net/www/URLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, 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 @@ -101,9 +101,21 @@ public abstract class URLConnection extends java.net.URLConnection { return Collections.emptyMap(); } + /** + * This method is called whenever the headers related methods are called on the + * {@code URLConnection}. This method does any necessary checks and initializations + * to make sure that the headers can be served. If this {@code URLConnection} cannot + * serve the headers, then this method throws an {@code IOException}. + * + * @throws IOException if the headers cannot be served + */ + protected void ensureCanServeHeaders() throws IOException { + getInputStream(); + } + public String getHeaderField(String name) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -111,13 +123,13 @@ public abstract class URLConnection extends java.net.URLConnection { } - Map> headerFields; + private Map> headerFields; @Override public Map> getHeaderFields() { if (headerFields == null) { try { - getInputStream(); + ensureCanServeHeaders(); if (properties == null) { headerFields = super.getHeaderFields(); } else { @@ -137,7 +149,7 @@ public abstract class URLConnection extends java.net.URLConnection { */ public String getHeaderFieldKey(int n) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -152,7 +164,7 @@ public abstract class URLConnection extends java.net.URLConnection { */ public String getHeaderField(int n) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -221,7 +233,7 @@ public abstract class URLConnection extends java.net.URLConnection { */ public int getContentLength() { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return -1; } diff --git a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java index fc947f8977f..8330c2b9b80 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java @@ -25,15 +25,30 @@ package sun.net.www.protocol.file; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.FileNameMap; import java.net.MalformedURLException; import java.net.URL; -import java.net.FileNameMap; -import java.io.*; -import java.text.Collator; import java.security.Permission; -import sun.net.www.*; -import java.util.*; +import java.text.Collator; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import sun.net.www.MessageHeader; +import sun.net.www.ParseUtil; +import sun.net.www.URLConnection; /** * Open a file input stream given a URL. @@ -67,25 +82,49 @@ public class FileURLConnection extends URLConnection { this.file = file; } - /* + /** + * If already connected, then this method is a no-op. + * If not already connected, then this method does + * readability checks for the File. + *

      + * If the File is a directory then the readability check + * is done by verifying that File.list() does not return + * null. On the other hand, if the File is not a directory, + * then this method constructs a temporary FileInputStream + * for the File and lets the FileInputStream's constructor + * implementation do the necessary readability checks. + * That temporary FileInputStream is closed before returning + * from this method. + *

      + * In either case, if the readability checks fail, then + * an IOException is thrown from this method and the + * FileURLConnection stays unconnected. + *

      + * A normal return from this method implies that the + * FileURLConnection is connected and the readability + * checks have passed for the File. + *

      * Note: the semantics of FileURLConnection object is that the * results of the various URLConnection calls, such as * getContentType, getInputStream or getContentLength reflect * whatever was true when connect was called. */ + @Override public void connect() throws IOException { if (!connected) { - isDirectory = file.isDirectory(); + // verify readability of the directory or the regular file if (isDirectory) { String[] fileList = file.list(); - if (fileList == null) + if (fileList == null) { throw new FileNotFoundException(file.getPath() + " exists, but is not accessible"); + } directoryListing = Arrays.asList(fileList); } else { - is = new BufferedInputStream(new FileInputStream(file.getPath())); + // let FileInputStream constructor do the necessary readability checks + // and propagate any failures + new FileInputStream(file.getPath()).close(); } - connected = true; } } @@ -112,9 +151,9 @@ public class FileURLConnection extends URLConnection { FileNameMap map = java.net.URLConnection.getFileNameMap(); String contentType = map.getContentTypeFor(file.getPath()); if (contentType != null) { - properties.add(CONTENT_TYPE, contentType); + properties.set(CONTENT_TYPE, contentType); } - properties.add(CONTENT_LENGTH, Long.toString(length)); + properties.set(CONTENT_LENGTH, Long.toString(length)); /* * Format the last-modified field into the preferred @@ -126,30 +165,34 @@ public class FileURLConnection extends URLConnection { SimpleDateFormat fo = new SimpleDateFormat ("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); fo.setTimeZone(TimeZone.getTimeZone("GMT")); - properties.add(LAST_MODIFIED, fo.format(date)); + properties.set(LAST_MODIFIED, fo.format(date)); } } else { - properties.add(CONTENT_TYPE, TEXT_PLAIN); + properties.set(CONTENT_TYPE, TEXT_PLAIN); } initializedHeaders = true; } } - public Map> getHeaderFields() { + @Override + public Map> getHeaderFields() { initializeHeaders(); return super.getHeaderFields(); } + @Override public String getHeaderField(String name) { initializeHeaders(); return super.getHeaderField(name); } + @Override public String getHeaderField(int n) { initializeHeaders(); return super.getHeaderField(n); } + @Override public int getContentLength() { initializeHeaders(); if (length > Integer.MAX_VALUE) @@ -157,54 +200,74 @@ public class FileURLConnection extends URLConnection { return (int) length; } + @Override public long getContentLengthLong() { initializeHeaders(); return length; } + @Override public String getHeaderFieldKey(int n) { initializeHeaders(); return super.getHeaderFieldKey(n); } + @Override public MessageHeader getProperties() { initializeHeaders(); return super.getProperties(); } + @Override public long getLastModified() { initializeHeaders(); return lastModified; } + @Override public synchronized InputStream getInputStream() throws IOException { connect(); + // connect() does the necessary readability checks and is expected to + // throw IOException if any of those checks fail. A normal completion of connect() + // must mean that connect succeeded. + assert connected : "not connected"; - if (is == null) { - if (isDirectory) { + // a FileURLConnection only ever creates and provides a single InputStream + if (is != null) { + return is; + } - if (directoryListing == null) { - throw new FileNotFoundException(file.getPath()); - } + if (isDirectory) { + // a successful connect() implies the directoryListing is non-null + // if the file is a directory + assert directoryListing != null : "missing directory listing"; - directoryListing.sort(Collator.getInstance()); + directoryListing.sort(Collator.getInstance()); - StringBuilder sb = new StringBuilder(); - for (String fileName : directoryListing) { - sb.append(fileName); - sb.append("\n"); - } - // Put it into a (default) locale-specific byte-stream. - is = new ByteArrayInputStream(sb.toString().getBytes()); - } else { - throw new FileNotFoundException(file.getPath()); + StringBuilder sb = new StringBuilder(); + for (String fileName : directoryListing) { + sb.append(fileName); + sb.append("\n"); } + // Put it into a (default) locale-specific byte-stream. + is = new ByteArrayInputStream(sb.toString().getBytes()); + } else { + is = new BufferedInputStream(new FileInputStream(file.getPath())); } return is; } + @Override + protected synchronized void ensureCanServeHeaders() throws IOException { + // connect() (if not already connected) does the readability checks + // and throws an IOException if those checks fail. A successful + // completion from connect() implies the File is readable. + connect(); + } + + Permission permission; /* since getOutputStream isn't supported, only read permission is diff --git a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java index 20b735fbdf3..71080950b80 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java @@ -87,8 +87,8 @@ public class JavaRuntimeURLConnection extends URLConnection { if (module.isEmpty() || path == null) { throw new IOException("cannot connect to jrt:/" + module); } - Node node = READER.findNode("/modules/" + module + "/" + path); - if (node == null || !node.isResource()) { + Node node = READER.findResourceNode(module, path); + if (node == null) { throw new IOException(module + "/" + path + " not found"); } this.resourceNode = node; diff --git a/src/java.base/share/classes/sun/nio/ByteBuffered.java b/src/java.base/share/classes/sun/nio/ByteBuffered.java deleted file mode 100644 index a547cefeb89..00000000000 --- a/src/java.base/share/classes/sun/nio/ByteBuffered.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2003, 2020, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ - -package sun.nio; - -import java.nio.ByteBuffer; -import java.io.IOException; - -/** - * This is an interface to adapt existing APIs to use {@link java.nio.ByteBuffer - * ByteBuffers} as the underlying data format. Only the initial producer and - * final consumer have to be changed. - * - *

      - * For example, the Zip/Jar code supports {@link java.io.InputStream InputStreams}. - * To make the Zip code use {@link java.nio.MappedByteBuffer MappedByteBuffers} as - * the underlying data structure, it can create a class of InputStream that wraps - * the ByteBuffer, and implements the ByteBuffered interface. A co-operating class - * several layers away can ask the InputStream if it is an instance of ByteBuffered, - * then call the {@link #getByteBuffer()} method. - */ -public interface ByteBuffered { - - /** - * Returns the {@code ByteBuffer} behind this object, if this particular - * instance has one. An implementation of {@code getByteBuffer()} is allowed - * to return {@code null} for any reason. - * - * @return The {@code ByteBuffer}, if this particular instance has one, - * or {@code null} otherwise. - * - * @throws IOException - * If the ByteBuffer is no longer valid. - * - * @since 1.5 - */ - public ByteBuffer getByteBuffer() throws IOException; -} diff --git a/src/java.base/share/classes/sun/nio/ch/Interruptible.java b/src/java.base/share/classes/sun/nio/ch/Interruptible.java index b5d9a7d2b3f..25f762a1d6a 100644 --- a/src/java.base/share/classes/sun/nio/ch/Interruptible.java +++ b/src/java.base/share/classes/sun/nio/ch/Interruptible.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -35,14 +35,14 @@ public interface Interruptible { * Invoked by Thread.interrupt when the given Thread is interrupted. Thread.interrupt * invokes this method while holding the given Thread's interrupt lock. This method * is also invoked by AbstractInterruptibleChannel when beginning an I/O operation - * with the current thread's interrupt status set. This method must not block. + * with the current thread's interrupted status set. This method must not block. */ void interrupt(Thread target); /** * Invoked by Thread.interrupt after releasing the Thread's interrupt lock. * It may also be invoked by AbstractInterruptibleChannel or AbstractSelector when - * beginning an I/O operation with the current thread's interrupt status set, or at + * beginning an I/O operation with the current thread's interrupted status set, or at * the end of an I/O operation when any thread doing I/O on the channel (or selector) * has been interrupted. This method closes the channel (or wakes up the Selector) to * ensure that AsynchronousCloseException or ClosedByInterruptException is thrown. diff --git a/src/java.base/share/classes/sun/nio/cs/ArrayEncoder.java b/src/java.base/share/classes/sun/nio/cs/ArrayEncoder.java index b4ced428b33..16a6d5df003 100644 --- a/src/java.base/share/classes/sun/nio/cs/ArrayEncoder.java +++ b/src/java.base/share/classes/sun/nio/cs/ArrayEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,25 +25,17 @@ package sun.nio.cs; -/* - * FastPath char[]/byte[] -> byte[] encoder, REPLACE on malformed input or - * unmappable input. +/** + * Fast-path for {@code byte[]}-to-{@code byte[]} encoding, + * {@link java.nio.charset.CodingErrorAction#REPLACE REPLACE} on malformed + * input, or unmappable input. */ - public interface ArrayEncoder { - // is only used by j.u.zip.ZipCoder for utf8 - int encode(char[] src, int off, int len, byte[] dst); + int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst, int dp); - default int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst) { - return -1; - } + int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst, int dp); - default int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst) { - return -1; - } + boolean isASCIICompatible(); - default boolean isASCIICompatible() { - return false; - } } diff --git a/src/java.base/share/classes/sun/nio/cs/CESU_8.java b/src/java.base/share/classes/sun/nio/cs/CESU_8.java index 9b907bcbc65..409b375ec88 100644 --- a/src/java.base/share/classes/sun/nio/cs/CESU_8.java +++ b/src/java.base/share/classes/sun/nio/cs/CESU_8.java @@ -394,8 +394,7 @@ class CESU_8 extends Unicode } } - private static class Encoder extends CharsetEncoder - implements ArrayEncoder { + private static class Encoder extends CharsetEncoder { private Encoder(Charset cs) { super(cs, 1.1f, 3.0f); @@ -544,48 +543,6 @@ class CESU_8 extends Unicode return encodeBufferLoop(src, dst); } - // returns -1 if there is malformed char(s) and the - // "action" for malformed input is not REPLACE. - public int encode(char[] sa, int sp, int len, byte[] da) { - int sl = sp + len; - int dp = 0; - - // Handle ASCII-only prefix - int n = JLA.encodeASCII(sa, sp, da, dp, Math.min(len, da.length)); - sp += n; - dp += n; - - while (sp < sl) { - char c = sa[sp++]; - if (c < 0x80) { - // Have at most seven bits - da[dp++] = (byte)c; - } else if (c < 0x800) { - // 2 bytes, 11 bits - da[dp++] = (byte)(0xc0 | (c >> 6)); - da[dp++] = (byte)(0x80 | (c & 0x3f)); - } else if (Character.isSurrogate(c)) { - if (sgp == null) - sgp = new Surrogate.Parser(); - int uc = sgp.parse(c, sa, sp - 1, sl); - if (uc < 0) { - if (malformedInputAction() != CodingErrorAction.REPLACE) - return -1; - da[dp++] = replacement()[0]; - } else { - to3Bytes(da, dp, Character.highSurrogate(uc)); - dp += 3; - to3Bytes(da, dp, Character.lowSurrogate(uc)); - dp += 3; - sp++; // 2 chars - } - } else { - // 3 bytes, 16 bits - to3Bytes(da, dp, c); - dp += 3; - } - } - return dp; - } } + } diff --git a/src/java.base/share/classes/sun/nio/cs/DoubleByte.java b/src/java.base/share/classes/sun/nio/cs/DoubleByte.java index 165e1e21c0f..0969669a35b 100644 --- a/src/java.base/share/classes/sun/nio/cs/DoubleByte.java +++ b/src/java.base/share/classes/sun/nio/cs/DoubleByte.java @@ -682,40 +682,7 @@ public class DoubleByte { } @Override - public int encode(char[] src, int sp, int len, byte[] dst) { - int dp = 0; - int sl = sp + len; - if (isASCIICompatible) { - int n = JLA.encodeASCII(src, sp, dst, dp, len); - sp += n; - dp += n; - } - while (sp < sl) { - char c = src[sp++]; - int bb = encodeChar(c); - if (bb == UNMAPPABLE_ENCODING) { - if (Character.isHighSurrogate(c) && sp < sl && - Character.isLowSurrogate(src[sp])) { - sp++; - } - dst[dp++] = repl[0]; - if (repl.length > 1) - dst[dp++] = repl[1]; - continue; - } //else - if (bb > MAX_SINGLEBYTE) { // DoubleByte - dst[dp++] = (byte)(bb >> 8); - dst[dp++] = (byte)bb; - } else { // SingleByte - dst[dp++] = (byte)bb; - } - } - return dp; - } - - @Override - public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + len; while (sp < sl) { char c = (char)(src[sp++] & 0xff); @@ -740,8 +707,7 @@ public class DoubleByte { } @Override - public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + len; while (sp < sl) { char c = StringUTF16.getChar(src, sp++); @@ -1000,49 +966,7 @@ public class DoubleByte { } @Override - public int encode(char[] src, int sp, int len, byte[] dst) { - int dp = 0; - int sl = sp + len; - while (sp < sl) { - char c = src[sp++]; - int bb = encodeChar(c); - - if (bb == UNMAPPABLE_ENCODING) { - if (Character.isHighSurrogate(c) && sp < sl && - Character.isLowSurrogate(src[sp])) { - sp++; - } - dst[dp++] = repl[0]; - if (repl.length > 1) - dst[dp++] = repl[1]; - continue; - } //else - if (bb > MAX_SINGLEBYTE) { // DoubleByte - if (currentState == SBCS) { - currentState = DBCS; - dst[dp++] = SO; - } - dst[dp++] = (byte)(bb >> 8); - dst[dp++] = (byte)bb; - } else { // SingleByte - if (currentState == DBCS) { - currentState = SBCS; - dst[dp++] = SI; - } - dst[dp++] = (byte)bb; - } - } - - if (currentState == DBCS) { - currentState = SBCS; - dst[dp++] = SI; - } - return dp; - } - - @Override - public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + len; while (sp < sl) { char c = (char)(src[sp++] & 0xff); @@ -1077,8 +1001,7 @@ public class DoubleByte { } @Override - public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + len; while (sp < sl) { char c = StringUTF16.getChar(src, sp++); diff --git a/src/java.base/share/classes/sun/nio/cs/HKSCS.java b/src/java.base/share/classes/sun/nio/cs/HKSCS.java index cfe9f879c04..f96fbf6be29 100644 --- a/src/java.base/share/classes/sun/nio/cs/HKSCS.java +++ b/src/java.base/share/classes/sun/nio/cs/HKSCS.java @@ -352,37 +352,9 @@ public class HKSCS { return encodeBufferLoop(src, dst); } - public int encode(char[] src, int sp, int len, byte[] dst) { - int dp = 0; + @Override + public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + len; - while (sp < sl) { - char c = src[sp++]; - int bb = encodeChar(c); - if (bb == UNMAPPABLE_ENCODING) { - if (!Character.isHighSurrogate(c) || sp == sl || - !Character.isLowSurrogate(src[sp]) || - (bb = encodeSupp(Character.toCodePoint(c, src[sp++]))) - == UNMAPPABLE_ENCODING) { - dst[dp++] = repl[0]; - if (repl.length > 1) - dst[dp++] = repl[1]; - continue; - } - } - if (bb > MAX_SINGLEBYTE) { // DoubleByte - dst[dp++] = (byte)(bb >> 8); - dst[dp++] = (byte)bb; - } else { // SingleByte - dst[dp++] = (byte)bb; - } - } - return dp; - } - - public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; - int sl = sp + len; - int dl = dst.length; while (sp < sl) { char c = StringUTF16.getChar(src, sp++); int bb = encodeChar(c); diff --git a/src/java.base/share/classes/sun/nio/cs/SingleByte.java b/src/java.base/share/classes/sun/nio/cs/SingleByte.java index 8efa6b295ff..a5bf06cb251 100644 --- a/src/java.base/share/classes/sun/nio/cs/SingleByte.java +++ b/src/java.base/share/classes/sun/nio/cs/SingleByte.java @@ -290,32 +290,8 @@ public class SingleByte repl = newReplacement[0]; } - public int encode(char[] src, int sp, int len, byte[] dst) { - int dp = 0; - int sl = sp + Math.min(len, dst.length); - while (sp < sl) { - char c = src[sp++]; - int b = encode(c); - if (b != UNMAPPABLE_ENCODING) { - dst[dp++] = (byte)b; - continue; - } - if (Character.isHighSurrogate(c) && sp < sl && - Character.isLowSurrogate(src[sp])) { - if (len > dst.length) { - sl++; - len--; - } - sp++; - } - dst[dp++] = repl; - } - return dp; - } - @Override - public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromLatin1(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + Math.min(len, dst.length); while (sp < sl) { char c = (char)(src[sp++] & 0xff); @@ -330,8 +306,7 @@ public class SingleByte } @Override - public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst) { - int dp = 0; + public int encodeFromUTF16(byte[] src, int sp, int len, byte[] dst, int dp) { int sl = sp + Math.min(len, dst.length); while (sp < sl) { char c = StringUTF16.getChar(src, sp++); diff --git a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java index 82751e3fcd2..74ed1e01553 100644 --- a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java +++ b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -34,12 +34,7 @@ import java.util.function.Supplier; import jdk.internal.reflect.ConstantPool; -import sun.reflect.generics.parser.SignatureParser; -import sun.reflect.generics.tree.TypeSignature; -import sun.reflect.generics.factory.GenericsFactory; -import sun.reflect.generics.factory.CoreReflectionFactory; -import sun.reflect.generics.visitor.Reifier; -import sun.reflect.generics.scope.ClassScope; +import sun.invoke.util.BytecodeDescriptor; /** * Parser for Java programming language annotations. Translates @@ -429,19 +424,11 @@ public class AnnotationParser { } private static Class parseSig(String sig, Class container) { - if (sig.equals("V")) return void.class; - SignatureParser parser = SignatureParser.make(); - TypeSignature typeSig = parser.parseTypeSig(sig); - GenericsFactory factory = CoreReflectionFactory.make(container, ClassScope.make(container)); - Reifier reify = Reifier.make(factory); - typeSig.accept(reify); - Type result = reify.getResult(); - return toClass(result); - } - static Class toClass(Type o) { - if (o instanceof GenericArrayType gat) - return toClass(gat.getGenericComponentType()).arrayType(); - return (Class) o; + try { + return BytecodeDescriptor.parseClass(sig, container.getClassLoader()); + } catch (IllegalArgumentException ex) { + throw new GenericSignatureFormatError(ex.getMessage()); + } } /** diff --git a/src/java.base/share/classes/sun/security/ec/ECDHKeyAgreement.java b/src/java.base/share/classes/sun/security/ec/ECDHKeyAgreement.java index 8f6bdb8d80a..714b06c93ba 100644 --- a/src/java.base/share/classes/sun/security/ec/ECDHKeyAgreement.java +++ b/src/java.base/share/classes/sun/security/ec/ECDHKeyAgreement.java @@ -52,6 +52,7 @@ import java.security.interfaces.ECPublicKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.EllipticCurve; +import java.util.Arrays; import java.util.Optional; /** @@ -259,7 +260,12 @@ public final class ECDHKeyAgreement extends KeyAgreementSpi { throw new NoSuchAlgorithmException( "Unsupported secret key algorithm: " + algorithm); } - return new SecretKeySpec(engineGenerateSecret(), algorithm); + byte[] bytes = engineGenerateSecret(); + try { + return new SecretKeySpec(bytes, algorithm); + } finally { + Arrays.fill(bytes, (byte)0); + } } private static diff --git a/src/java.base/share/classes/sun/security/ec/XDHKeyAgreement.java b/src/java.base/share/classes/sun/security/ec/XDHKeyAgreement.java index 01dce4c53e9..d7ea1c674e7 100644 --- a/src/java.base/share/classes/sun/security/ec/XDHKeyAgreement.java +++ b/src/java.base/share/classes/sun/security/ec/XDHKeyAgreement.java @@ -41,11 +41,12 @@ import javax.crypto.KeyAgreementSpi; import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; +import java.util.Arrays; import java.util.function.Function; public class XDHKeyAgreement extends KeyAgreementSpi { - private byte[] privateKey; + private XECPrivateKey privateKey; private byte[] secret; private XECOperations ops; private XECParameters lockedParams = null; @@ -101,15 +102,16 @@ public class XDHKeyAgreement extends KeyAgreementSpi { throw new InvalidKeyException ("Unsupported key type"); } - XECPrivateKey privateKey = (XECPrivateKey) key; + privateKey = (XECPrivateKey) key; XECParameters xecParams = XECParameters.get( InvalidKeyException::new, privateKey.getParams()); checkLockedParams(InvalidKeyException::new, xecParams); this.ops = new XECOperations(xecParams); - this.privateKey = privateKey.getScalar().orElseThrow( + byte[] tmp = privateKey.getScalar().orElseThrow( () -> new InvalidKeyException("No private key value") ); + Arrays.fill(tmp, (byte)0); secret = null; } @@ -144,9 +146,11 @@ public class XDHKeyAgreement extends KeyAgreementSpi { // The privateKey may be modified to a value that is equivalent for // the purposes of this algorithm. + byte[] scalar = this.privateKey.getScalar().get(); byte[] computedSecret = ops.encodedPointMultiply( - this.privateKey, + scalar, publicKey.getU()); + Arrays.fill(scalar, (byte)0); // test for contributory behavior if (allZero(computedSecret)) { @@ -213,7 +217,12 @@ public class XDHKeyAgreement extends KeyAgreementSpi { throw new NoSuchAlgorithmException( "Unsupported secret key algorithm: " + algorithm); } - return new SecretKeySpec(engineGenerateSecret(), algorithm); + byte[] bytes = engineGenerateSecret(); + try { + return new SecretKeySpec(bytes, algorithm); + } finally { + Arrays.fill(bytes, (byte)0); + } } static class X25519 extends XDHKeyAgreement { diff --git a/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java index 416a3e10af4..9b6a5676f4e 100644 --- a/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java @@ -27,6 +27,7 @@ package sun.security.ec; import java.io.*; import java.security.interfaces.XECPrivateKey; +import java.util.Arrays; import java.util.Optional; import java.security.*; import java.security.spec.*; @@ -106,12 +107,15 @@ public final class XDHPrivateKeyImpl extends PKCS8Key implements XECPrivateKey { XECParameters params = paramSpec.getName().equalsIgnoreCase("X25519") ? XECParameters.X25519 : XECParameters.X448; + var kClone = k.clone(); try { return new XDHPublicKeyImpl(params, - new XECOperations(params).computePublic(k.clone())); + new XECOperations(params).computePublic(kClone)); } catch (InvalidKeyException e) { throw new ProviderException( "Unexpected error calculating public key", e); + } finally { + Arrays.fill(kClone, (byte)0); } } diff --git a/src/java.base/share/classes/sun/security/ec/ed/EdDSASignature.java b/src/java.base/share/classes/sun/security/ec/ed/EdDSASignature.java index 1757f9eb67d..6af39de34dc 100644 --- a/src/java.base/share/classes/sun/security/ec/ed/EdDSASignature.java +++ b/src/java.base/share/classes/sun/security/ec/ed/EdDSASignature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -44,6 +44,7 @@ import java.security.interfaces.EdECPublicKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.EdDSAParameterSpec; import java.security.spec.NamedParameterSpec; +import java.util.Arrays; import java.util.function.Function; public class EdDSASignature extends SignatureSpi { @@ -92,7 +93,7 @@ public class EdDSASignature extends SignatureSpi { } } - private byte[] privateKey; + private EdECPrivateKey privateKey; private AffinePoint publicKeyPoint; private byte[] publicKeyBytes; private EdDSAOperations ops; @@ -141,11 +142,13 @@ public class EdDSASignature extends SignatureSpi { if (!(privateKey instanceof EdECPrivateKey)) { throw new InvalidKeyException("Unsupported key type"); } - EdECPrivateKey edKey = (EdECPrivateKey) privateKey; + this.privateKey = (EdECPrivateKey) privateKey; + + initImpl(this.privateKey.getParams()); + byte[] tmp = this.privateKey.getBytes().orElseThrow( + () -> new InvalidKeyException("No private key value")); + Arrays.fill(tmp, (byte)0); - initImpl(edKey.getParams()); - this.privateKey = edKey.getBytes().orElseThrow( - () -> new InvalidKeyException("No private key value")); this.publicKeyPoint = null; this.publicKeyBytes = null; } @@ -199,10 +202,14 @@ public class EdDSASignature extends SignatureSpi { throw new SignatureException("Missing private key"); } ensureMessageInit(); - byte[] result = ops.sign(this.sigParams, this.privateKey, - message.getMessage()); - message = null; - return result; + byte[] bytes = this.privateKey.getBytes().get(); + try { + return ops.sign(this.sigParams, bytes, + message.getMessage()); + } finally { + Arrays.fill(bytes, (byte)0); + message = null; + } } @Override diff --git a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java index f127ac65cc7..4acc0833c5d 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java @@ -221,7 +221,7 @@ public final class AlgorithmChecker extends PKIXCertPathChecker { currSigAlg, prevPubKey, currSigAlgParams)) { throw new CertPathValidatorException( "Algorithm constraints check failed on " + - currSigAlg + "signature and " + + currSigAlg + " signature and " + currPubKey.getAlgorithm() + " key with size of " + sun.security.util.KeyUtil.getKeySize(currPubKey) + "bits", diff --git a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java index 270d10e82fa..397e8093cf7 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -212,6 +212,11 @@ public final class PKIXCertPathValidator extends CertPathValidatorSpi { ((RevocationChecker)checker).init(anchor, params); } } + + // Set trust anchor for the user-specified AlgorithmChecker. + if (checker instanceof AlgorithmChecker algChecker) { + algChecker.trySetTrustAnchor(anchor); + } } // only add a RevocationChecker if revocation is enabled and // a PKIXRevocationChecker has not already been added diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index 4e1ccf385c7..ed5e079bf44 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -34,9 +34,9 @@ import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLProtocolException; /** - * SSL/(D)TLS Alter description + * SSL/(D)TLS Alert description */ -enum Alert { +public enum Alert { // Please refer to TLS Alert Registry for the latest (D)TLS Alert values: // https://www.iana.org/assignments/tls-parameters/ CLOSE_NOTIFY ((byte)0, "close_notify", false), @@ -103,7 +103,7 @@ enum Alert { return null; } - static String nameOf(byte id) { + public static String nameOf(byte id) { for (Alert al : Alert.values()) { if (al.id == id) { return al.description; @@ -181,6 +181,16 @@ enum Alert { AlertMessage(TransportContext context, ByteBuffer m) throws IOException { + // From RFC 8446 "Implementations + // MUST NOT send Handshake and Alert records that have a zero-length + // TLSInnerPlaintext.content; if such a message is received, the + // receiving implementation MUST terminate the connection with an + // "unexpected_message" alert." + if (m.remaining() == 0) { + throw context.fatal(Alert.UNEXPECTED_MESSAGE, + "Alert fragments must not be zero length."); + } + // struct { // AlertLevel level; // AlertDescription description; diff --git a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java index d44ec034411..aa5933ddab0 100644 --- a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -71,7 +71,7 @@ final class AlpnExtension { /** * The "application_layer_protocol_negotiation" extension. - * + *

      * See RFC 7301 for the specification of this extension. */ static final class AlpnSpec implements SSLExtensionSpec { @@ -344,6 +344,13 @@ final class AlpnExtension { // The producing happens in server side only. ServerHandshakeContext shc = (ServerHandshakeContext)context; + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST use ALPN + throw shc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Client did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. shc.applicationProtocol = ""; shc.conContext.applicationProtocol = ""; @@ -513,6 +520,15 @@ final class AlpnExtension { // The producing happens in client side only. ClientHandshakeContext chc = (ClientHandshakeContext)context; + if (chc.sslConfig.isQuic) { + // RFC 9001: QUIC clients MUST use error 0x0178 + // [no_application_protocol] to terminate a connection when + // ALPN negotiation fails + throw chc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Server did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. chc.applicationProtocol = ""; chc.conContext.applicationProtocol = ""; diff --git a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java index b975290d09d..2125a148162 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java @@ -36,6 +36,11 @@ import sun.security.ssl.SignatureAlgorithmsExtension.SignatureSchemesSpec; /** * Pack of the "signature_algorithms_cert" extensions. + *

      + * Note: Per RFC 8446, if no "signature_algorithms_cert" extension is + * present, then the "signature_algorithms" extension also applies to + * signatures appearing in certificates. + * See {@code SignatureAlgorithmsExtension} for details. */ final class CertSignAlgsExtension { static final HandshakeProducer chNetworkProducer = diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java index 609a81571ed..d4587d35ae9 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java +++ b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java @@ -1219,12 +1219,19 @@ final class CertificateMessage { certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)shc.conContext.transport; + } else if (shc.conContext.transport instanceof SSLSocket socket){ ((X509ExtendedTrustManager)tm).checkClientTrusted( certs.clone(), authType, socket); + } else if (shc.conContext.transport + instanceof QuicTLSEngineImpl qtlse) { + if (tm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkClientTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } } } else { // Unlikely to happen, because we have wrapped the old @@ -1268,18 +1275,26 @@ final class CertificateMessage { try { X509TrustManager tm = chc.sslContext.getX509TrustManager(); - if (tm instanceof X509ExtendedTrustManager) { + if (tm instanceof X509ExtendedTrustManager x509ExtTm) { if (chc.conContext.transport instanceof SSLEngine engine) { - ((X509ExtendedTrustManager)tm).checkServerTrusted( + x509ExtTm.checkServerTrusted( certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)chc.conContext.transport; - ((X509ExtendedTrustManager)tm).checkServerTrusted( + } else if (chc.conContext.transport instanceof SSLSocket socket) { + x509ExtTm.checkServerTrusted( certs.clone(), authType, socket); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl qtlse) { + if (x509ExtTm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkServerTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } + } else { + throw new AssertionError("Unexpected transport type"); } } else { // Unlikely to happen, because we have wrapped the old diff --git a/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java b/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java index 6da6f745691..4ea61161c1d 100644 --- a/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java +++ b/src/java.base/share/classes/sun/security/ssl/ChangeCipherSpec.java @@ -232,7 +232,8 @@ final class ChangeCipherSpec { tc.consumers.remove(ContentType.CHANGE_CIPHER_SPEC.id); // parse - if (message.remaining() != 1 || message.get() != 1) { + if (message.remaining() != 1 || message.get() != 1 + || tc.isNegotiated) { throw tc.fatal(Alert.UNEXPECTED_MESSAGE, "Malformed or unexpected ChangeCipherSpec message"); } diff --git a/src/java.base/share/classes/sun/security/ssl/ClientHello.java b/src/java.base/share/classes/sun/security/ssl/ClientHello.java index 3e43921520d..c9432ea3979 100644 --- a/src/java.base/share/classes/sun/security/ssl/ClientHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ClientHello.java @@ -568,7 +568,7 @@ final class ClientHello { } if (sessionId.length() == 0 && chc.maximumActiveProtocol.useTLS13PlusSpec() && - SSLConfiguration.useCompatibilityMode) { + chc.sslConfig.isUseCompatibilityMode()) { // In compatibility mode, the TLS 1.3 legacy_session_id // field MUST be non-empty, so a client not offering a // pre-TLS 1.3 session MUST generate a new 32-byte value. diff --git a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java index e0196f3009c..4e82fd25a7b 100644 --- a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -170,7 +170,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // Buffer next epoch message if necessary. if (this.readEpoch < recordEpoch) { - // Discard the record younger than the current epcoh if: + // Discard the record younger than the current epoch if: // 1. it is not a handshake message, or // 3. it is not of next epoch. if ((contentType != ContentType.HANDSHAKE.id && @@ -801,8 +801,11 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // buffer this fragment if (hsf.handshakeType == SSLHandshake.FINISHED.id) { - // Need no status update. - bufferedFragments.add(hsf); + // Make sure it's not a retransmitted message + if (hsf.recordEpoch > handshakeEpoch) { + bufferedFragments.add(hsf); + flightIsReady = holes.isEmpty(); + } } else { bufferFragment(hsf); } @@ -1442,7 +1445,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // if (expectCCSFlight) { // Have the ChangeCipherSpec/Finished flight been received? - boolean isReady = hasFinishedMessage(bufferedFragments); + boolean isReady = hasFinishedMessage(); if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine( "Has the final flight been received? " + isReady); @@ -1489,7 +1492,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // // an abbreviated handshake // - if (hasFinishedMessage(bufferedFragments)) { + if (hasFinishedMessage()) { if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine("It's an abbreviated handshake."); } @@ -1562,7 +1565,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { } } - if (!hasFinishedMessage(bufferedFragments)) { + if (!hasFinishedMessage()) { // not yet have the ChangeCipherSpec/Finished messages if (SSLLogger.isOn && SSLLogger.isOn("verbose")) { SSLLogger.fine( @@ -1598,35 +1601,33 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { return false; } - // Looking for the ChangeCipherSpec and Finished messages. + // Looking for the ChangeCipherSpec, Finished and + // NewSessionTicket messages. // // As the cached Finished message should be a ciphertext, we don't // exactly know a ciphertext is a Finished message or not. According // to the spec of TLS/DTLS handshaking, a Finished message is always // sent immediately after a ChangeCipherSpec message. The first // ciphertext handshake message should be the expected Finished message. - private boolean hasFinishedMessage(Set fragments) { - + private boolean hasFinishedMessage() { boolean hasCCS = false; boolean hasFin = false; - for (RecordFragment fragment : fragments) { + + for (RecordFragment fragment : bufferedFragments) { if (fragment.contentType == ContentType.CHANGE_CIPHER_SPEC.id) { - if (hasFin) { - return true; - } hasCCS = true; - } else if (fragment.contentType == ContentType.HANDSHAKE.id) { - // Finished is the first expected message of a new epoch. - if (fragment.isCiphertext) { - if (hasCCS) { - return true; - } - hasFin = true; - } + } else if (fragment.contentType == ContentType.HANDSHAKE.id + && fragment.isCiphertext) { + hasFin = true; } } - return false; + // NewSessionTicket message presence in the Finished flight + // should only be expected on the client side, and only + // if stateless resumption is enabled. + return hasCCS && hasFin && (!tc.sslConfig.isClientMode + || !tc.handshakeContext.statelessResumption + || hasCompleted(SSLHandshake.NEW_SESSION_TICKET.id)); } // Is client CertificateVerify a mandatory message? @@ -1671,7 +1672,7 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { int presentMsgSeq, int endMsgSeq) { // The caller should have checked the completion of the first - // present handshake message. Need not to check it again. + // present handshake message. Need not check it again. for (RecordFragment rFrag : fragments) { if ((rFrag.contentType != ContentType.HANDSHAKE.id) || rFrag.isCiphertext) { diff --git a/src/java.base/share/classes/sun/security/ssl/Finished.java b/src/java.base/share/classes/sun/security/ssl/Finished.java index 9421d12ec15..04fe61760d0 100644 --- a/src/java.base/share/classes/sun/security/ssl/Finished.java +++ b/src/java.base/share/classes/sun/security/ssl/Finished.java @@ -846,6 +846,16 @@ final class Finished { // update the context for the following key derivation shc.handshakeKeyDerivation = secretKD; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw shc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw shc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); @@ -1010,6 +1020,16 @@ final class Finished { // update the context for the following key derivation chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw chc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw chc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); diff --git a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java index c4549070f02..2b17c7406a3 100644 --- a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java +++ b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java @@ -269,6 +269,12 @@ final class KeyUpdate { HandshakeMessage message) throws IOException { // The producing happens in server side only. PostHandshakeContext hc = (PostHandshakeContext)context; + if (hc.sslConfig.isQuic) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own Quic specific + // key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. + return null; + } KeyUpdateMessage km = (KeyUpdateMessage)message; if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { SSLLogger.fine( diff --git a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java index 0fa831f6351..f2c30b3ff72 100644 --- a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -31,6 +31,8 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.ssl.SSLCipher.SSLWriteCipher; /** @@ -154,6 +156,16 @@ abstract class OutputRecord throw new UnsupportedOperationException(); } + // apply to QuicEngine only + byte[] getHandshakeMessage() { + throw new UnsupportedOperationException(); + } + + // apply to QuicEngine only + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + throw new UnsupportedOperationException(); + } + // apply to SSLEngine only void encodeV2NoCipher() throws IOException { throw new UnsupportedOperationException(); diff --git a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java index b06549b40e3..a4f87616245 100644 --- a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java +++ b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java @@ -47,17 +47,15 @@ final class PostHandshakeContext extends HandshakeContext { context.conSession.getLocalSupportedSignatureSchemes()); // Add the potential post-handshake consumers. - if (context.sslConfig.isClientMode) { + if (!context.sslConfig.isQuic) { handshakeConsumers.putIfAbsent( SSLHandshake.KEY_UPDATE.id, SSLHandshake.KEY_UPDATE); + } + if (context.sslConfig.isClientMode) { handshakeConsumers.putIfAbsent( SSLHandshake.NEW_SESSION_TICKET.id, SSLHandshake.NEW_SESSION_TICKET); - } else { - handshakeConsumers.putIfAbsent( - SSLHandshake.KEY_UPDATE.id, - SSLHandshake.KEY_UPDATE); } handshakeFinished = true; @@ -93,6 +91,15 @@ final class PostHandshakeContext extends HandshakeContext { static boolean isConsumable(TransportContext context, byte handshakeType) { if (handshakeType == SSLHandshake.KEY_UPDATE.id) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own + // Quic-specific key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. Endpoints + // MUST treat the receipt of a TLS KeyUpdate message as a + // connection error of type 0x010a, equivalent to a fatal + // TLS alert of unexpected_message; + if (context.sslConfig.isQuic) { + return false; + } // The KeyUpdate handshake message does not apply to TLS 1.2 and // previous protocols. return context.protocolVersion.useTLS13PlusSpec(); diff --git a/src/java.base/share/classes/sun/security/ssl/QuicCipher.java b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java new file mode 100644 index 00000000000..c1d812e4c40 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java @@ -0,0 +1,699 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package sun.security.ssl; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import java.security.Security; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +abstract class QuicCipher { + private static final String + SEC_PROP_QUIC_TLS_KEY_LIMITS = "jdk.quic.tls.keyLimits"; + + private static final Map KEY_LIMITS; + + static { + final String propVal = Security.getProperty( + SEC_PROP_QUIC_TLS_KEY_LIMITS); + if (propVal == null) { + KEY_LIMITS = Map.of(); // no specific limits + } else { + final Map limits = new HashMap<>(); + for (final String entry : propVal.split(",")) { + // each entry is of the form + // example: + // AES/GCM/NoPadding 2^23 + // ChaCha20-Poly1305 -1 + final String[] parts = entry.trim().split(" "); + if (parts.length != 2) { + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + final String cipher = parts[0]; + if (limits.containsKey(cipher)) { + throw new RuntimeException( + "key limit defined more than once for cipher " + + cipher); + } + final String limitVal = parts[1]; + final long limit; + final int index = limitVal.indexOf("^"); + if (index >= 0) { + // of the form x^y (example: 2^23) + limit = (long) Math.pow( + Integer.parseInt(limitVal.substring(0, index)), + Integer.parseInt(limitVal.substring(index + 1))); + } else { + limit = Long.parseLong(limitVal); + } + if (limit == 0 || limit < -1) { + // we allow -1 to imply no limits, but any other zero + // or negative value is invalid + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + limits.put(cipher, limit); + } + KEY_LIMITS = Collections.unmodifiableMap(limits); + } + } + + private final CipherSuite cipherSuite; + private final QuicHeaderProtectionCipher hpCipher; + private final SecretKey baseSecret; + private final int keyPhase; + + protected QuicCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final QuicHeaderProtectionCipher hpCipher, final int keyPhase) { + assert keyPhase == 0 || keyPhase == 1 : + "invalid key phase: " + keyPhase; + this.cipherSuite = cipherSuite; + this.baseSecret = baseSecret; + this.hpCipher = hpCipher; + this.keyPhase = keyPhase; + } + + final SecretKey getBaseSecret() { + return this.baseSecret; + } + + final CipherSuite getCipherSuite() { + return this.cipherSuite; + } + + final SecretKey getHeaderProtectionKey() { + return this.hpCipher.headerProtectionKey; + } + + final ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + return hpCipher.computeHeaderProtectionMask(sample); + } + + final int getKeyPhase() { + return this.keyPhase; + } + + final void discard(boolean destroyHP) { + safeDiscard(this.baseSecret); + if (destroyHP) { + this.hpCipher.discard(); + } + this.doDiscard(); + } + + protected abstract void doDiscard(); + + static QuicReadCipher createReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305ReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static QuicWriteCipher createWriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMWriteCipher(cipherSuite, baseSecret, key, iv, hp, + keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305WriteCipher(cipherSuite, baseSecret, key, iv, + hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static void safeDiscard(final SecretKey secretKey) { + KeyUtil.destroySecretKeys(secretKey); + } + + abstract static class QuicReadCipher extends QuicCipher { + private final AtomicLong lowestDecryptedPktNum = new AtomicLong(-1); + + QuicReadCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void decryptPacket(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + doDecrypt(packetNumber, packet, headerLength, output); + boolean updated; + do { + final long current = lowestDecryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestDecryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + protected abstract void doDecrypt(long packetNumber, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that fail + * decryption, across all key (updates), using this + * {@code QuicReadCipher}. This method must not return a value less + * than 0. + * + * @return the limit + */ + // RFC-9001, section 6.6 + abstract long integrityLimit(); + + /** + * {@return the lowest packet number that this {@code QuicReadCipher} + * has decrypted. If no packets have yet been decrypted by this + * instance, then this method returns -1} + */ + final long lowestDecryptedPktNum() { + return this.lowestDecryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicReadCipher} has successfully + * decrypted any packet sent by the peer, else returns false} + */ + final boolean hasDecryptedAny() { + return this.lowestDecryptedPktNum.get() != -1; + } + } + + abstract static class QuicWriteCipher extends QuicCipher { + private final AtomicLong numPacketsEncrypted = new AtomicLong(); + private final AtomicLong lowestEncryptedPktNum = new AtomicLong(-1); + + QuicWriteCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void encryptPacket(final long packetNumber, + final ByteBuffer packetHeader, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicTransportException, ShortBufferException { + final long confidentialityLimit = confidentialityLimit(); + final long numEncrypted = this.numPacketsEncrypted.get(); + if (confidentialityLimit > 0 && + numEncrypted > confidentialityLimit) { + // the OneRttKeyManager is responsible for detecting and + // initiating a key update before this limit is hit. The fact + // that we hit this limit indicates that either the key + // update wasn't initiated or the key update failed. In + // either case we just throw an exception which + // should lead to the connection being closed as required by + // RFC-9001, section 6.6: + // If a key update is not possible or integrity limits are + // reached, the endpoint MUST stop using the connection and + // only send stateless resets in response to receiving + // packets. It is RECOMMENDED that endpoints immediately + // close the connection with a connection error of type + // AEAD_LIMIT_REACHED before reaching a state where key + // updates are not possible. + throw new QuicTransportException("confidentiality limit " + + "reached", ONE_RTT, 0, + QuicTransportErrors.AEAD_LIMIT_REACHED); + } + this.numPacketsEncrypted.incrementAndGet(); + doEncryptPacket(packetNumber, packetHeader, packetPayload, output); + boolean updated; + do { + final long current = lowestEncryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestEncryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + /** + * {@return the lowest packet number that this {@code QuicWriteCipher} + * has encrypted. If no packets have yet been encrypted by this + * instance, then this method returns -1} + */ + final long lowestEncryptedPktNum() { + return this.lowestEncryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicWriteCipher} has successfully + * encrypted any packet to send to the peer, else returns false} + */ + final boolean hasEncryptedAny() { + // rely on the lowestEncryptedPktNum field instead of the + // numPacketsEncrypted field. this avoids a race where the + // lowestEncryptedPktNum() might return a value contradicting + // the return value of this method. + return this.lowestEncryptedPktNum.get() != -1; + } + + /** + * {@return the number of packets encrypted by this {@code + * QuicWriteCipher}} + */ + final long getNumEncrypted() { + return this.numPacketsEncrypted.get(); + } + + abstract void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that are allowed + * to be encrypted with this instance of {@code QuicWriteCipher}. A + * value less than 0 implies that there's no limit. + * + * @return the limit or -1 + */ + // RFC-9001, section 6.6: The confidentiality limit applies to the + // number of + // packets encrypted with a given key. + abstract long confidentialityLimit(); + } + + abstract static class QuicHeaderProtectionCipher { + protected final SecretKey headerProtectionKey; + + protected QuicHeaderProtectionCipher( + final SecretKey headerProtectionKey) { + this.headerProtectionKey = headerProtectionKey; + } + + int getHeaderProtectionSampleSize() { + return 16; + } + + abstract ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException; + + final void discard() { + safeDiscard(this.headerProtectionKey); + } + } + + static final class T13GCMReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the integrity limit is 2^52 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 52; + + private final Cipher cipher; + private final SecretKey key; + private final byte[] iv; + + T13GCMReadCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("AES/GCM/NoPadding"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13GCMWriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "AES/GCM/NoPadding"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the confidentiality limit is 2^23 encrypted packets + final long defaultVal = 1 << 23; + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + // don't allow the configuration to increase the confidentiality + // limit, but only let it lower the limit + limit = limit > defaultVal ? defaultVal : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13GCMWriteCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13AESHPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13AESHPCipher(SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + ByteBuffer output = ByteBuffer.allocate(sample.remaining()); + try { + synchronized (cipher) { + // Some providers (Jipher) don't re-initialize the cipher + // after doFinal, and need init every time. + cipher.init(Cipher.ENCRYPT_MODE, headerProtectionKey); + cipher.doFinal(sample, output); + } + output.flip(); + assert output.remaining() >= 5; + return output; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + static final class T13CC20P1305ReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, + // the integrity limit is 2^36 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 36; + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305ReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("ChaCha20-Poly1305"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20P1305WriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "ChaCha20-Poly1305"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, the + // confidentiality limit is greater than the number of possible + // packets (2^62) and so can be disregarded. + final long defaultVal = -1; // no limit + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + limit = limit < 0 ? -1 /* no limit */ : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305WriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(final long packetNumber, final ByteBuffer packetHeader, + final ByteBuffer packetPayload, final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + long pn = packetNumber; + while (pn > 0) { + iv[i] ^= (byte) (pn & 0xFF); + pn = pn >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20HPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13CC20HPCipher(final SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("ChaCha20"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + try { + // RFC 7539: [counter is a] 32-bit block count parameter, + // treated as a 32-bit little-endian integer + // RFC 9001: + // counter = sample[0..3] + // nonce = sample[4..15] + // mask = ChaCha20(hp_key, counter, nonce, {0,0,0,0,0}) + + sample.order(ByteOrder.LITTLE_ENDIAN); + byte[] nonce = new byte[12]; + int counter = sample.getInt(); + sample.get(nonce); + ChaCha20ParameterSpec ivSpec = + new ChaCha20ParameterSpec(nonce, counter); + byte[] output = new byte[5]; + + synchronized (cipher) { + // DECRYPT produces the same output as ENCRYPT, but does + // not throw when the same IV is used repeatedly + cipher.init(Cipher.DECRYPT_MODE, headerProtectionKey, + ivSpec); + int numBytes = cipher.doFinal(output, 0, 5, output); + assert numBytes == 5; + } + return ByteBuffer.wrap(output); + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java new file mode 100644 index 00000000000..893eb282116 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package sun.security.ssl; + +import jdk.internal.net.quic.QuicTLSEngine; +import sun.security.ssl.SSLCipher.SSLWriteCipher; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedList; + +/** + * {@code OutputRecord} implementation for {@code QuicTLSEngineImpl}. + */ +final class QuicEngineOutputRecord extends OutputRecord implements SSLRecord { + + private final HandshakeFragment fragmenter = new HandshakeFragment(); + + private volatile boolean isCloseWaiting; + + private Alert alert; + + QuicEngineOutputRecord(HandshakeHash handshakeHash) { + super(handshakeHash, SSLWriteCipher.nullTlsWriteCipher()); + + this.packetSize = SSLRecord.maxRecordSize; + this.protocolVersion = ProtocolVersion.NONE; + } + + @Override + public void close() throws IOException { + recordLock.lock(); + try { + if (!isClosed) { + if (!fragmenter.isEmpty()) { + isCloseWaiting = true; + } else { + super.close(); + } + } + } finally { + recordLock.unlock(); + } + } + + boolean isClosed() { + return isClosed || isCloseWaiting; + } + + @Override + void encodeAlert(byte level, byte description) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + if (level == Alert.Level.WARNING.level) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing warning-level " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + + if (alert != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing subsequent alert: " + + description + ", original: " + alert.id); + } + return; + } + + alert = Alert.valueOf(description); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeHandshake(byte[] source, + int offset, int length) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "handshake message", + ByteBuffer.wrap(source, offset, length)); + } + return; + } + + firstMessage = false; + + byte handshakeType = source[offset]; + if (handshakeHash.isHashable(handshakeType)) { + handshakeHash.deliver(source, offset, length); + } + + fragmenter.queueUpFragment(source, offset, length); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeChangeCipherSpec() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, boolean useChangeCipherSpec) throws IOException { + recordLock.lock(); + try { + fragmenter.changePacketSpace(); + } finally { + recordLock.unlock(); + } + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, byte keyUpdateRequest) throws IOException { + throw new UnsupportedOperationException("Should not call this"); + } + + @Override + byte[] getHandshakeMessage() { + recordLock.lock(); + try { + return fragmenter.acquireCiphertext(); + } finally { + recordLock.unlock(); + } + } + + @Override + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + recordLock.lock(); + try { + return switch (fragmenter.currentPacketSpace) { + case 0-> QuicTLSEngine.KeySpace.INITIAL; + case 1-> QuicTLSEngine.KeySpace.HANDSHAKE; + case 2-> QuicTLSEngine.KeySpace.ONE_RTT; + default -> throw new IllegalStateException("Unexpected state"); + }; + } finally { + recordLock.unlock(); + } + } + + @Override + boolean isEmpty() { + recordLock.lock(); + try { + return fragmenter.isEmpty(); + } finally { + recordLock.unlock(); + } + } + + Alert getAlert() { + recordLock.lock(); + try { + return alert; + } finally { + recordLock.unlock(); + } + } + + // buffered record fragment + private static class HandshakeMemo { + boolean changeSpace; + byte[] fragment; + } + + static final class HandshakeFragment { + private final LinkedList handshakeMemos = + new LinkedList<>(); + + private int currentPacketSpace; + + void queueUpFragment(byte[] source, + int offset, int length) throws IOException { + HandshakeMemo memo = new HandshakeMemo(); + + memo.fragment = new byte[length]; + assert Record.getInt24(ByteBuffer.wrap(source, offset + 1, 3)) + == length - 4 : "Invalid handshake message length"; + System.arraycopy(source, offset, memo.fragment, 0, length); + + handshakeMemos.add(memo); + } + + void changePacketSpace() { + HandshakeMemo lastMemo = handshakeMemos.peekLast(); + if (lastMemo != null) { + lastMemo.changeSpace = true; + } else { + currentPacketSpace++; + } + } + + byte[] acquireCiphertext() { + HandshakeMemo hsMemo = handshakeMemos.pollFirst(); + if (hsMemo == null) { + return null; + } + if (hsMemo.changeSpace) { + currentPacketSpace++; + } + return hsMemo.fragment; + } + + boolean isEmpty() { + return handshakeMemos.isEmpty(); + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java new file mode 100644 index 00000000000..fb9077af022 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java @@ -0,0 +1,1216 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package sun.security.ssl; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.IntFunction; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.KDF; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.vm.annotation.Stable; +import sun.security.ssl.QuicCipher.QuicReadCipher; +import sun.security.ssl.QuicCipher.QuicWriteCipher; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.HANDSHAKE; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.INITIAL; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static jdk.internal.net.quic.QuicTransportErrors.AEAD_LIMIT_REACHED; +import static jdk.internal.net.quic.QuicTransportErrors.KEY_UPDATE_ERROR; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +sealed abstract class QuicKeyManager + permits QuicKeyManager.HandshakeKeyManager, + QuicKeyManager.InitialKeyManager, QuicKeyManager.OneRttKeyManager { + + private record QuicKeys(SecretKey key, byte[] iv, SecretKey hp) { + } + + private record CipherPair(QuicReadCipher readCipher, + QuicWriteCipher writeCipher) { + void discard(boolean destroyHP) { + writeCipher.discard(destroyHP); + readCipher.discard(destroyHP); + } + + /** + * {@return true if the keys represented by this {@code CipherPair} + * were used by both this endpoint and the peer, thus implying these + * keys are available to both of them} + */ + boolean usedByBothEndpoints() { + return this.readCipher.hasDecryptedAny() && + this.writeCipher.hasEncryptedAny(); + } + } + + final QuicTLSEngine.KeySpace keySpace; + // counter towards the integrity limit + final AtomicLong invalidPackets = new AtomicLong(); + volatile boolean keysDiscarded; + + private QuicKeyManager(final QuicTLSEngine.KeySpace keySpace) { + this.keySpace = keySpace; + } + + protected abstract boolean keysAvailable(); + + protected abstract QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException; + + protected abstract QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException; + + abstract void discardKeys(); + + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet,final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + IllegalArgumentException, AEADBadTagException, + QuicTransportException, ShortBufferException { + // keyPhase is only applicable for 1-RTT packets; the decryptPacket + // method is overridden by OneRttKeyManager, so this check is for + // other packet types + if (keyPhase != -1) { + throw new IllegalArgumentException( + "Unexpected key phase value: " + keyPhase); + } + // use current keys to decrypt + QuicReadCipher readCipher = getReadCipher(); + try { + readCipher.decryptPacket(packetNumber, packet, headerLength, output); + } catch (AEADBadTagException e) { + if (invalidPackets.incrementAndGet() >= + readCipher.integrityLimit()) { + throw new QuicTransportException("Integrity limit reached", + keySpace, 0, AEAD_LIMIT_REACHED); + } + throw e; + } + } + + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(0); // key phase is always 0 for non-ONE_RTT + getWriteCipher().encryptPacket(packetNumber, header, packetPayload, output); + } + + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret) + throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + final SecretKey quic_hp = kd.deriveKey(tlsData.getTlsHpLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + // Used in 1RTT when advancing the keyphase. quic_hp is not advanced. + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret, + final SecretKey quic_hp) throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + private static QuicTLSData getQuicData(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> QuicTLSData.V1; + case QUIC_V2 -> QuicTLSData.V2; + }; + } + + private static byte[] createHkdfInfo(final String label, final int length) { + final byte[] tls13Label = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + return createHkdfInfo(tls13Label, length); + } + + private static byte[] createHkdfInfo(final byte[] tls13Label, + final int length) { + final byte[] info = new byte[4 + tls13Label.length]; + final ByteBuffer m = ByteBuffer.wrap(info); + try { + Record.putInt16(m, length); + Record.putBytes8(m, tls13Label); + Record.putInt8(m, 0x00); // zero-length context + } catch (IOException ioe) { + // unlikely + throw new UncheckedIOException("Unexpected exception", ioe); + } + return info; + } + + static final class InitialKeyManager extends QuicKeyManager { + + private volatile CipherPair cipherPair; + + InitialKeyManager() { + super(INITIAL); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.keysDiscarded = true; + this.cipherPair = null; // no longer needed + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final byte[] connectionId, + final boolean clientMode) throws IOException{ + Objects.requireNonNull(quicVersion); + final CipherSuite cs = CipherSuite.TLS_AES_128_GCM_SHA256; + final CipherSuite.HashAlg hashAlg = cs.hashAlg; + + KDF hkdf; + try { + hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new SSLHandshakeException("Could not generate secret", e); + } + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + SecretKey initial_secret = null; + SecretKey server_initial_secret = null; + SecretKey client_initial_secret = null; + try { + initial_secret = hkdf.deriveKey("TlsInitialSecret", + HKDFParameterSpec.ofExtract() + .addSalt(tlsData.getInitialSalt()) + .addIKM(connectionId).extractOnly()); + + byte[] clientInfo = createHkdfInfo("client in", + hashAlg.hashLength); + client_initial_secret = + hkdf.deriveKey("TlsClientInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + clientInfo, + hashAlg.hashLength)); + QuicKeys clientKeys = deriveQuicKeys(quicVersion, cs, + client_initial_secret); + + byte[] serverInfo = createHkdfInfo("server in", + hashAlg.hashLength); + server_initial_secret = + hkdf.deriveKey("TlsServerInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + serverInfo, + hashAlg.hashLength)); + QuicKeys serverKeys = deriveQuicKeys(quicVersion, cs, + server_initial_secret); + + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + } + final CipherPair old = this.cipherPair; + // we don't check if keys are already available, since it's a + // valid case where the INITIAL keys are regenerated due to a + // RETRY packet from the peer or even for the case where a + // different quic version was negotiated by the server + this.cipherPair = new CipherPair(readCipher, writeCipher); + if (old != null) { + old.discard(true); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(initial_secret, client_initial_secret, + server_initial_secret); + } + } + + static Cipher getRetryCipher(final QuicVersion quicVersion, + final boolean incoming) throws QuicTransportException { + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + return tlsData.getRetryCipher(incoming); + } + } + + static final class HandshakeKeyManager extends QuicKeyManager { + private volatile CipherPair cipherPair; + + HandshakeKeyManager() { + super(HANDSHAKE); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.cipherPair = null; // no longer needed + this.keysDiscarded = true; + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(quicVersion); + if (keysAvailable()) { + throw new IllegalStateException( + "Keys already derived for " + this.keySpace + + " key space"); + } + SecretKey client_handshake_traffic_secret = null; + SecretKey server_handshake_traffic_secret = null; + try { + final SSLKeyDerivation kd = + handshakeContext.handshakeKeyDerivation; + client_handshake_traffic_secret = kd.deriveKey( + "TlsClientHandshakeTrafficSecret"); + final QuicKeys clientKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + client_handshake_traffic_secret); + server_handshake_traffic_secret = kd.deriveKey( + "TlsServerHandshakeTrafficSecret"); + final QuicKeys serverKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + server_handshake_traffic_secret); + + final CipherSuite negotiatedCipherSuite = + handshakeContext.negotiatedCipherSuite; + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + } else { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + } + synchronized (this) { + if (this.cipherPair != null) { + // don't allow setting more than once + throw new IllegalStateException("Keys already " + + "available for keyspace: " + + this.keySpace); + } + this.cipherPair = new CipherPair(readCipher, writeCipher); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(client_handshake_traffic_secret, + server_handshake_traffic_secret); + } + } + } + + static final class OneRttKeyManager extends QuicKeyManager { + // a series of keys that the 1-RTT key manager uses + private record KeySeries(QuicReadCipher old, CipherPair current, + CipherPair next) { + private KeySeries { + Objects.requireNonNull(current); + if (old != null) { + if (old.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both old keys and" + + " current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + if (next != null) { + if (next.writeCipher.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both next keys " + + "and current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + } + + /** + * {@return true if this {@code KeySeries} has an old decryption key + * and the {@code pktNum} is lower than the least packet number the + * current decryption key has decrypted so far} + * + * @param pktNum the packet number for which the old key + * might be needed + */ + boolean canUseOldDecryptKey(final long pktNum) { + assert pktNum >= 0 : "unexpected packet number: " + pktNum; + if (this.old == null) { + return false; + } + final QuicReadCipher currentKey = this.current.readCipher; + final long lowestDecrypted = currentKey.lowestDecryptedPktNum(); + // if the incoming packet number is lesser than the lowest + // decrypted packet number by the current key, then it + // implies that this might be a delayed packet and thus is + // allowed to use the old key (if available) from + // the previous key phase. + // see RFC-9001, section 6.5 + if (lowestDecrypted == -1) { + return true; + } + return pktNum < lowestDecrypted; + } + } + + // will be set when the keys are derived + private volatile QuicVersion negotiatedVersion; + + private final Lock keySeriesLock = new ReentrantLock(); + // will be set when keys are derived and will + // be updated whenever keys are updated. + // Must be updated/written only + // when holding the keySeriesLock lock + private volatile KeySeries keySeries; + + @Stable + private volatile QuicOneRttContext oneRttContext; + + OneRttKeyManager() { + super(ONE_RTT); + } + + @Override + protected boolean keysAvailable() { + return this.keySeries != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.writeCipher; + } + + @Override + void discardKeys() { + this.keysDiscarded = true; + final KeySeries series; + this.keySeriesLock.lock(); + try { + series = this.keySeries; + this.keySeries = null; // no longer available + } finally { + this.keySeriesLock.unlock(); + } + if (series == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding key (series) of " + + this.keySpace + " key space"); + } + if (series.old != null) { + series.old.discard(false); + } + discardKeys(series.current); + discardKeys(series.next); + } + + @Override + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + QuicTransportException, AEADBadTagException, ShortBufferException { + if (keyPhase != 0 && keyPhase != 1) { + throw new IllegalArgumentException("Unexpected key phase " + + "value: " + keyPhase); + } + final KeySeries series = requireKeySeries(); + final CipherPair current = series.current; + // Use the write cipher's key phase to detect a key update as noted + // in RFC-9001, section 6.2: + // An endpoint detects a key update when processing a packet with + // a key phase that differs from the value used to protect the + // last packet it sent. + final int currentKeyPhase = current.writeCipher.getKeyPhase(); + if (keyPhase == currentKeyPhase) { + current.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + return; + } + // incoming packet is using a key phase which doesn't match the + // current key phase. this implies that either a key update + // is being initiated or a key update initiated by the current + // endpoint is in progress and some older packet with the + // previous key phase has arrived. + if (series.canUseOldDecryptKey(packetNumber)) { + final QuicReadCipher oldReadCipher = series.old; + assert oldReadCipher != null : "old key is unexpectedly null"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("using old read key to decrypt packet: " + + packetNumber + ", with incoming key phase: " + + keyPhase + ", current key phase: " + + currentKeyPhase); + } + oldReadCipher.decryptPacket( + packetNumber, packet, headerLength, output); + // we were able to decrypt using an old key. now verify + // that it was OK to use this old key for this packet. + if (!series.current.usedByBothEndpoints() + && series.current.writeCipher.hasEncryptedAny() + && oneRttContext.getLargestPeerAckedPN() + >= series.current.writeCipher.lowestEncryptedPktNum()) { + // RFC-9001, section 6.2: + // An endpoint that receives an acknowledgment that is + // carried in a packet protected with old keys where any + // acknowledged packet was protected with newer keys MAY + // treat that as a connection error of type + // KEY_UPDATE_ERROR. This indicates that a peer has + // received and acknowledged a packet that initiates a key + // update, but has not updated keys in response. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("peer used incorrect key, was" + + " expected to use updated key of" + + " key phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + throw new QuicTransportException("peer used incorrect" + + " key, was expected to use updated key", + this.keySpace, 0, KEY_UPDATE_ERROR); + } + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("detected ONE_RTT key update, current key " + + "phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + decryptUsingNextKeys( + series, packetNumber, packet, headerLength, output); + } + + @Override + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + KeySeries currentSeries = requireKeySeries(); + if (currentSeries.next == null) { + // next keys haven't yet been generated, + // generate them now + try { + currentSeries = generateNextKeys( + this.negotiatedVersion, currentSeries); + } catch (GeneralSecurityException | IOException e) { + throw new QuicTransportException("Failed to update keys", + ONE_RTT, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + maybeInitiateKeyUpdate(currentSeries, packetNumber); + // call getWriteCipher() afresh so that it can use + // the new keyseries if at all the key update was + // initiated + final QuicWriteCipher writeCipher = getWriteCipher(); + final int keyPhase = writeCipher.getKeyPhase(); + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(keyPhase); + writeCipher.encryptPacket(packetNumber, header, packetPayload, output); + } + + void setOneRttContext(final QuicOneRttContext ctx) { + Objects.requireNonNull(ctx); + this.oneRttContext = ctx; + } + + private KeySeries requireKeySeries() + throws QuicKeyUnavailableException { + final KeySeries series = this.keySeries; + if (series != null) { + return series; + } + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + + // based on certain internal criteria, this method may trigger a key + // update. + // returns true if it does trigger the key update. false otherwise. + private boolean maybeInitiateKeyUpdate(final KeySeries currentSeries, + final long packetNumber) { + final QuicWriteCipher cipher = currentSeries.current.writeCipher; + // when we notice that we have reached 80% (which is arbitrary) + // of the confidentiality limit, we trigger a key update instead + // of waiting to hit the limit + final long confidentialityLimit = cipher.confidentialityLimit(); + if (confidentialityLimit < 0) { + return false; + } + final long numEncrypted = cipher.getNumEncrypted(); + if (numEncrypted >= 0.8 * confidentialityLimit) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("about to reach confidentiality limit, " + + "attempting to initiate a 1-RTT key update," + + " packet number: " + + packetNumber + ", current key phase: " + + cipher.getKeyPhase()); + } + final boolean initiated = initiateKeyUpdate(currentSeries); + if (initiated) { + final int newKeyPhase = + this.keySeries.current.writeCipher.getKeyPhase(); + assert cipher.getKeyPhase() != newKeyPhase + : "key phase of updated key unexpectedly matches " + + "the key phase " + + cipher.getKeyPhase() + " of current keys"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "1-RTT key update initiated, new key phase: " + + newKeyPhase); + } + } + return initiated; + } + return false; + } + + private boolean initiateKeyUpdate(final KeySeries series) { + // we only initiate a key update if this current endpoint and the + // peer have both been using this current key + if (!series.current.usedByBothEndpoints()) { + // RFC-9001, section 6.1: + // An endpoint MUST NOT initiate a subsequent key update + // unless it has received + // an acknowledgment for a packet that was sent protected + // with keys from the + // current key phase. This ensures that keys are + // available to both peers before + // another key update can be initiated. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "skipping key update initiation because peer " + + "hasn't yet sent us a packet encrypted with " + + "current key of key phase: " + + series.current.readCipher.getKeyPhase()); + } + return false; + } + // OK to initiate a key update. + // An endpoint initiates a key update by updating its packet + // protection write secret + // and using that to protect new packets. + rolloverKeys(this.negotiatedVersion, series); + return true; + } + + private static void discardKeys(final CipherPair cipherPair) { + if (cipherPair == null) { + return; + } + cipherPair.discard(true); + } + + /** + * uses "next" keys to try and decrypt the incoming packet. if that + * succeeded then it implies that the key update was indeed initiated by + * the peer and this method then rolls over the keys to start using + * these "next" keys. this method then returns true in such cases. if + * the packet decryption using the "next" key fails, then this method + * just returns back false (and doesn't roll over the keys) + */ + private void decryptUsingNextKeys( + final KeySeries currentKeySeries, + final long packetNumber, + final ByteBuffer packet, + final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + ShortBufferException, QuicTransportException { + if (currentKeySeries.next == null) { + // this can happen if the peer initiated another + // key update before we could generate the next + // keys during our encryption flow. in such + // cases we reject the key update for the packet + // (we avoid timing attacks by not generating + // keys during decryption, our key generation + // only happens during encryption) + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("next keys unavailable," + + " won't decrypt a packet which appears to be" + + " a key update"); + } + throw new QuicKeyUnavailableException( + "next keys unavailable to handle key update", + this.keySpace); + } + // use the next keys to attempt decrypting + currentKeySeries.next.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "decrypted using next keys for peer-initiated" + + " key update; will now switch to new key phase: " + + currentKeySeries.next.readCipher.getKeyPhase()); + } + // we have successfully decrypted the packet using the new/next + // read key. So we now update even the write key as noted in + // RFC-9001, section 6.2: + // If a packet is successfully processed using the next key and + // IV, then the peer has initiated a key update. The endpoint + // MUST update its send keys to the corresponding + // key phase in response, as described in Section 6.1. Sending + // keys MUST be updated before sending an acknowledgment for the + // packet that was received with updated keys. rollover the + // keys == old gets discarded and is replaced by + // current, current is replaced by next and next is set to null + // (a new set of next keys will be generated separately on + // a schedule) + rolloverKeys(this.negotiatedVersion, currentKeySeries); + } + + void deriveKeys(final QuicVersion negotiatedVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(negotiatedVersion); + if (keysAvailable()) { + throw new IllegalStateException("Keys already derived for " + + this.keySpace + " key space"); + } + this.negotiatedVersion = negotiatedVersion; + + try { + SSLKeyDerivation kd = handshakeContext.handshakeKeyDerivation; + SecretKey client_application_traffic_secret_0 = kd.deriveKey( + "TlsClientAppTrafficSecret"); + SecretKey server_application_traffic_secret_0 = kd.deriveKey( + "TlsServerAppTrafficSecret"); + + deriveOneRttKeys(this.negotiatedVersion, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + handshakeContext.negotiatedCipherSuite, + clientMode); + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } + } + + void deriveOneRttKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + final QuicKeys clientKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + client_application_traffic_secret_0); + final QuicKeys serverKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + server_application_traffic_secret_0); + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + // this method always derives the first key for the 1-RTT, so key + // phase is always 0 + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + } + // generate the next set of keys beforehand to prevent any timing + // attacks + // during key update + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, writeCipher); + this.keySeriesLock.lock(); + try { + if (this.keySeries != null) { + // don't allow deriving the first set of 1-RTT keys more + // than once + throw new IllegalStateException("Keys already available " + + "for keyspace: " + + this.keySpace); + } + this.keySeries = new KeySeries(null, + new CipherPair(readCipher, writeCipher), + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + } finally { + this.keySeriesLock.unlock(); + } + } + + private static QuicWriteCipher generateNextWriteCipher( + final QuicVersion quicVersion, final QuicWriteCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nplus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nplus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicWriteCipher next = + QuicCipher.createWriteCipher(current.getCipherSuite(), + nplus1Secret, quicKeys.key, quicKeys.iv, + quicKeys.hp, nextKeyPhase); + return next; + } + + private static QuicReadCipher generateNextReadCipher( + final QuicVersion quicVersion, final QuicReadCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nPlus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nPlus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicReadCipher next = + QuicCipher.createReadCipher(current.getCipherSuite(), + nPlus1Secret, quicKeys.key, + quicKeys.iv, quicKeys.hp, nextKeyPhase); + return next; + } + + private KeySeries generateNextKeys(final QuicVersion version, + final KeySeries currentSeries) + throws GeneralSecurityException, IOException { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, + currentSeries.current.readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, + currentSeries.current.writeCipher); + // only the next keys will differ in the new series + // as compared to the current series + final KeySeries newSeries = new KeySeries(currentSeries.old, + currentSeries.current, + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + this.keySeries = newSeries; + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + + /** + * Updates the key series by "left shifting" the series of keys. + * i.e. old keys (if any) are discarded, current keys + * are moved to old keys and next keys are moved to current keys. + * Note that no new keys will be generated by this method. + * @return the key series that will be in use going forward + */ + private KeySeries rolloverKeys(final QuicVersion version, + final KeySeries currentSeries) { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + assert currentSeries.next != null : "Key series missing next" + + " keys"; + // discard the old read cipher which will no longer be used + final QuicReadCipher oldReadCipher = currentSeries.old; + // once we move current key to old, we won't be using the + // write cipher of that + // moved pair + final QuicWriteCipher writeCipherToDiscard = + currentSeries.current.writeCipher; + final KeySeries newSeries = new KeySeries( + currentSeries.current.readCipher, currentSeries.next, + null); + // update the key series + this.keySeries = newSeries; + if (oldReadCipher != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "discarding old read key of key phase: " + + oldReadCipher.getKeyPhase()); + } + oldReadCipher.discard(false); + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding write key of key phase: " + + writeCipherToDiscard.getKeyPhase()); + } + writeCipherToDiscard.discard(false); + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + } + + private static final class QuicTLSKeyDerivation + implements SSLKeyDerivation { + + private enum HkdfLabel { + // RFC 9001: quic version 1 + quickey("quic key"), + quiciv("quic iv"), + quichp("quic hp"), + quicku("quic ku"), + + // RFC 9369: quic version 2 + quicv2key("quicv2 key"), + quicv2iv("quicv2 iv"), + quicv2hp("quicv2 hp"), + quicv2ku("quicv2 ku"); + + private final String label; + private final byte[] tls13LabelBytes; + + HkdfLabel(final String label) { + Objects.requireNonNull(label); + this.label = label; + this.tls13LabelBytes = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + } + + private static HkdfLabel fromLabel(final String label) { + Objects.requireNonNull(label); + for (final HkdfLabel hkdfLabel : HkdfLabel.values()) { + if (hkdfLabel.label.equals(label)) { + return hkdfLabel; + } + } + throw new IllegalArgumentException( + "unrecognized label: " + label); + } + } + + private final CipherSuite cs; + private final SecretKey secret; + + private QuicTLSKeyDerivation(final CipherSuite cs, + final SecretKey secret) { + this.cs = Objects.requireNonNull(cs); + this.secret = Objects.requireNonNull(secret); + } + + @Override + public SecretKey deriveKey(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + final String keyAlgo = getKeyAlgorithm(hkdfLabel); + return hkdf.deriveKey(keyAlgo, + HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + @Override + public byte[] deriveData(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + return hkdf.deriveData(HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + private int getKeyLength(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> { + // RFC-9001, section 6.1: + // secret_ = HKDF-Expand-Label(secret_, "quic + // ku", "", Hash.length) + yield this.cs.hashAlg.hashLength; + } + case quiciv, quicv2iv -> this.cs.bulkCipher.ivSize; + default -> this.cs.bulkCipher.keySize; + }; + } + + private String getKeyAlgorithm(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> "TlsUpdateNplus1"; + case quiciv, quicv2iv -> + throw new IllegalArgumentException("IV not expected"); + default -> this.cs.bulkCipher.algorithm; + }; + } + } + + private enum QuicTLSData { + V1("38762cf7f55934b34d179ae6a4c80cadccbb7f0a", + "be0c690b9f66575a1d766b54e368c84e", + "461599d35d632bf2239825bb", + "quic key", "quic iv", "quic hp", "quic ku"), + V2("0dede3def700a6db819381be6e269dcbf9bd2ed9", + "8fb4b01b56ac48e260fbcbcead7ccc92", + "d86969bc2d7c6d9990efb04a", + "quicv2 key", "quicv2 iv", "quicv2 hp", "quicv2 ku"); + + private final byte[] initialSalt; + private final SecretKey retryKey; + private final GCMParameterSpec retryIvSpec; + private final String keyLabel; + private final String ivLabel; + private final String hpLabel; + private final String kuLabel; + + QuicTLSData(String initialSalt, String retryKey, String retryIv, + String keyLabel, String ivLabel, String hpLabel, + String kuLabel) { + this.initialSalt = HexFormat.of() + .parseHex(initialSalt); + this.retryKey = new SecretKeySpec(HexFormat.of() + .parseHex(retryKey), "AES"); + retryIvSpec = new GCMParameterSpec(128, + HexFormat.of().parseHex(retryIv)); + this.keyLabel = keyLabel; + this.ivLabel = ivLabel; + this.hpLabel = hpLabel; + this.kuLabel = kuLabel; + } + + public byte[] getInitialSalt() { + return initialSalt; + } + + public Cipher getRetryCipher(boolean incoming) throws QuicTransportException { + Cipher retryCipher = null; + try { + retryCipher = Cipher.getInstance("AES/GCM/NoPadding"); + retryCipher.init(incoming ? Cipher.DECRYPT_MODE : + Cipher.ENCRYPT_MODE, + retryKey, retryIvSpec); + } catch (Exception e) { + throw new QuicTransportException("Cipher not available", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + return retryCipher; + } + + public String getTlsKeyLabel() { + return keyLabel; + } + + public String getTlsIvLabel() { + return ivLabel; + } + + public String getTlsHpLabel() { + return hpLabel; + } + + public String getTlsKeyUpdateLabel() { + return kuLabel; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java new file mode 100644 index 00000000000..6765f554fcc --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java @@ -0,0 +1,893 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package sun.security.ssl; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; +import sun.security.ssl.QuicKeyManager.HandshakeKeyManager; +import sun.security.ssl.QuicKeyManager.InitialKeyManager; +import sun.security.ssl.QuicKeyManager.OneRttKeyManager; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.security.AlgorithmConstraints; +import java.security.GeneralSecurityException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicTLSEngine.HandshakeState.*; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.*; + +/** + * One instance per QUIC connection. Configuration methods similar to + * SSLEngine. + *

      + * The implementation of this class uses the {@link QuicKeyManager} to maintain + * all state relating to keys for each encryption levels. + */ +public final class QuicTLSEngineImpl implements QuicTLSEngine, SSLTransport { + + private static final Map messageTypeMap = + Map.of(SSLHandshake.CLIENT_HELLO.id, INITIAL, + SSLHandshake.SERVER_HELLO.id, INITIAL, + SSLHandshake.ENCRYPTED_EXTENSIONS.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_REQUEST.id, HANDSHAKE, + SSLHandshake.CERTIFICATE.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_VERIFY.id, HANDSHAKE, + SSLHandshake.FINISHED.id, HANDSHAKE, + SSLHandshake.NEW_SESSION_TICKET.id, ONE_RTT); + static final long BASE_CRYPTO_ERROR = 256; + + private static final Set SUPPORTED_QUIC_VERSIONS = + Set.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + + // VarHandles are used to access compareAndSet semantics. + private static final VarHandle HANDSHAKE_STATE_HANDLE; + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + final Class quicTlsEngineImpl = QuicTLSEngineImpl.class; + HANDSHAKE_STATE_HANDLE = lookup.findVarHandle( + quicTlsEngineImpl, + "handshakeState", + HandshakeState.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private final TransportContext conContext; + private final String peerHost; + private final int peerPort; + private volatile HandshakeState handshakeState; + private volatile KeySpace sendKeySpace; + // next message to send or receive + private volatile ByteBuffer localQuicTransportParameters; + private volatile QuicTransportParametersConsumer + remoteQuicTransportParametersConsumer; + + // keymanagers for individual keyspaces + private final InitialKeyManager initialKeyManager = new InitialKeyManager(); + private final HandshakeKeyManager handshakeKeyManager = + new HandshakeKeyManager(); + private final OneRttKeyManager oneRttKeyManager = new OneRttKeyManager(); + + // buffer for crypto data that was received but not yet processed (i.e. + // incomplete messages) + private volatile ByteBuffer incomingCryptoBuffer; + // key space for incomingCryptoBuffer + private volatile KeySpace incomingCryptoSpace; + + private volatile QuicVersion negotiatedVersion; + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl) { + this(sslContextImpl, null, -1); + } + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl, final String peerHost, final int peerPort) { + this.peerHost = peerHost; + this.peerPort = peerPort; + this.sendKeySpace = INITIAL; + HandshakeHash handshakeHash = new HandshakeHash(); + this.conContext = new TransportContext(sslContextImpl, this, + new SSLEngineInputRecord(handshakeHash), + new QuicEngineOutputRecord(handshakeHash)); + conContext.sslConfig.enabledProtocols = List.of(ProtocolVersion.TLS13); + if (peerHost != null) { + conContext.sslConfig.serverNames = + Utilities.addToSNIServerNameList( + conContext.sslConfig.serverNames, peerHost); + } + conContext.setQuic(true); + } + + @Override + public void setUseClientMode(boolean mode) { + conContext.setUseClientMode(mode); + this.handshakeState = mode + ? HandshakeState.NEED_SEND_CRYPTO + : HandshakeState.NEED_RECV_CRYPTO; + } + + @Override + public boolean getUseClientMode() { + return conContext.sslConfig.isClientMode; + } + + @Override + public void setSSLParameters(final SSLParameters params) { + Objects.requireNonNull(params); + // section, 4.2 of RFC-9001 + // Clients MUST NOT offer TLS versions older than 1.3 + final String[] protos = params.getProtocols(); + if (protos == null || protos.length == 0) { + throw new IllegalArgumentException("No TLS protocols set"); + } + boolean tlsv13Present = false; + Set unsupported = new HashSet<>(); + for (String p : protos) { + if ("TLSv1.3".equals(p)) { + tlsv13Present = true; + } else { + unsupported.add(p); + } + } + if (!tlsv13Present) { + throw new IllegalArgumentException( + "required TLSv1.3 protocol version hasn't been set"); + } + if (!unsupported.isEmpty()) { + throw new IllegalArgumentException( + "Unsupported TLS protocol versions " + unsupported); + } + conContext.sslConfig.setSSLParameters(params); + } + + @Override + public SSLSession getSession() { + return conContext.conSession; + } + + @Override + public SSLSession getHandshakeSession() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + return handshakeContext == null + ? null + : handshakeContext.handshakeSession; + } + + /** + * {@return the {@link AlgorithmConstraints} that are applicable for this engine, + * or null if none are applicable} + */ + AlgorithmConstraints getAlgorithmConstraints() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + // if we are handshaking then use the handshake context + // to determine the constraints, else use the configured + // SSLParameters + return handshakeContext == null + ? getSSLParameters().getAlgorithmConstraints() + : handshakeContext.sslConfig.userSpecifiedAlgorithmConstraints; + } + + @Override + public SSLParameters getSSLParameters() { + return conContext.sslConfig.getSSLParameters(); + } + + @Override + public String getApplicationProtocol() { + // TODO: review thread safety when dealing with conContext + return conContext.applicationProtocol; + } + + @Override + public Set getSupportedQuicVersions() { + return SUPPORTED_QUIC_VERSIONS; + } + + @Override + public void setOneRttContext(final QuicOneRttContext ctx) { + this.oneRttKeyManager.setOneRttContext(ctx); + } + + private QuicVersion getNegotiatedVersion() { + final QuicVersion negotiated = this.negotiatedVersion; + if (negotiated == null) { + throw new IllegalStateException( + "Quic version hasn't been negotiated yet"); + } + return negotiated; + } + + private boolean isEnabled(final QuicVersion quicVersion) { + final Set enabled = getSupportedQuicVersions(); + if (enabled == null) { + return false; + } + return enabled.contains(quicVersion); + } + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + @Override + public HandshakeState getHandshakeState() { + return handshakeState; + } + + /** + * Returns the current sending key space (encryption level) + * + * @return the current sending key space + */ + @Override + public KeySpace getCurrentSendKeySpace() { + return sendKeySpace; + } + + @Override + public boolean keysAvailable(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager.keysAvailable(); + case HANDSHAKE -> this.handshakeKeyManager.keysAvailable(); + case ONE_RTT -> this.oneRttKeyManager.keysAvailable(); + case ZERO_RTT -> false; + case RETRY -> true; + default -> throw new IllegalArgumentException( + keySpace + " not expected here"); + }; + } + + @Override + public void discardKeys(KeySpace keySpace) { + switch (keySpace) { + case INITIAL -> this.initialKeyManager.discardKeys(); + case HANDSHAKE -> this.handshakeKeyManager.discardKeys(); + case ONE_RTT -> this.oneRttKeyManager.discardKeys(); + default -> throw new IllegalArgumentException( + "key discarding not implemented for " + keySpace); + } + } + + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL, HANDSHAKE, ZERO_RTT, ONE_RTT -> 16; + default -> throw new IllegalArgumentException( + "Type '" + keySpace + "' not expected here"); + }; + } + + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicKeyManager keyManager = keyManager(keySpace); + if (incoming) { + final QuicCipher.QuicReadCipher quicCipher = + keyManager.getReadCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } else { + final QuicCipher.QuicWriteCipher quicCipher = + keyManager.getWriteCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } + } + + @Override + public int getAuthTagSize() { + // RFC-9001, section 5.3 + // QUIC can use any of the cipher suites defined in [TLS13] with the + // exception of TLS_AES_128_CCM_8_SHA256. ... + // These cipher suites have a 16-byte authentication tag and produce + // an output 16 bytes larger than their input. + return 16; + } + + @Override + public void encryptPacket(final KeySpace keySpace, final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.encryptPacket(packetNumber, headerGenerator, packetPayload, output); + } + + @Override + public void decryptPacket(final KeySpace keySpace, + final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + QuicTransportException, ShortBufferException { + if (keySpace == ONE_RTT && !isTLSHandshakeComplete()) { + // RFC-9001, section 5.7 specifies that the server or the client MUST NOT + // decrypt 1-RTT packets, even if 1-RTT keys are available, before the + // TLS handshake is complete. + throw new QuicKeyUnavailableException("QUIC TLS handshake not yet complete", ONE_RTT); + } + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.decryptPacket(packetNumber, keyPhase, packet, headerLength, + output); + } + + @Override + public void signRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, final ByteBuffer packet, + final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + final Cipher cipher = InitialKeyManager.getRetryCipher( + quicVersion, false); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + try { + // No data to encrypt, just outputting the tag which will be + // verified later. + cipher.doFinal(ByteBuffer.allocate(0), output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + @Override + public void verifyRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, + final ByteBuffer packet) + throws AEADBadTagException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + int originalLimit = packet.limit(); + packet.limit(originalLimit - 16); + final Cipher cipher = + InitialKeyManager.getRetryCipher(quicVersion, true); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + packet.limit(originalLimit); + try { + assert packet.remaining() == 16; + int outBufLength = cipher.getOutputSize(packet.remaining()); + // No data to decrypt, just checking the tag. + ByteBuffer outBuffer = ByteBuffer.allocate(outBufLength); + cipher.doFinal(packet, outBuffer); + assert outBuffer.position() == 0; + } catch (AEADBadTagException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to verify packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + private QuicKeyManager keyManager(final KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager; + case HANDSHAKE -> this.handshakeKeyManager; + case ONE_RTT -> this.oneRttKeyManager; + default -> throw new IllegalArgumentException( + "No key manager available for key space: " + keySpace); + }; + } + + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException { + if (keySpace != sendKeySpace) { + throw new IllegalStateException("Unexpected key space: " + + keySpace + " (expected " + sendKeySpace + ")"); + } + if (handshakeState == HandshakeState.NEED_SEND_CRYPTO || + !conContext.outputRecord.isEmpty()) { // session ticket + byte[] bytes = produceNextHandshakeMessage(); + return ByteBuffer.wrap(bytes); + } else { + return null; + } + } + + private byte[] produceNextHandshakeMessage() throws IOException { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + byte[] message = conContext.outputRecord.getHandshakeMessage(); + if (handshakeState == NEED_SEND_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // client, done + handshakeState = NEED_RECV_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + handshakeState = NEED_RECV_CRYPTO; + } + } else if (sendKeySpace == INITIAL && !getUseClientMode()) { + // Server sends handshake messages immediately after + // the initial server hello. Need to check the next key space. + sendKeySpace = conContext.outputRecord.getHandshakeMessageKeySpace(); + } + } else { + assert conContext.isNegotiated; + } + return message; + } + + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new IllegalArgumentException("Empty crypto buffer"); + } + if (keySpace == KeySpace.ZERO_RTT) { + throw new IllegalArgumentException("Crypto in zero-rtt"); + } + if (incomingCryptoSpace != null && incomingCryptoSpace != keySpace) { + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Unfinished message in " + incomingCryptoSpace)); + } + try { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + } catch (IOException e) { + throw new QuicTransportException(e.toString(), null, 0, + BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + // previously unconsumed bytes in incomingCryptoBuffer, new bytes in + // payload. if incomingCryptoBuffer is not null, it's either 4 bytes + // or large enough to hold the entire message. + while (payload.hasRemaining()) { + if (keySpace != KeySpace.ONE_RTT && + handshakeState != HandshakeState.NEED_RECV_CRYPTO) { + // in one-rtt we may receive session tickets at any time; + // during handshake we're either sending or receiving + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Not expecting a handshake message, state: " + + handshakeState)); + } + if (incomingCryptoBuffer != null) { + // message type validated already; pump more bytes + if (payload.remaining() <= incomingCryptoBuffer.remaining()) { + incomingCryptoBuffer.put(payload); + } else { + // more than one message in buffer, or we don't have a + // header yet + int remaining = incomingCryptoBuffer.remaining(); + incomingCryptoBuffer.put(incomingCryptoBuffer.position(), + payload, payload.position(), remaining); + incomingCryptoBuffer.position(incomingCryptoBuffer.limit()); + payload.position(payload.position() + remaining); + if (incomingCryptoBuffer.capacity() == 4) { + // small buffer for header only; retrieve size and + // expand if necessary + int messageSize = + ((incomingCryptoBuffer.get(1) & 0xFF) << 16) | + ((incomingCryptoBuffer.get(2) & 0xFF) << 8) | + (incomingCryptoBuffer.get(3) & 0xFF); + if (messageSize != 0) { + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + ByteBuffer newBuffer = + ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.flip(); + newBuffer.put(incomingCryptoBuffer); + incomingCryptoBuffer = newBuffer; + assert incomingCryptoBuffer.position() == 4 : + incomingCryptoBuffer.position(); + // start over with larger buffer + continue; + } + // message size was zero... can it really happen? + } + } + } else { + // incoming crypto buffer is null. Validate message type, + // check if size is available + byte messageType = payload.get(payload.position()); + if (SSLLogger.isOn) { + SSLLogger.fine("Received message of type 0x" + + Integer.toHexString(messageType & 0xFF)); + } + KeySpace expected = messageTypeMap.get(messageType); + if (expected != keySpace) { + throw new QuicTransportException("Unexpected message", + null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException("Message " + messageType + + " received in " + keySpace + + " but should be " + expected)); + } + if (payload.remaining() < 4) { + // partial message, length missing. Store in + // incomingCryptoBuffer + incomingCryptoBuffer = ByteBuffer.allocate(4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + int payloadPos = payload.position(); + int messageSize = ((payload.get(payloadPos + 1) & 0xFF) << 16) + | ((payload.get(payloadPos + 2) & 0xFF) << 8) + | (payload.get(payloadPos + 3) & 0xFF); + if (payload.remaining() < messageSize + 4) { + // partial message, length known. Store in + // incomingCryptoBuffer + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + incomingCryptoBuffer = ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + incomingCryptoSpace = keySpace; + incomingCryptoBuffer = payload.slice(payloadPos, + messageSize + 4); + // set position at end to indicate that the buffer is ready + // for processing + incomingCryptoBuffer.position(messageSize + 4); + assert !incomingCryptoBuffer.hasRemaining() : + incomingCryptoBuffer.remaining(); + payload.position(payloadPos + messageSize + 4); + } + if (!incomingCryptoBuffer.hasRemaining()) { + incomingCryptoBuffer.flip(); + handleHandshakeMessage(keySpace, incomingCryptoBuffer); + incomingCryptoBuffer = null; + incomingCryptoSpace = null; + } else { + assert !payload.hasRemaining() : payload.remaining(); + return; + } + } + } + + private void handleHandshakeMessage(KeySpace keySpace, ByteBuffer message) + throws QuicTransportException { + // message param contains one whole TLS message + boolean useClientMode = getUseClientMode(); + byte messageType = message.get(); + int messageSize = ((message.get() & 0xFF) << 16) + | ((message.get() & 0xFF) << 8) + | (message.get() & 0xFF); + + assert message.remaining() == messageSize : + message.remaining() - messageSize; + try { + if (conContext.inputRecord.handshakeHash.isHashable(messageType)) { + ByteBuffer temp = message.duplicate(); + temp.position(0); + conContext.inputRecord.handshakeHash.receive(temp); + } + if (conContext.handshakeContext == null) { + if (!conContext.isNegotiated) { + throw new QuicTransportException( + "Cannot process crypto message, broken: " + + conContext.isBroken, + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + conContext.handshakeContext = + new PostHandshakeContext(conContext); + } + conContext.handshakeContext.dispatch(messageType, message.slice()); + } catch (SSLHandshakeException e) { + if (e.getCause() instanceof QuicTransportException qte) { + // rethrow quic transport parameters validation exception + throw qte; + } + Alert alert = ((QuicEngineOutputRecord) + conContext.outputRecord).getAlert(); + throw new QuicTransportException(alert.description, keySpace, 0, + BASE_CRYPTO_ERROR + alert.id, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (handshakeState == NEED_RECV_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // dead code? done, server side, no session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // expect more messages + // client side: if we're still in INITIAL, switch + // to HANDSHAKE + if (sendKeySpace == INITIAL) { + sendKeySpace = HANDSHAKE; + } + } + } else { + // our turn to send + if (conContext.isNegotiated && !useClientMode) { + // done, server side, wants to send session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // more messages needed to finish handshake + handshakeState = HandshakeState.NEED_SEND_CRYPTO; + } + } + } else { + assert conContext.isNegotiated; + } + } + + @Override + public void deriveInitialKeys(final QuicVersion quicVersion, + final ByteBuffer connectionId) throws IOException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " isn't enabled"); + } + final byte[] connectionIdBytes = new byte[connectionId.remaining()]; + connectionId.get(connectionIdBytes); + this.initialKeyManager.deriveKeys(quicVersion, connectionIdBytes, + getUseClientMode()); + } + + @Override + public void versionNegotiated(final QuicVersion quicVersion) { + Objects.requireNonNull(quicVersion); + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " is not enabled"); + } + synchronized (this) { + final QuicVersion prevNegotiated = this.negotiatedVersion; + if (prevNegotiated != null) { + throw new IllegalStateException("A Quic version has already " + + "been negotiated previously"); + } + this.negotiatedVersion = quicVersion; + } + } + + public void deriveHandshakeKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.handshakeKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + public void deriveOneRTTKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.oneRttKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + // for testing (PacketEncryptionTest) + void deriveOneRTTKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + this.oneRttKeyManager.deriveOneRttKeys(version, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + negotiatedCipherSuite, clientMode); + } + + @Override + public Runnable getDelegatedTask() { + // TODO: actually delegate tasks + return null; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public boolean useDelegatedTask() { + return true; + } + + public byte[] getLocalQuicTransportParameters() { + ByteBuffer ltp = localQuicTransportParameters; + if (ltp == null) { + return null; + } + byte[] result = new byte[ltp.remaining()]; + ltp.get(0, result); + return result; + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + this.localQuicTransportParameters = params; + } + + @Override + public void restartHandshake() throws IOException { + if (negotiatedVersion != null) { + throw new IllegalStateException("Version already negotiated"); + } + if (sendKeySpace != INITIAL || handshakeState != NEED_RECV_CRYPTO) { + throw new IllegalStateException("Unexpected handshake state"); + } + HandshakeContext context = conContext.handshakeContext; + ClientHandshakeContext chc = (ClientHandshakeContext)context; + + // Refresh handshake hash + chc.handshakeHash.finish(); // reset the handshake hash + + // Update the initial ClientHello handshake message. + chc.initialClientHelloMsg.extensions.reproduce(chc, + new SSLExtension[] { + SSLExtension.CH_QUIC_TRANSPORT_PARAMETERS, + SSLExtension.CH_PRE_SHARED_KEY + }); + + // produce handshake message + chc.initialClientHelloMsg.write(chc.handshakeOutput); + handshakeState = NEED_SEND_CRYPTO; + } + + @Override + public void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer) { + this.remoteQuicTransportParametersConsumer = consumer; + } + + void processRemoteQuicTransportParameters(ByteBuffer buffer) + throws QuicTransportException{ + remoteQuicTransportParametersConsumer.accept(buffer); + } + + @Override + public boolean tryMarkHandshakeDone() { + if (getUseClientMode()) { + // not expected to be called on client + throw new IllegalStateException( + "Not expected to be called in client mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_SEND_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine("QuicTLSEngine (server) marked handshake " + + "state as HANDSHAKE_CONFIRMED"); + } + } + return confirmed; + } + + @Override + public boolean tryReceiveHandshakeDone() { + final boolean isClient = getUseClientMode(); + if (!isClient) { + throw new IllegalStateException( + "Not expected to receive HANDSHAKE_DONE in server mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_RECV_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine( + "QuicTLSEngine (client) received HANDSHAKE_DONE," + + " marking state as HANDSHAKE_DONE"); + } + } + return confirmed; + } + + @Override + public boolean isTLSHandshakeComplete() { + final boolean isClient = getUseClientMode(); + final HandshakeState hsState = this.handshakeState; + if (isClient) { + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and is waiting for the server + // to send QUIC HANDSHAKE_DONE frame. + // OR + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and has even received the + // QUIC HANDSHAKE_DONE frame. + // Either of these implies the TLS handshake is complete for the client + return hsState == NEED_RECV_HANDSHAKE_DONE || hsState == HANDSHAKE_CONFIRMED; + } + // on the server side the TLS handshake is complete only when the server has + // sent a TLS Finished message and received the client's Finished message. + return hsState == HANDSHAKE_CONFIRMED; + } + + /** + * {@return the key phase being used when decrypting incoming 1-RTT + * packets} + */ + // this is only used in tests + public int getOneRttKeyPhase() throws QuicKeyUnavailableException { + return this.oneRttKeyManager.getReadCipher().getKeyPhase(); + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java new file mode 100644 index 00000000000..83e977ee446 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package sun.security.ssl; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.quic.QuicTransportException; +import sun.security.ssl.SSLExtension.ExtensionConsumer; +import sun.security.ssl.SSLHandshake.HandshakeMessage; + +/** + * Pack of the "quic_transport_parameters" extensions [RFC 9001]. + */ +final class QuicTransportParametersExtension { + + static final HandshakeProducer chNetworkProducer = + new T13CHQuicParametersProducer(); + static final ExtensionConsumer chOnLoadConsumer = + new T13CHQuicParametersConsumer(); + static final HandshakeAbsence chOnLoadAbsence = + new T13CHQuicParametersAbsence(); + static final HandshakeProducer eeNetworkProducer = + new T13EEQuicParametersProducer(); + static final ExtensionConsumer eeOnLoadConsumer = + new T13EEQuicParametersConsumer(); + static final HandshakeAbsence eeOnLoadAbsence = + new T13EEQuicParametersAbsence(); + + private static final class T13CHQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13CHQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + + } + + private static final class T13CHQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13CHQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + throw shc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Client sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw shc.conContext.fatal(Alert.DECODE_ERROR, e); + } + + } + } + + private static final class T13CHQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13CHQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in server side only. + ServerHandshakeContext shc = (ServerHandshakeContext)context; + + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw shc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Client did not send QUIC transport parameters"); + } + } + } + + private static final class T13EEQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13EEQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) { + + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + } + + private static final class T13EEQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13EEQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + throw chc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Server sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw chc.conContext.fatal(Alert.DECODE_ERROR, e); + } + } + } + + private static final class T13EEQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13EEQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + + if (chc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw chc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Server did not send QUIC transport parameters"); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java index 88cdfbca5ff..1d5a4c4e73d 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java @@ -29,19 +29,32 @@ import java.security.AlgorithmConstraints; import java.security.AlgorithmParameters; import java.security.CryptoPrimitive; import java.security.Key; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Set; +import java.util.TreeSet; import javax.net.ssl.*; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.util.DisabledAlgorithmConstraints; import static sun.security.util.DisabledAlgorithmConstraints.*; /** * Algorithm constraints for disabled algorithms property - * + *

      * See the "jdk.certpath.disabledAlgorithms" specification in java.security * for the syntax of the disabled algorithm string. */ final class SSLAlgorithmConstraints implements AlgorithmConstraints { + public enum SIGNATURE_CONSTRAINTS_MODE { + PEER, // Check against peer supported signatures + LOCAL // Check against local supported signatures + } + private static final DisabledAlgorithmConstraints tlsDisabledAlgConstraints = new DisabledAlgorithmConstraints(PROPERTY_TLS_DISABLED_ALGS, new SSLAlgorithmDecomposer()); @@ -57,14 +70,15 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // the default algorithm constraints static final SSLAlgorithmConstraints DEFAULT = - new SSLAlgorithmConstraints(null, true); + new SSLAlgorithmConstraints(null, true); // the default SSL only algorithm constraints static final SSLAlgorithmConstraints DEFAULT_SSL_ONLY = - new SSLAlgorithmConstraints(null, false); + new SSLAlgorithmConstraints(null, false); - private SSLAlgorithmConstraints(AlgorithmConstraints userSpecifiedConstraints, - boolean enabledX509DisabledAlgConstraints) { + private SSLAlgorithmConstraints( + AlgorithmConstraints userSpecifiedConstraints, + boolean enabledX509DisabledAlgConstraints) { this(userSpecifiedConstraints, null, enabledX509DisabledAlgConstraints); } @@ -81,10 +95,12 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the provided * {@code userSpecifiedConstraints} in addition to standard checks. * Returns a singleton instance if parameter is null or DEFAULT. + * * @param userSpecifiedConstraints additional constraints to check * @return a SSLAlgorithmConstraints instance */ - static SSLAlgorithmConstraints wrap(AlgorithmConstraints userSpecifiedConstraints) { + static SSLAlgorithmConstraints wrap( + AlgorithmConstraints userSpecifiedConstraints) { return wrap(userSpecifiedConstraints, true); } @@ -102,23 +118,24 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code socket} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param socket socket with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forSocket(SSLSocket socket, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(socket); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forSocket( SSLSocket socket, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (socket == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(socket)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + socket.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -126,23 +143,51 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code engine} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param engine engine with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forEngine(SSLEngine engine, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(engine); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forEngine( SSLEngine engine, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(engine)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + engine.getHandshakeSession(), mode), + withDefaultCertPathConstraints); + } + + /** + * Returns an {@link AlgorithmConstraints} instance that uses the + * constraints configured for the given {@code engine} in addition + * to the platform configured constraints. + *

      + * If the given {@code allowedAlgorithms} is non-null then the returned + * {@code AlgorithmConstraints} will only permit those allowed algorithms. + * + * @param engine QuicTLSEngine used to determine the constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE + * @param withDefaultCertPathConstraints whether or not to apply the default certpath + * algorithm constraints too + * @return a AlgorithmConstraints instance + */ + static AlgorithmConstraints forQUIC(QuicTLSEngine engine, + SIGNATURE_CONSTRAINTS_MODE mode, + boolean withDefaultCertPathConstraints) { + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + + return new SSLAlgorithmConstraints( + nullIfDefault(getUserSpecifiedConstraints(engine)), + new SupportedSignatureAlgorithmConstraints(engine.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -159,7 +204,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLEngineImpl. if (engine instanceof SSLEngineImpl) { HandshakeContext hc = - ((SSLEngineImpl)engine).conContext.handshakeContext; + ((SSLEngineImpl) engine).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -179,7 +224,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLSocketImpl. if (socket instanceof SSLSocketImpl) { HandshakeContext hc = - ((SSLSocketImpl)socket).conContext.handshakeContext; + ((SSLSocketImpl) socket).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -191,6 +236,17 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { return null; } + private static AlgorithmConstraints getUserSpecifiedConstraints( + QuicTLSEngine quicEngine) { + if (quicEngine != null) { + if (quicEngine instanceof QuicTLSEngineImpl engineImpl) { + return engineImpl.getAlgorithmConstraints(); + } + return quicEngine.getSSLParameters().getAlgorithmConstraints(); + } + return null; + } + @Override public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { @@ -279,15 +335,55 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { } private static class SupportedSignatureAlgorithmConstraints - implements AlgorithmConstraints { - // supported signature algorithms - private final String[] supportedAlgorithms; + implements AlgorithmConstraints { - SupportedSignatureAlgorithmConstraints(String[] supportedAlgorithms) { - if (supportedAlgorithms != null) { - this.supportedAlgorithms = supportedAlgorithms.clone(); - } else { - this.supportedAlgorithms = null; + // Supported signature algorithms + private Set supportedAlgorithms; + // Supported signature schemes + private List supportedSignatureSchemes; + private boolean checksDisabled; + + SupportedSignatureAlgorithmConstraints( + SSLSession session, SIGNATURE_CONSTRAINTS_MODE mode) { + + if (mode == null + || !(session instanceof ExtendedSSLSession extSession + // "signature_algorithms_cert" TLS extension is only + // available starting with TLSv1.2. + && ProtocolVersion.useTLS12PlusSpec( + extSession.getProtocol()))) { + + checksDisabled = true; + return; + } + + supportedAlgorithms = new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getPeerSupportedSignatureAlgorithms())); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getLocalSupportedSignatureAlgorithms())); + } + + // Do additional SignatureSchemes checks for in-house + // ExtendedSSLSession implementation. + if (extSession instanceof SSLSessionImpl sslSessionImpl) { + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getPeerSupportedSignatureSchemes()); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getLocalSupportedSignatureSchemes()); + } } } @@ -295,6 +391,10 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { + if (checksDisabled) { + return true; + } + if (algorithm == null || algorithm.isEmpty()) { throw new IllegalArgumentException( "No algorithm name specified"); @@ -305,24 +405,11 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No cryptographic primitive specified"); } - if (supportedAlgorithms == null || - supportedAlgorithms.length == 0) { + if (supportedAlgorithms == null || supportedAlgorithms.isEmpty()) { return false; } - // trim the MGF part: withand - int position = algorithm.indexOf("and"); - if (position > 0) { - algorithm = algorithm.substring(0, position); - } - - for (String supportedAlgorithm : supportedAlgorithms) { - if (algorithm.equalsIgnoreCase(supportedAlgorithm)) { - return true; - } - } - - return false; + return supportedAlgorithms.contains(algorithm); } @Override @@ -339,7 +426,41 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No algorithm name specified"); } - return permits(primitives, algorithm, parameters); + return permits(primitives, algorithm, parameters) + && checkRsaSsaPssParams(algorithm, key, parameters); + } + + // Additional check for RSASSA-PSS signature algorithm parameters. + private boolean checkRsaSsaPssParams( + String algorithm, Key key, AlgorithmParameters parameters) { + + if (supportedSignatureSchemes == null + || key == null + || parameters == null + || !"RSASSA-PSS".equalsIgnoreCase(algorithm)) { + return true; + } + + try { + String keyAlg = key.getAlgorithm(); + String paramDigestAlg = parameters.getParameterSpec( + PSSParameterSpec.class).getDigestAlgorithm(); + + return supportedSignatureSchemes.stream().anyMatch(ss -> + ss.algorithm.equalsIgnoreCase(algorithm) + && ss.keyAlgorithm.equalsIgnoreCase(keyAlg) + && ((PSSParameterSpec) ss.signAlgParams.parameterSpec) + .getDigestAlgorithm() + .equalsIgnoreCase(paramDigestAlg)); + + } catch (InvalidParameterSpecException e) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Invalid AlgorithmParameters: " + + parameters + "; Error: " + e.getMessage()); + } + + return true; + } } } } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java index d11ffc96b47..4a52a2ea583 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -1923,9 +1923,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--); + for (; i >= pos && pt.get(i) == 0; i--); - if (i < (pos + 1)) { + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } @@ -2441,10 +2441,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--) { - // blank - } - if (i < (pos + 1)) { + for (; i >= pos && pt.get(i) == 0; i--); + + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java index bb032e019d3..aacac465027 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -78,6 +78,7 @@ final class SSLConfiguration implements Cloneable { boolean noSniExtension; boolean noSniMatcher; + boolean isQuic; // To switch off the extended_master_secret extension. static final boolean useExtendedMasterSecret; @@ -91,7 +92,7 @@ final class SSLConfiguration implements Cloneable { Utilities.getBooleanProperty("jdk.tls.allowLegacyMasterSecret", true); // Use TLS1.3 middlebox compatibility mode. - static final boolean useCompatibilityMode = Utilities.getBooleanProperty( + private static final boolean useCompatibilityMode = Utilities.getBooleanProperty( "jdk.tls.client.useCompatibilityMode", true); // Respond a close_notify alert if receiving close_notify alert. @@ -524,6 +525,14 @@ final class SSLConfiguration implements Cloneable { } } + public boolean isUseCompatibilityMode() { + return useCompatibilityMode && !isQuic; + } + + public void setQuic(boolean quic) { + isQuic = quic; + } + @Override @SuppressWarnings({"unchecked", "CloneDeclaresCloneNotSupported"}) public Object clone() { @@ -567,7 +576,10 @@ final class SSLConfiguration implements Cloneable { */ private static String[] getCustomizedSignatureScheme(String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); @@ -595,7 +607,8 @@ final class SSLConfiguration implements Cloneable { if (scheme != null && scheme.isAvailable) { signatureSchemes.add(schemeName); } else { - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "The current installed providers do not " + "support signature scheme: " + schemeName); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java index f09270aa9e6..85dde5b0dbb 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, 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 @@ -33,6 +33,7 @@ import java.util.*; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.*; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.validator.Validator; /** @@ -480,6 +481,10 @@ public abstract class SSLContextImpl extends SSLContextSpi { return availableProtocols; } + public boolean isUsableWithQuic() { + return trustManager instanceof X509TrustManagerImpl; + } + /* * The SSLContext implementation for SSL/(D)TLS algorithm * @@ -1449,22 +1454,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forSocket(sslSocket, true); - } - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } @@ -1474,6 +1465,7 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); @@ -1487,22 +1479,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forEngine(engine, true); - } - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java index c7175ea7fdc..fb0490d70f1 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java @@ -286,7 +286,7 @@ enum SSLExtension implements SSLStringizer { ProtocolVersion.PROTOCOLS_10_12, SessionTicketExtension.shNetworkProducer, SessionTicketExtension.shOnLoadConsumer, - null, + SessionTicketExtension.shOnLoadAbsence, null, null, SessionTicketExtension.steStringizer), @@ -458,6 +458,28 @@ enum SSLExtension implements SSLStringizer { null, null, null, null, KeyShareExtension.hrrStringizer), + // Extension defined in RFC 9001 + CH_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.CLIENT_HELLO, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.chNetworkProducer, + QuicTransportParametersExtension.chOnLoadConsumer, + QuicTransportParametersExtension.chOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output. + null), + EE_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.ENCRYPTED_EXTENSIONS, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.eeNetworkProducer, + QuicTransportParametersExtension.eeOnLoadConsumer, + QuicTransportParametersExtension.eeOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output + null), + // Extensions defined in RFC 5746 (TLS Renegotiation Indication Extension) CH_RENEGOTIATION_INFO (0xff01, "renegotiation_info", SSLHandshake.CLIENT_HELLO, @@ -820,7 +842,10 @@ enum SSLExtension implements SSLStringizer { private static Collection getDisabledExtensions( String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java index 47afee8ddf2..f55ab27d297 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java @@ -46,6 +46,7 @@ import sun.security.util.Debug; import sun.security.x509.*; import static java.nio.charset.StandardCharsets.UTF_8; +import static sun.security.ssl.Utilities.LINE_SEP; /** * Implementation of SSL logger. @@ -62,6 +63,7 @@ public final class SSLLogger { private static final String property; public static final boolean isOn; + static { String p = System.getProperty("javax.net.debug"); if (p != null) { @@ -190,7 +192,12 @@ public final class SSLLogger { try { String formatted = SSLSimpleFormatter.formatParameters(params); - logger.log(level, msg, formatted); + // use the customized log method for SSLConsoleLogger + if (logger instanceof SSLConsoleLogger) { + logger.log(level, msg, formatted); + } else { + logger.log(level, msg + ":" + LINE_SEP + formatted); + } } catch (Exception exp) { // ignore it, just for debugging. } @@ -282,7 +289,7 @@ public final class SSLLogger { """, Locale.ENGLISH); - private static final MessageFormat extendedCertFormart = + private static final MessageFormat extendedCertFormat = new MessageFormat( """ "version" : "v{0}", @@ -299,15 +306,6 @@ public final class SSLLogger { """, Locale.ENGLISH); - // - // private static MessageFormat certExtFormat = new MessageFormat( - // "{0} [{1}] '{'\n" + - // " critical: {2}\n" + - // " value: {3}\n" + - // "'}'", - // Locale.ENGLISH); - // - private static final MessageFormat messageFormatNoParas = new MessageFormat( """ @@ -325,7 +323,7 @@ public final class SSLLogger { private static final MessageFormat messageCompactFormatNoParas = new MessageFormat( - "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n", + "{0}|{1}|{2}|{3}|{4}|{5}|{6}" + LINE_SEP, Locale.ENGLISH); private static final MessageFormat messageFormatWithParas = @@ -423,7 +421,7 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - builder.append(",\n"); + builder.append("," + LINE_SEP); } if (parameter instanceof Throwable) { @@ -504,10 +502,10 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - extBuilder.append(",\n"); + extBuilder.append("," + LINE_SEP); } - extBuilder.append("{\n" + - Utilities.indent(certExt.toString()) + "\n}"); + extBuilder.append("{" + LINE_SEP + + Utilities.indent(certExt.toString()) + LINE_SEP +"}"); } Object[] certFields = { x509.getVersion(), @@ -521,7 +519,7 @@ public final class SSLLogger { Utilities.indent(extBuilder.toString()) }; builder.append(Utilities.indent( - extendedCertFormart.format(certFields))); + extendedCertFormat.format(certFields))); } } catch (Exception ce) { // ignore the exception @@ -578,7 +576,7 @@ public final class SSLLogger { // "string c" // ] StringBuilder builder = new StringBuilder(512); - builder.append("\"" + key + "\": [\n"); + builder.append("\"" + key + "\": [" + LINE_SEP); int len = strings.length; for (int i = 0; i < len; i++) { String string = strings[i]; @@ -586,7 +584,7 @@ public final class SSLLogger { if (i != len - 1) { builder.append(","); } - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(" ]"); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java index 5eb9f72af46..1bf561c47e6 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java @@ -1388,10 +1388,10 @@ final class SSLSessionImpl extends ExtendedSSLSession { } /** - * Gets an array of supported signature schemes that the local side is + * Gets a collection of supported signature schemes that the local side is * willing to verify. */ - public Collection getLocalSupportedSignatureSchemes() { + Collection getLocalSupportedSignatureSchemes() { return localSupportedSignAlgs; } @@ -1404,6 +1404,15 @@ final class SSLSessionImpl extends ExtendedSSLSession { return SignatureScheme.getAlgorithmNames(peerSupportedSignAlgs); } + /** + * Gets a collection of supported signature schemes that the peer is + * willing to verify. Those are sent with the "signature_algorithms_cert" + * TLS extension. + */ + Collection getPeerSupportedSignatureSchemes() { + return peerSupportedSignAlgs; + } + /** * Obtains a List containing all {@link SNIServerName}s * of the requested Server Name Indication (SNI) extension. diff --git a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java index 1db07c77160..5cb78ed44f7 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java @@ -29,13 +29,11 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.ProviderException; -import java.security.spec.AlgorithmParameterSpec; import javax.crypto.KDF; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.SSLHandshakeException; import sun.security.internal.spec.TlsKeyMaterialParameterSpec; import sun.security.internal.spec.TlsKeyMaterialSpec; @@ -191,26 +189,26 @@ enum SSLTrafficKeyDerivation implements SSLKeyDerivationGenerator { private enum KeySchedule { // Note that we use enum name as the key name. - TlsKey ("key", false), - TlsIv ("iv", true), - TlsUpdateNplus1 ("traffic upd", false); + TlsKey ("key"), + TlsIv ("iv"), + TlsUpdateNplus1 ("traffic upd"); private final byte[] label; - private final boolean isIv; - KeySchedule(String label, boolean isIv) { + KeySchedule(String label) { this.label = ("tls13 " + label).getBytes(); - this.isIv = isIv; } int getKeyLength(CipherSuite cs) { - if (this == KeySchedule.TlsUpdateNplus1) - return cs.hashAlg.hashLength; - return isIv ? cs.bulkCipher.ivSize : cs.bulkCipher.keySize; + return switch (this) { + case TlsUpdateNplus1 -> cs.hashAlg.hashLength; + case TlsIv -> cs.bulkCipher.ivSize; + case TlsKey -> cs.bulkCipher.keySize; + }; } String getAlgorithm(CipherSuite cs, String algorithm) { - return isIv ? algorithm : cs.bulkCipher.algorithm; + return this == TlsKey ? cs.bulkCipher.algorithm : algorithm; } } diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java index d092d6c07de..1d2faa5351f 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -235,7 +235,8 @@ final class ServerHello { serverVersion.name, Utilities.toHexString(serverRandom.randomBytes), sessionId.toString(), - cipherSuite.name + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", + cipherSuite.name + + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", HexFormat.of().toHexDigits(compressionMethod), Utilities.indent(extensions.toString(), " ") }; @@ -534,8 +535,9 @@ final class ServerHello { // consider the handshake extension impact SSLExtension[] enabledExtensions = - shc.sslConfig.getEnabledExtensions( - SSLHandshake.CLIENT_HELLO, shc.negotiatedProtocol); + shc.sslConfig.getEnabledExtensions( + SSLHandshake.CLIENT_HELLO, + shc.negotiatedProtocol); clientHello.extensions.consumeOnTrade(shc, enabledExtensions); shc.negotiatedProtocol = @@ -670,6 +672,17 @@ final class ServerHello { // Update the context for master key derivation. shc.handshakeKeyDerivation = kd; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } // Check if the server supports stateless resumption if (sessionCache.statelessEnabled()) { shc.statelessResumption = true; @@ -784,9 +797,9 @@ final class ServerHello { // first handshake message. This may either be after // a ServerHello or a HelloRetryRequest. // (RFC 8446, Appendix D.4) - shc.conContext.outputRecord.changeWriteCiphers( - SSLWriteCipher.nullTlsWriteCipher(), - (clientHello.sessionId.length() != 0)); + if (clientHello.sessionId.length() != 0) { + shc.conContext.outputRecord.encodeChangeCipherSpec(); + } // Stateless, shall we clean up the handshake context as well? shc.handshakeHash.finish(); // forgot about the handshake hash @@ -1366,10 +1379,21 @@ final class ServerHello { // Should use resumption_master_secret for TLS 1.3. // chc.handshakeSession.setMasterSecret(masterSecret); - // Update the context for master key derivation. chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw chc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } + // update the consumers and producers // // The server sends a dummy change_cipher_spec record immediately diff --git a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java index c0d2bea77ca..9a84bbad8fd 100644 --- a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java @@ -72,6 +72,8 @@ final class SessionTicketExtension { new T12SHSessionTicketProducer(); static final ExtensionConsumer shOnLoadConsumer = new T12SHSessionTicketConsumer(); + static final HandshakeAbsence shOnLoadAbsence = + new T12SHSessionTicketOnLoadAbsence(); static final SSLStringizer steStringizer = new SessionTicketStringizer(); // No need to compress a ticket if it can fit in a single packet. @@ -278,8 +280,10 @@ final class SessionTicketExtension { aad.putInt(keyID).put(compressed); c.updateAAD(aad); + // use getOutputSize to avoid a ShortBufferException + // from providers that require oversized buffers. See JDK-8368514. ByteBuffer out = ByteBuffer.allocate( - data.remaining() - GCM_TAG_LEN / 8); + c.getOutputSize(data.remaining())); c.doFinal(data, out); out.flip(); @@ -291,7 +295,7 @@ final class SessionTicketExtension { return out; } catch (Exception e) { if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { - SSLLogger.fine("Decryption failed." + e.getMessage()); + SSLLogger.fine("Decryption failed." + e); } } @@ -527,4 +531,27 @@ final class SessionTicketExtension { chc.statelessResumption = true; } } + + /** + * The absence processing if a "session_ticket" extension is + * not present in the ServerHello handshake message. + */ + private static final class T12SHSessionTicketOnLoadAbsence + implements HandshakeAbsence { + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + + // Disable stateless resumption if server doesn't send the extension. + if (chc.statelessResumption) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { + SSLLogger.info( + "Server doesn't support stateless resumption"); + } + chc.statelessResumption = false; + } + } + } } diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java index a95b31583bb..b298da05e9a 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java @@ -31,6 +31,7 @@ import static sun.security.ssl.SignatureScheme.HANDSHAKE_SCOPE; import java.io.IOException; import java.nio.ByteBuffer; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -191,21 +192,9 @@ final class SignatureAlgorithmsExtension { // Produce the extension. SignatureScheme.updateHandshakeLocalSupportedAlgs(chc); - int vectorLen = SignatureScheme.sizeInRecord() * - chc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : chc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - chc.handshakeExtensions.put( + return produceNetworkLoad(chc, SSLExtension.CH_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(chc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CH_SIGNATURE_ALGORITHMS_CERT); } } @@ -391,23 +380,11 @@ final class SignatureAlgorithmsExtension { } // Produce the extension. - // localSupportedSignAlgs has been already updated when we - // set the negotiated protocol. - int vectorLen = SignatureScheme.sizeInRecord() - * shc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : shc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - shc.handshakeExtensions.put( + // localSupportedSignAlgs and localSupportedCertSignAlgs have been + // already updated when we set the negotiated protocol. + return produceNetworkLoad(shc, SSLExtension.CR_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(shc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CR_SIGNATURE_ALGORITHMS_CERT); } } @@ -546,4 +523,45 @@ final class SignatureAlgorithmsExtension { hc.handshakeSession.setPeerSupportedSignatureAlgorithms(certSS); } } + + /** + * Produce network load and update context. + * + * @param hc HandshakeContext + * @param signatureAlgorithmsExt "signature_algorithms" extension + * @param signatureAlgorithmsCertExt "signature_algorithms_cert" + * extension + * @return network load as byte array + */ + private static byte[] produceNetworkLoad( + HandshakeContext hc, SSLExtension signatureAlgorithmsExt, + SSLExtension signatureAlgorithmsCertExt) throws IOException { + + List sigAlgs; + + // If we don't produce "signature_algorithms_cert" extension, then + // the "signature_algorithms" extension should contain signatures + // supported for both: handshake signatures and certificate signatures. + if (hc.sslConfig.isAvailable(signatureAlgorithmsCertExt)) { + sigAlgs = hc.localSupportedSignAlgs; + } else { + sigAlgs = new ArrayList<>(hc.localSupportedSignAlgs); + sigAlgs.retainAll(hc.localSupportedCertSignAlgs); + } + + int vectorLen = SignatureScheme.sizeInRecord() * sigAlgs.size(); + byte[] extData = new byte[vectorLen + 2]; + ByteBuffer m = ByteBuffer.wrap(extData); + Record.putInt16(m, vectorLen); + + for (SignatureScheme ss : sigAlgs) { + Record.putInt16(m, ss.id); + } + + // Update the context. + hc.handshakeExtensions.put( + signatureAlgorithmsExt, new SignatureSchemesSpec(sigAlgs)); + + return extData; + } } diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java index b3ed5810c56..043a0d84c61 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java @@ -146,12 +146,12 @@ enum SignatureScheme { "RSA", 511, ProtocolVersion.PROTOCOLS_TO_12); - final int id; // hash + signature - final String name; // literal name - private final String algorithm; // signature algorithm - final String keyAlgorithm; // signature key algorithm - private final SigAlgParamSpec signAlgParams; // signature parameters - private final NamedGroup namedGroup; // associated named group + final int id; // hash + signature + final String name; // literal name + final String algorithm; // signature algorithm + final String keyAlgorithm; // signature key algorithm + final SigAlgParamSpec signAlgParams; // signature parameters + private final NamedGroup namedGroup; // associated named group // The minimal required key size in bits. // @@ -185,7 +185,7 @@ enum SignatureScheme { RSA_PSS_SHA384 ("SHA-384", 48), RSA_PSS_SHA512 ("SHA-512", 64); - private final AlgorithmParameterSpec parameterSpec; + final AlgorithmParameterSpec parameterSpec; private final AlgorithmParameters parameters; private final boolean isAvailable; diff --git a/src/java.base/share/classes/sun/security/ssl/StatusResponseManager.java b/src/java.base/share/classes/sun/security/ssl/StatusResponseManager.java index 1383db1ce82..ec200c6e495 100644 --- a/src/java.base/share/classes/sun/security/ssl/StatusResponseManager.java +++ b/src/java.base/share/classes/sun/security/ssl/StatusResponseManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -278,7 +278,7 @@ final class StatusResponseManager { } } } catch (InterruptedException intex) { - // Log and reset the interrupt state + // Log and reset the interrupted state Thread.currentThread().interrupt(); if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { SSLLogger.fine("Interrupt occurred while fetching: " + diff --git a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java index 2441ad91fde..6bf138f4e45 100644 --- a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java @@ -195,6 +195,13 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + /* * Choose an alias to authenticate the server side of a secure * socket given the public key type and the list of @@ -222,6 +229,16 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { X509TrustManagerImpl.getRequestedServerNames(engine), "HTTPS"); } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + /* * Get the matching aliases for authenticating the client side of a secure * socket given the public key type and the list of diff --git a/src/java.base/share/classes/sun/security/ssl/TransportContext.java b/src/java.base/share/classes/sun/security/ssl/TransportContext.java index 717c81723ff..49fd664e9ed 100644 --- a/src/java.base/share/classes/sun/security/ssl/TransportContext.java +++ b/src/java.base/share/classes/sun/security/ssl/TransportContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -489,6 +489,10 @@ final class TransportContext implements ConnectionContext { isUnsureMode = false; } + public void setQuic(boolean quic) { + sslConfig.setQuic(quic); + } + // The OutputRecord is closed and not buffered output record. boolean isOutboundDone() { return outputRecord.isClosed() && outputRecord.isEmpty(); diff --git a/src/java.base/share/classes/sun/security/ssl/Utilities.java b/src/java.base/share/classes/sun/security/ssl/Utilities.java index 3ed022db382..50cd3bee751 100644 --- a/src/java.base/share/classes/sun/security/ssl/Utilities.java +++ b/src/java.base/share/classes/sun/security/ssl/Utilities.java @@ -40,6 +40,7 @@ final class Utilities { Pattern.compile("\\r\\n|\\n|\\r"); private static final HexFormat HEX_FORMATTER = HexFormat.of().withUpperCase(); + static final String LINE_SEP = System.lineSeparator(); /** * Puts {@code hostname} into the {@code serverNames} list. @@ -150,7 +151,7 @@ final class Utilities { static String indent(String source, String prefix) { StringBuilder builder = new StringBuilder(); if (source == null) { - builder.append("\n").append(prefix).append(""); + builder.append(LINE_SEP).append(prefix).append(""); } else { String[] lines = lineBreakPatern.split(source); boolean isFirst = true; @@ -158,7 +159,7 @@ final class Utilities { if (isFirst) { isFirst = false; } else { - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(prefix).append(line); } diff --git a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java index 4e91df2806e..5abc2cb1bf4 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java +++ b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -218,6 +218,28 @@ enum X509Authentication implements SSLAuthentication { chc.peerSupportedAuthorities == null ? null : chc.peerSupportedAuthorities.clone(), engine); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the client alias and + // just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseClientAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + clientAlias = xkm.chooseQuicClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + clientAlias = km.chooseClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + null); + } } if (clientAlias == null) { @@ -290,6 +312,28 @@ enum X509Authentication implements SSLAuthentication { shc.peerSupportedAuthorities == null ? null : shc.peerSupportedAuthorities.clone(), engine); + } else if (shc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the server alias + // and just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseServerAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + serverAlias = xkm.chooseQuicServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + serverAlias = km.chooseServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + null); + } } if (serverAlias == null) { diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java index 6f18b80395a..9484ab4f830 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java @@ -39,16 +39,15 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.StandardConstants; import javax.net.ssl.X509ExtendedKeyManager; import javax.security.auth.x500.X500Principal; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.KnownOIDs; import sun.security.validator.Validator; @@ -75,6 +74,15 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { abstract boolean isCheckingDisabled(); + // TODO move this method to a public interface / class + abstract String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + + // TODO move this method to a public interface / class + abstract String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + // Entry point to do all certificate checks. protected EntryStatus checkAlias(int keyStoreIndex, String alias, Certificate[] chain, Date verificationDate, List keyTypes, @@ -168,19 +176,8 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { } if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { - SSLSession session = sslSocket.getHandshakeSession(); - - if (session instanceof ExtendedSSLSession extSession - && ProtocolVersion.useTLS12PlusSpec( - extSession.getProtocol())) { - // Use peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - return SSLAlgorithmConstraints.forSocket(sslSocket, - extSession.getPeerSupportedSignatureAlgorithms(), - true); - } - - return SSLAlgorithmConstraints.forSocket(sslSocket, true); + return SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } return SSLAlgorithmConstraints.DEFAULT; @@ -193,21 +190,19 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { return null; } - if (engine != null) { - SSLSession session = engine.getHandshakeSession(); + return SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); + } - if (session instanceof ExtendedSSLSession extSession - && ProtocolVersion.useTLS12PlusSpec( - extSession.getProtocol())) { - // Use peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - return SSLAlgorithmConstraints.forEngine(engine, - extSession.getPeerSupportedSignatureAlgorithms(), - true); - } + // Gets algorithm constraints of QUIC TLS engine. + protected AlgorithmConstraints getAlgorithmConstraints(QuicTLSEngineImpl engine) { + + if (checksDisabled) { + return null; } - return SSLAlgorithmConstraints.forEngine(engine, true); + return SSLAlgorithmConstraints.forQUIC( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } // Algorithm constraints check. diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java index df6ecaf7a42..c607fe0f25d 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java @@ -33,8 +33,10 @@ import java.security.KeyStore.Builder; import java.security.KeyStore.Entry; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.atomic.AtomicLong; @@ -129,6 +131,13 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { @@ -165,6 +174,16 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { // It is not a really HTTPS endpoint identification. } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + @Override public String[] getClientAliases(String keyType, Principal[] issuers) { return getAliases(keyType, issuers, CheckType.CLIENT); @@ -204,10 +223,17 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { // parse the alias int firstDot = alias.indexOf('.'); int secondDot = alias.indexOf('.', firstDot + 1); - if ((firstDot == -1) || (secondDot == firstDot)) { - // invalid alias + + if ((firstDot < 1) + || (secondDot - firstDot < 2) + || (alias.length() - secondDot < 2)) { + + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.warning("Invalid alias format: " + alias); + } return null; } + try { int builderIndex = Integer.parseInt (alias.substring(firstDot + 1, secondDot)); @@ -223,8 +249,16 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { entry = (PrivateKeyEntry)newEntry; entryCacheMap.put(alias, new SoftReference<>(entry)); return entry; - } catch (Exception e) { - // ignore + } catch (UnrecoverableEntryException | + KeyStoreException | + NumberFormatException | + NoSuchAlgorithmException | + IndexOutOfBoundsException e) { + // ignore and only log exception + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.warning("Exception thrown while getting an alias " + + alias + ": " + e); + } return null; } } @@ -261,8 +295,9 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { if (results != null) { for (EntryStatus status : results) { if (status.checkResult == CheckResult.OK) { - if (SSLLogger.isOn && SSLLogger.isOn("keymanager")) { - SSLLogger.fine("KeyMgr: choosing key: " + status); + if (SSLLogger.isOn + && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.fine("Choosing key: " + status); } return makeAlias(status); } @@ -277,15 +312,15 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { } } if (allResults == null) { - if (SSLLogger.isOn && SSLLogger.isOn("keymanager")) { - SSLLogger.fine("KeyMgr: no matching key found"); + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.fine("No matching key found"); } return null; } Collections.sort(allResults); - if (SSLLogger.isOn && SSLLogger.isOn("keymanager")) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { SSLLogger.fine( - "KeyMgr: no good matching key found, " + "No good matching key found, " + "returning best match out of", allResults); } return makeAlias(allResults.get(0)); @@ -323,14 +358,14 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { } } if (allResults == null || allResults.isEmpty()) { - if (SSLLogger.isOn && SSLLogger.isOn("keymanager")) { - SSLLogger.fine("KeyMgr: no matching alias found"); + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.fine("No matching alias found"); } return null; } Collections.sort(allResults); - if (SSLLogger.isOn && SSLLogger.isOn("keymanager")) { - SSLLogger.fine("KeyMgr: getting aliases", allResults); + if (SSLLogger.isOn && SSLLogger.isOn("ssl,keymanager")) { + SSLLogger.fine("Getting aliases", allResults); } return toAliases(allResults); } diff --git a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java index 58794e5dce8..d82b94a1d7d 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -30,7 +30,10 @@ import java.security.*; import java.security.cert.*; import java.util.*; import java.util.concurrent.locks.ReentrantLock; + import javax.net.ssl.*; + +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.AnchorCertificates; import sun.security.util.HostnameChecker; import sun.security.validator.*; @@ -144,6 +147,16 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager checkTrusted(chain, authType, engine, false); } + public void checkClientTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, true); + } + + void checkServerTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, false); + } + private Validator checkTrustedInit(X509Certificate[] chain, String authType, boolean checkClientTrusted) { if (chain == null || chain.length == 0) { @@ -198,32 +211,19 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager Validator v = checkTrustedInit(chain, authType, checkClientTrusted); X509Certificate[] trustedChain; - if ((socket != null) && socket.isConnected() && - (socket instanceof SSLSocket sslSocket)) { + if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { SSLSession session = sslSocket.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } @@ -248,6 +248,52 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager } } + private void checkTrusted(X509Certificate[] chain, + String authType, QuicTLSEngineImpl quicTLSEngine, + boolean checkClientTrusted) throws CertificateException { + Validator v = checkTrustedInit(chain, authType, checkClientTrusted); + + final X509Certificate[] trustedChain; + if (quicTLSEngine != null) { + + final SSLSession session = quicTLSEngine.getHandshakeSession(); + if (session == null) { + throw new CertificateException("No handshake session"); + } + + // create the algorithm constraints + final AlgorithmConstraints constraints = SSLAlgorithmConstraints.forQUIC( + quicTLSEngine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); + final List responseList; + // grab any stapled OCSP responses for use in validation + if (!checkClientTrusted && + session instanceof ExtendedSSLSession extSession) { + responseList = extSession.getStatusResponses(); + } else { + responseList = Collections.emptyList(); + } + // do the certificate chain validation + trustedChain = v.validate(chain, null, responseList, + constraints, checkClientTrusted ? null : authType); + + // check endpoint identity + String identityAlg = quicTLSEngine.getSSLParameters(). + getEndpointIdentificationAlgorithm(); + if (identityAlg != null && !identityAlg.isEmpty()) { + checkIdentity(session, trustedChain, + identityAlg, checkClientTrusted); + } + } else { + trustedChain = v.validate(chain, null, Collections.emptyList(), + null, checkClientTrusted ? null : authType); + } + + if (SSLLogger.isOn && SSLLogger.isOn("ssl,trustmanager")) { + SSLLogger.fine("Found trusted certificate", + trustedChain[trustedChain.length - 1]); + } + } + private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { @@ -255,29 +301,18 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager X509Certificate[] trustedChain; if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } @@ -367,6 +402,13 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager return Collections.emptyList(); } + static List getRequestedServerNames(QuicTLSEngineImpl engine) { + if (engine != null) { + return getRequestedServerNames(engine.getHandshakeSession()); + } + return Collections.emptyList(); + } + private static List getRequestedServerNames( SSLSession session) { if (session instanceof ExtendedSSLSession) { diff --git a/src/java.base/share/classes/sun/security/util/DerValue.java b/src/java.base/share/classes/sun/security/util/DerValue.java index 19e7083180b..ec8b482b07d 100644 --- a/src/java.base/share/classes/sun/security/util/DerValue.java +++ b/src/java.base/share/classes/sun/security/util/DerValue.java @@ -859,6 +859,22 @@ public class DerValue { return readStringInternal(tag_UniversalString, new UTF_32BE()); } + /** + * Checks that the BMPString does not contain any surrogate characters, + * which are outside the Basic Multilingual Plane. + * + * @throws IOException if illegal characters are detected + */ + public void validateBMPString() throws IOException { + String bmpString = getBMPString(); + for (int i = 0; i < bmpString.length(); i++) { + if (Character.isSurrogate(bmpString.charAt(i))) { + throw new IOException( + "Illegal character in BMPString, index: " + i); + } + } + } + /** * Reads the ASN.1 NULL value */ diff --git a/src/java.base/share/classes/sun/security/util/Password.java b/src/java.base/share/classes/sun/security/util/Password.java index 7acece65a57..e358bcd95de 100644 --- a/src/java.base/share/classes/sun/security/util/Password.java +++ b/src/java.base/share/classes/sun/security/util/Password.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -29,11 +29,12 @@ import java.io.*; import java.nio.*; import java.nio.charset.*; import java.util.Arrays; + import jdk.internal.access.SharedSecrets; +import jdk.internal.io.JdkConsoleImpl; /** * A utility class for reading passwords - * */ public class Password { /** Reads user password from given input stream. */ @@ -50,30 +51,36 @@ public class Password { char[] consoleEntered = null; byte[] consoleBytes = null; + char[] buf = null; try { // Only use Console if `in` is the initial System.in - Console con; - if (!isEchoOn && - in == SharedSecrets.getJavaLangAccess().initialSystemIn() && - ((con = System.console()) != null)) { - consoleEntered = con.readPassword(); - // readPassword returns "" if you just press ENTER with the built-in Console, - // to be compatible with old Password class, change to null - if (consoleEntered == null || consoleEntered.length == 0) { - return null; + if (!isEchoOn) { + if (in == SharedSecrets.getJavaLangAccess().initialSystemIn() + && ConsoleHolder.consoleIsAvailable()) { + consoleEntered = ConsoleHolder.readPassword(); + // readPassword might return null. Stop now. + if (consoleEntered == null) { + return null; + } + consoleBytes = ConsoleHolder.convertToBytes(consoleEntered); + in = new ByteArrayInputStream(consoleBytes); + } else if (System.in.available() == 0) { + // This may be running in an IDE Run Window or in JShell, + // which acts like an interactive console and echoes the + // entered password. In this case, print a warning that + // the password might be echoed. If available() is not zero, + // it's more likely the input comes from a pipe, such as + // "echo password |" or "cat password_file |" where input + // will be silently consumed without echoing to the screen. + System.err.print(ResourcesMgr.getString + ("warning.input.may.be.visible.on.screen")); } - consoleBytes = convertToBytes(consoleEntered); - in = new ByteArrayInputStream(consoleBytes); } // Rest of the lines still necessary for KeyStoreLoginModule // and when there is no console. - - char[] lineBuffer; - char[] buf; - - buf = lineBuffer = new char[128]; + buf = new char[128]; int room = buf.length; int offset = 0; @@ -101,11 +108,11 @@ public class Password { /* fall through */ default: if (--room < 0) { + char[] oldBuf = buf; buf = new char[offset + 128]; room = buf.length - offset - 1; - System.arraycopy(lineBuffer, 0, buf, 0, offset); - Arrays.fill(lineBuffer, ' '); - lineBuffer = buf; + System.arraycopy(oldBuf, 0, buf, 0, offset); + Arrays.fill(oldBuf, ' '); } buf[offset++] = (char) c; break; @@ -118,8 +125,6 @@ public class Password { char[] ret = new char[offset]; System.arraycopy(buf, 0, ret, 0, offset); - Arrays.fill(buf, ' '); - return ret; } finally { if (consoleEntered != null) { @@ -128,35 +133,72 @@ public class Password { if (consoleBytes != null) { Arrays.fill(consoleBytes, (byte)0); } + if (buf != null) { + Arrays.fill(buf, ' '); + } } } - /** - * Change a password read from Console.readPassword() into - * its original bytes. - * - * @param pass a char[] - * @return its byte[] format, similar to new String(pass).getBytes() - */ - private static byte[] convertToBytes(char[] pass) { - if (enc == null) { - synchronized (Password.class) { - enc = System.console() - .charset() - .newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); + // Everything on Console or JdkConsoleImpl is inside this class. + private static class ConsoleHolder { + + // primary console; may be null + private static final Console c1; + // secondary console (when stdout is redirected); may be null + private static final JdkConsoleImpl c2; + // encoder for c1 or c2 + private static final CharsetEncoder enc; + + static { + c1 = System.console(); + Charset charset; + if (c1 != null) { + c2 = null; + charset = c1.charset(); + } else { + c2 = JdkConsoleImpl.passwordConsole().orElse(null); + charset = (c2 != null) ? c2.charset() : null; + } + enc = charset == null ? null : charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + public static boolean consoleIsAvailable() { + return c1 != null || c2 != null; + } + + public static char[] readPassword() { + assert consoleIsAvailable(); + if (c1 != null) { + return c1.readPassword(); + } else { + try { + return c2.readPasswordNoNewLine(); + } finally { + System.err.println(); + } } } - byte[] ba = new byte[(int)(enc.maxBytesPerChar() * pass.length)]; - ByteBuffer bb = ByteBuffer.wrap(ba); - synchronized (enc) { - enc.reset().encode(CharBuffer.wrap(pass), bb, true); + + /** + * Convert a password read from console into its original bytes. + * + * @param pass a char[] + * @return its byte[] format, equivalent to new String(pass).getBytes() + * but String is immutable and cannot be cleaned up. + */ + public static byte[] convertToBytes(char[] pass) { + assert consoleIsAvailable(); + byte[] ba = new byte[(int) (enc.maxBytesPerChar() * pass.length)]; + ByteBuffer bb = ByteBuffer.wrap(ba); + synchronized (enc) { + enc.reset().encode(CharBuffer.wrap(pass), bb, true); + } + if (bb.remaining() > 0) { + bb.put((byte)'\n'); // will be recognized as a stop sign + } + return ba; } - if (bb.position() < ba.length) { - ba[bb.position()] = '\n'; - } - return ba; } - private static volatile CharsetEncoder enc; } diff --git a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java index 1910746fe44..987b6967b50 100644 --- a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java +++ b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java @@ -32,6 +32,7 @@ import sun.security.util.math.IntegerFieldModuloP; import java.math.BigInteger; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; // Reference: // - [1] Shay Gueron and Vlad Krasnov "Fast Prime Field Elliptic Curve @@ -63,7 +64,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial private static final long[] zero = new long[] { 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L }; - private static final long[] modulus = new long[] { + @Stable private static final long[] modulus = new long[] { 0x000fffffffffffffL, 0x00000fffffffffffL, 0x0000000000000000L, 0x0000001000000000L, 0x0000ffffffff0000L }; @@ -207,9 +208,8 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; nn1 = Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); n1 &= LIMB_MASK; - n2 = n * modulus[2]; - nn2 = Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - n2 &= LIMB_MASK; + n2 = 0; + nn2 = 0; n3 = n * modulus[3]; nn3 = Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); n3 &= LIMB_MASK; @@ -221,8 +221,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial d0 += n0; dd1 += nn1; d1 += n1; - dd2 += nn2; - d2 += n2; dd3 += nn3; d3 += n3; dd4 += nn4; @@ -259,9 +257,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -300,9 +295,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -341,9 +333,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -382,9 +371,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -411,7 +397,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial c0 = c5 - modulus[0]; c1 = c6 - modulus[1] + (c0 >> BITS_PER_LIMB); c0 &= LIMB_MASK; - c2 = c7 - modulus[2] + (c1 >> BITS_PER_LIMB); + c2 = c7 + (c1 >> BITS_PER_LIMB); c1 &= LIMB_MASK; c3 = c8 - modulus[3] + (c2 >> BITS_PER_LIMB); c2 &= LIMB_MASK; diff --git a/src/java.base/share/classes/sun/security/util/resources/security.properties b/src/java.base/share/classes/sun/security/util/resources/security.properties index 9533b4b8eee..d8a0cbfac29 100644 --- a/src/java.base/share/classes/sun/security/util/resources/security.properties +++ b/src/java.base/share/classes/sun/security/util/resources/security.properties @@ -74,3 +74,6 @@ line.number.expected.expect.found.actual.=line {0}: expected [{1}], found [{2}] # sun.security.pkcs11.SunPKCS11 PKCS11.Token.providerName.Password.=PKCS11 Token [{0}] Password:\u0020 + +# sun.security.util.Password +warning.input.may.be.visible.on.screen=[WARNING: Input may be visible on screen]\u0020 diff --git a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java index 7cbca031cdb..be9d041a4bc 100644 --- a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java +++ b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, 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 @@ -261,7 +261,6 @@ public final class PKIXValidator extends Validator { // apparently issued by trust anchor? X509Certificate last = chain[chain.length - 1]; X500Principal issuer = last.getIssuerX500Principal(); - X500Principal subject = last.getSubjectX500Principal(); if (trustedSubjects.containsKey(issuer)) { return doValidate(chain, pkixParameters); } diff --git a/src/java.base/share/classes/sun/security/x509/AVA.java b/src/java.base/share/classes/sun/security/x509/AVA.java index 915421c76f2..214ae718288 100644 --- a/src/java.base/share/classes/sun/security/x509/AVA.java +++ b/src/java.base/share/classes/sun/security/x509/AVA.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -28,10 +28,13 @@ package sun.security.x509; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Reader; +import java.nio.charset.Charset; import java.text.Normalizer; import java.util.*; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.charset.StandardCharsets.UTF_16BE; import sun.security.util.*; import sun.security.pkcs.PKCS9Attribute; @@ -589,6 +592,10 @@ public class AVA implements DerEncoder { throw new IOException("AVA, extra bytes = " + derval.data.available()); } + + if (value.tag == DerValue.tag_BMPString) { + value.validateBMPString(); + } } AVA(DerInputStream in) throws IOException { @@ -713,7 +720,8 @@ public class AVA implements DerEncoder { * NOTE: this implementation only emits DirectoryStrings of the * types returned by isDerString(). */ - String valStr = new String(value.getDataBytes(), UTF_8); + String valStr = + new String(value.getDataBytes(), getCharset(value, false)); /* * 2.4 (cont): If the UTF-8 string does not have any of the @@ -832,7 +840,8 @@ public class AVA implements DerEncoder { * NOTE: this implementation only emits DirectoryStrings of the * types returned by isDerString(). */ - String valStr = new String(value.getDataBytes(), UTF_8); + String valStr = + new String(value.getDataBytes(), getCharset(value, true)); /* * 2.4 (cont): If the UTF-8 string does not have any of the @@ -927,6 +936,39 @@ public class AVA implements DerEncoder { } } + /* + * Returns the charset that should be used to decode each DN string type. + * + * This method ensures that multi-byte (UTF8String and BMPString) types + * are decoded using the correct charset and the String forms represent + * the correct characters. For 8-bit ASCII-based types (PrintableString + * and IA5String), we return ISO_8859_1 rather than ASCII, so that the + * complete range of characters can be represented, as many certificates + * do not comply with the Internationalized Domain Name ACE format. + * + * NOTE: this method only supports DirectoryStrings of the types returned + * by isDerString(). + */ + private static Charset getCharset(DerValue value, boolean canonical) { + if (canonical) { + return switch (value.tag) { + case DerValue.tag_PrintableString -> ISO_8859_1; + case DerValue.tag_UTF8String -> UTF_8; + default -> throw new Error("unexpected tag: " + value.tag); + }; + } + + return switch (value.tag) { + case DerValue.tag_PrintableString, + DerValue.tag_T61String, + DerValue.tag_IA5String, + DerValue.tag_GeneralString -> ISO_8859_1; + case DerValue.tag_BMPString -> UTF_16BE; + case DerValue.tag_UTF8String -> UTF_8; + default -> throw new Error("unexpected tag: " + value.tag); + }; + } + boolean hasRFC2253Keyword() { return AVAKeyword.hasKeyword(oid, RFC2253); } diff --git a/src/java.base/share/classes/sun/security/x509/AlgorithmId.java b/src/java.base/share/classes/sun/security/x509/AlgorithmId.java index 7d525a9add7..8d2c761a011 100644 --- a/src/java.base/share/classes/sun/security/x509/AlgorithmId.java +++ b/src/java.base/share/classes/sun/security/x509/AlgorithmId.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -127,10 +127,35 @@ public class AlgorithmId implements Serializable, DerEncoder { public AlgorithmId(ObjectIdentifier oid, DerValue params) throws IOException { this.algid = oid; - if (params != null) { - encodedParams = params.toByteArray(); - decodeParams(); + + if (params == null) { + this.encodedParams = null; + this.algParams = null; + return; } + + /* + * If the parameters field explicitly contains an ASN.1 NULL, treat it as + * "no parameters" rather than storing a literal NULL encoding. + * + * This canonicalization ensures consistent encoding/decoding behavior: + * - Algorithms that omit parameters and those that encode explicit NULL + * are treated equivalently (encodedParams == null). + */ + if (params.tag == DerValue.tag_Null) { + if (params.length() != 0) { + throw new IOException("Invalid ASN.1 NULL in AlgorithmId parameters: " + + "non-zero length"); + } + // Canonicalize to "no parameters" representation for consistency + this.encodedParams = null; + this.algParams = null; + return; + } + + // Normal case: non-NULL params -> store and decode + this.encodedParams = params.toByteArray(); + decodeParams(); } protected void decodeParams() throws IOException { @@ -163,38 +188,10 @@ public class AlgorithmId implements Serializable, DerEncoder { bytes.putOID(algid); if (encodedParams == null) { - // MessageDigest algorithms usually have a NULL parameters even - // if most RFCs suggested absent. - // RSA key and signature algorithms requires the NULL parameters - // to be present, see A.1 and A.2.4 of RFC 8017. - if (algid.equals(RSAEncryption_oid) - || algid.equals(MD2_oid) - || algid.equals(MD5_oid) - || algid.equals(SHA_oid) - || algid.equals(SHA224_oid) - || algid.equals(SHA256_oid) - || algid.equals(SHA384_oid) - || algid.equals(SHA512_oid) - || algid.equals(SHA512_224_oid) - || algid.equals(SHA512_256_oid) - || algid.equals(SHA3_224_oid) - || algid.equals(SHA3_256_oid) - || algid.equals(SHA3_384_oid) - || algid.equals(SHA3_512_oid) - || algid.equals(SHA1withRSA_oid) - || algid.equals(SHA224withRSA_oid) - || algid.equals(SHA256withRSA_oid) - || algid.equals(SHA384withRSA_oid) - || algid.equals(SHA512withRSA_oid) - || algid.equals(SHA512$224withRSA_oid) - || algid.equals(SHA512$256withRSA_oid) - || algid.equals(MD2withRSA_oid) - || algid.equals(MD5withRSA_oid) - || algid.equals(SHA3_224withRSA_oid) - || algid.equals(SHA3_256withRSA_oid) - || algid.equals(SHA3_384withRSA_oid) - || algid.equals(SHA3_512withRSA_oid)) { + if (OIDS_REQUIRING_NULL.contains(algid.toString())) { bytes.putNull(); + } else { + // Parameters omitted } } else { bytes.writeBytes(encodedParams); @@ -646,30 +643,54 @@ public class AlgorithmId implements Serializable, DerEncoder { public static final ObjectIdentifier MGF1_oid = ObjectIdentifier.of(KnownOIDs.MGF1); - public static final ObjectIdentifier SHA1withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA1withRSA); - public static final ObjectIdentifier SHA224withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA224withRSA); - public static final ObjectIdentifier SHA256withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA256withRSA); - public static final ObjectIdentifier SHA384withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA384withRSA); - public static final ObjectIdentifier SHA512withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA512withRSA); - public static final ObjectIdentifier SHA512$224withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA512$224withRSA); - public static final ObjectIdentifier SHA512$256withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA512$256withRSA); - public static final ObjectIdentifier MD2withRSA_oid = - ObjectIdentifier.of(KnownOIDs.MD2withRSA); - public static final ObjectIdentifier MD5withRSA_oid = - ObjectIdentifier.of(KnownOIDs.MD5withRSA); - public static final ObjectIdentifier SHA3_224withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA3_224withRSA); - public static final ObjectIdentifier SHA3_256withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA3_256withRSA); - public static final ObjectIdentifier SHA3_384withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA3_384withRSA); - public static final ObjectIdentifier SHA3_512withRSA_oid = - ObjectIdentifier.of(KnownOIDs.SHA3_512withRSA); + /* Set of OIDs that must explicitly encode a NULL parameter in AlgorithmIdentifier. + * References: + - RFC 8017 (PKCS #1) §A.1, §A.2.4: RSA key and signature algorithms + - RFC 9879 (HMAC) §4: HMAC algorithm identifiers + - RFC 9688 (HMAC with SHA-3) §4.3: HMAC-SHA3 algorithms MUST omit parameters + */ + private static final Set OIDS_REQUIRING_NULL = Set.of( + // MessageDigest algorithms usually have a NULL parameters even + // if most RFCs suggested absent. + KnownOIDs.MD2.value(), + KnownOIDs.MD5.value(), + KnownOIDs.SHA_1.value(), + KnownOIDs.SHA_224.value(), + KnownOIDs.SHA_256.value(), + KnownOIDs.SHA_384.value(), + KnownOIDs.SHA_512.value(), + KnownOIDs.SHA_512$224.value(), + KnownOIDs.SHA_512$256.value(), + KnownOIDs.SHA3_224.value(), + KnownOIDs.SHA3_256.value(), + KnownOIDs.SHA3_384.value(), + KnownOIDs.SHA3_512.value(), + + //--- RSA key and signature algorithms (RFC 8017 §A.1, §A.2.4) + KnownOIDs.RSA.value(), + KnownOIDs.SHA1withRSA.value(), + KnownOIDs.SHA224withRSA.value(), + KnownOIDs.SHA256withRSA.value(), + KnownOIDs.SHA384withRSA.value(), + KnownOIDs.SHA512withRSA.value(), + KnownOIDs.SHA512$224withRSA.value(), + KnownOIDs.SHA512$256withRSA.value(), + KnownOIDs.MD2withRSA.value(), + KnownOIDs.MD5withRSA.value(), + KnownOIDs.SHA3_224withRSA.value(), + KnownOIDs.SHA3_256withRSA.value(), + KnownOIDs.SHA3_384withRSA.value(), + KnownOIDs.SHA3_512withRSA.value(), + + // HMACs per RFC 9879 (Section 4): these require explicit NULL parameters + // Note: HMAC-SHA3 algorithms (RFC 9688 §4.3) MUST omit parameters, + // so they are intentionally excluded from this list. + KnownOIDs.HmacSHA1.value(), + KnownOIDs.HmacSHA224.value(), + KnownOIDs.HmacSHA256.value(), + KnownOIDs.HmacSHA384.value(), + KnownOIDs.HmacSHA512.value(), + KnownOIDs.HmacSHA512$224.value(), + KnownOIDs.HmacSHA512$256.value() + ); } diff --git a/src/java.base/share/classes/sun/util/locale/LanguageTag.java b/src/java.base/share/classes/sun/util/locale/LanguageTag.java index 6036c1dd04f..0b2fee7f2cd 100644 --- a/src/java.base/share/classes/sun/util/locale/LanguageTag.java +++ b/src/java.base/share/classes/sun/util/locale/LanguageTag.java @@ -34,17 +34,21 @@ package sun.util.locale; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.IllformedLocaleException; import java.util.List; import java.util.Locale; -import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringJoiner; // List fields are unmodifiable -public record LanguageTag(String language, String script, String region, String privateuse, - List extlangs, List variants, List extensions) { +public record LanguageTag(String language, + String script, + String region, + String privateuse, + List extlangs, + List variants, + List extensions) { public static final String SEP = "-"; public static final String PRIVATEUSE = "x"; @@ -53,78 +57,6 @@ public record LanguageTag(String language, String script, String region, String private static final String EMPTY_SUBTAG = ""; private static final List EMPTY_SUBTAGS = List.of(); - // Map contains legacy language tags and its preferred mappings from - // http://www.ietf.org/rfc/rfc5646.txt - // Keys are lower-case strings. - private static final Map LEGACY; - - static { - // grandfathered = irregular ; non-redundant tags registered - // / regular ; during the RFC 3066 era - // - // irregular = "en-GB-oed" ; irregular tags do not match - // / "i-ami" ; the 'langtag' production and - // / "i-bnn" ; would not otherwise be - // / "i-default" ; considered 'well-formed' - // / "i-enochian" ; These tags are all valid, - // / "i-hak" ; but most are deprecated - // / "i-klingon" ; in favor of more modern - // / "i-lux" ; subtags or subtag - // / "i-mingo" ; combination - // / "i-navajo" - // / "i-pwn" - // / "i-tao" - // / "i-tay" - // / "i-tsu" - // / "sgn-BE-FR" - // / "sgn-BE-NL" - // / "sgn-CH-DE" - // - // regular = "art-lojban" ; these tags match the 'langtag' - // / "cel-gaulish" ; production, but their subtags - // / "no-bok" ; are not extended language - // / "no-nyn" ; or variant subtags: their meaning - // / "zh-guoyu" ; is defined by their registration - // / "zh-hakka" ; and all of these are deprecated - // / "zh-min" ; in favor of a more modern - // / "zh-min-nan" ; subtag or sequence of subtags - // / "zh-xiang" - - final String[][] entries = { - //{"tag", "preferred"}, - {"art-lojban", "jbo"}, - {"cel-gaulish", "xtg-x-cel-gaulish"}, // fallback - {"en-GB-oed", "en-GB-x-oed"}, // fallback - {"i-ami", "ami"}, - {"i-bnn", "bnn"}, - {"i-default", "en-x-i-default"}, // fallback - {"i-enochian", "und-x-i-enochian"}, // fallback - {"i-hak", "hak"}, - {"i-klingon", "tlh"}, - {"i-lux", "lb"}, - {"i-mingo", "see-x-i-mingo"}, // fallback - {"i-navajo", "nv"}, - {"i-pwn", "pwn"}, - {"i-tao", "tao"}, - {"i-tay", "tay"}, - {"i-tsu", "tsu"}, - {"no-bok", "nb"}, - {"no-nyn", "nn"}, - {"sgn-BE-FR", "sfb"}, - {"sgn-BE-NL", "vgt"}, - {"sgn-CH-DE", "sgg"}, - {"zh-guoyu", "cmn"}, - {"zh-hakka", "hak"}, - {"zh-min", "nan-x-zh-min"}, // fallback - {"zh-min-nan", "nan"}, - {"zh-xiang", "hsn"}, - }; - LEGACY = HashMap.newHashMap(entries.length); - for (String[] e : entries) { - LEGACY.put(LocaleUtils.toLowerString(e[0]), e); - } - } - /* * BNF in RFC5646 * @@ -175,14 +107,10 @@ public record LanguageTag(String language, String script, String region, String StringTokenIterator itr; var errorMsg = new StringBuilder(); - // Check if the tag is a legacy language tag - String[] gfmap = LEGACY.get(LocaleUtils.toLowerString(languageTag)); - if (gfmap != null) { - // use preferred mapping - itr = new StringTokenIterator(gfmap[1], SEP); - } else { - itr = new StringTokenIterator(languageTag, SEP); - } + // Check if the tag is a legacy tag + var pref = legacyToPreferred(LocaleUtils.toLowerString(languageTag)); + // If legacy use preferred mapping, otherwise use the tag as is + itr = new StringTokenIterator(Objects.requireNonNullElse(pref, languageTag), SEP); String language = parseLanguage(itr, pp); List extlangs; @@ -400,15 +328,24 @@ public record LanguageTag(String language, String script, String region, String public static String caseFoldTag(String tag) { parse(tag, new ParsePosition(0), false); + StringBuilder bldr = new StringBuilder(tag.length()); + String[] subtags = tag.split(SEP); // Legacy tags - String potentialLegacy = tag.toLowerCase(Locale.ROOT); - if (LEGACY.containsKey(potentialLegacy)) { - return LEGACY.get(potentialLegacy)[0]; + if (legacyToPreferred(tag.toLowerCase(Locale.ROOT)) != null) { + // Fold the legacy tag + for (int i = 0; i < subtags.length ; i++) { + // 2 ALPHA Region subtag(s) are upper, all other subtags are lower + if (i > 0 && subtags[i].length() == 2) { + bldr.append(LocaleUtils.toUpperString(subtags[i])).append(SEP); + } else { + bldr.append(LocaleUtils.toLowerString(subtags[i])).append(SEP); + } + } + bldr.setLength(bldr.length() - 1); // Remove trailing '-' + return bldr.toString(); } // Non-legacy tags - StringBuilder bldr = new StringBuilder(tag.length()); - String[] subtags = tag.split("-"); boolean privateFound = false; boolean singletonFound = false; boolean privUseVarFound = false; @@ -435,7 +372,7 @@ public record LanguageTag(String language, String script, String region, String bldr.append(subtag.toLowerCase(Locale.ROOT)); } if (i != subtags.length-1) { - bldr.append("-"); + bldr.append(SEP); } } return bldr.substring(0); @@ -567,6 +504,47 @@ public record LanguageTag(String language, String script, String region, String return new LanguageTag(language, script, region, privateuse, EMPTY_SUBTAGS, variants, extensions); } + /* + * Converts a legacy tag to its preferred mapping if it exists, otherwise null. + * The keys are mapped and stored as lower case. (Folded on demand). + * See http://www.ietf.org/rfc/rfc5646.txt Section 2.1 and 2.2.8 for the + * full syntax and case accurate legacy tags. + */ + private static String legacyToPreferred(String tag) { + if (tag.length() < 5) { + return null; + } + return switch (tag) { + case "art-lojban" -> "jbo"; + case "cel-gaulish" -> "xtg-x-cel-gaulish"; // fallback + case "en-gb-oed" -> "en-GB-x-oed"; // fallback + case "i-ami" -> "ami"; + case "i-bnn" -> "bnn"; + case "i-default" -> "en-x-i-default"; // fallback + case "i-enochian" -> "und-x-i-enochian"; // fallback + case "i-hak", + "zh-hakka" -> "hak"; + case "i-klingon" -> "tlh"; + case "i-lux" -> "lb"; + case "i-mingo" -> "see-x-i-mingo"; // fallback + case "i-navajo" -> "nv"; + case "i-pwn" -> "pwn"; + case "i-tao" -> "tao"; + case "i-tay" -> "tay"; + case "i-tsu" -> "tsu"; + case "no-bok" -> "nb"; + case "no-nyn" -> "nn"; + case "sgn-be-fr" -> "sfb"; + case "sgn-be-nl" -> "vgt"; + case "sgn-ch-de" -> "sgg"; + case "zh-guoyu" -> "cmn"; + case "zh-min" -> "nan-x-zh-min"; // fallback + case "zh-min-nan" -> "nan"; + case "zh-xiang" -> "hsn"; + default -> null; + }; + } + // // Language subtag syntax checking methods // diff --git a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java index 634932e9e19..7f66febf65f 100644 --- a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java +++ b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index 32d1ddaf0f7..2464361b9ef 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -971,6 +971,33 @@ jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \ ChaCha20-Poly1305 KeyUpdate 2^37 +# +# QUIC TLS key limits on symmetric cryptographic algorithms +# +# This security property sets limits on algorithms key usage in QUIC. +# When the number of encrypted datagrams reaches the algorithm value +# listed below, key update operation will be initiated. +# +# The syntax for the property is described below: +# KeyLimits: +# " KeyLimit { , KeyLimit } " +# +# KeyLimit: +# AlgorithmName Length +# +# AlgorithmName: +# A full algorithm transformation. +# +# Length: +# The amount of encrypted data in a session before the Action occurs +# This value may be an integer value in bytes, or as a power of two, 2^23. +# +# Note: This property is currently used by OpenJDK's JSSE implementation. It +# is not guaranteed to be examined and used by other implementations. +# +jdk.quic.tls.keyLimits=AES/GCM/NoPadding 2^23, \ + ChaCha20-Poly1305 2^23 + # # Cryptographic Jurisdiction Policy defaults # diff --git a/src/java.base/share/data/currency/CurrencyData.properties b/src/java.base/share/data/currency/CurrencyData.properties index ff2d1f87ed3..9b82318776a 100644 --- a/src/java.base/share/data/currency/CurrencyData.properties +++ b/src/java.base/share/data/currency/CurrencyData.properties @@ -32,7 +32,7 @@ formatVersion=3 # Version of the currency code information in this class. # It is a serial number that accompanies with each amendment. -dataVersion=179 +dataVersion=180 # List of all valid ISO 4217 currency codes. # To ensure compatibility, do not remove codes. @@ -147,7 +147,7 @@ IO=USD # BRUNEI DARUSSALAM BN=BND # BULGARIA -BG=BGN +BG=BGN;2025-12-31-22-00-00;EUR # BURKINA FASO BF=XOF # BURUNDI @@ -193,7 +193,7 @@ HR=EUR # CUBA CU=CUP # Curaçao -CW=ANG;2025-04-01-04-00-00;XCG +CW=XCG # CYPRUS CY=EUR # CZECHIA @@ -510,7 +510,7 @@ SR=SRD # SVALBARD AND JAN MAYEN SJ=NOK # Sint Maarten (Dutch part) -SX=ANG;2025-04-01-04-00-00;XCG +SX=XCG # ESWATINI SZ=SZL # SWEDEN diff --git a/src/java.base/share/legal/aes.md b/src/java.base/share/legal/aes.md deleted file mode 100644 index 6d0ee2e2bb4..00000000000 --- a/src/java.base/share/legal/aes.md +++ /dev/null @@ -1,36 +0,0 @@ -## Cryptix AES v3.2.0 - -### Cryptix General License -

      -
      -Cryptix General License
      -
      -Copyright (c) 1995-2005 The Cryptix Foundation Limited.
      -All rights reserved.
      -
      -Redistribution and use in source and binary forms, with or without
      -modification, are permitted provided that the following conditions are
      -met:
      -
      -  1. Redistributions of source code must retain the copyright notice,
      -     this list of conditions and the following disclaimer.
      -
      -  2. Redistributions in binary form must reproduce the above copyright
      -     notice, this list of conditions and the following disclaimer in
      -     the documentation and/or other materials provided with the
      -     distribution.
      -
      -THIS SOFTWARE IS PROVIDED BY THE CRYPTIX FOUNDATION LIMITED AND
      -CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
      -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
      -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
      -IN NO EVENT SHALL THE CRYPTIX FOUNDATION LIMITED OR CONTRIBUTORS BE
      -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
      -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
      -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
      -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
      -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
      -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
      -IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
      -
      -
      diff --git a/src/java.base/share/man/java.md b/src/java.base/share/man/java.md index 1a6a944594f..1e9eaa67d6d 100644 --- a/src/java.base/share/man/java.md +++ b/src/java.base/share/man/java.md @@ -2448,7 +2448,7 @@ Java HotSpot VM. : Sets the initial amount of memory that the JVM will use for the Java heap before applying ergonomics heuristics as a percentage of the maximum amount determined as described in the `-XX:MaxRAM` option. The default value is - 1.5625 percent. + 0.2 percent. The following example shows how to set the percentage of the initial amount of memory used for the Java heap: @@ -2566,25 +2566,6 @@ Java HotSpot VM. : Sets the maximum size (in bytes) of the heap for the young generation (nursery). The default value is set ergonomically. -`-XX:MaxRAM=`*size* -: Sets the maximum amount of memory that the JVM may use for the Java heap - before applying ergonomics heuristics. The default value is the maximum - amount of available memory to the JVM process or 128 GB, whichever is lower. - - The maximum amount of available memory to the JVM process is the minimum - of the machine's physical memory and any constraints set by the environment - (e.g. container). - - Specifying this option disables automatic use of compressed oops if - the combined result of this and other options influencing the maximum amount - of memory is larger than the range of memory addressable by compressed oops. - See `-XX:UseCompressedOops` for further information about compressed oops. - - The following example shows how to set the maximum amount of available - memory for sizing the Java heap to 2 GB: - - > `-XX:MaxRAM=2G` - `-XX:MaxRAMPercentage=`*percent* : Sets the maximum amount of memory that the JVM may use for the Java heap before applying ergonomics heuristics as a percentage of the maximum amount @@ -2951,6 +2932,25 @@ they're used. (`-XX:+UseParallelGC` or `-XX:+UseG1GC`). Other collectors employing multiple threads always perform reference processing in parallel. +`-XX:MaxRAM=`*size* +: Sets the maximum amount of memory that the JVM may use for the Java heap + before applying ergonomics heuristics. The default value is the amount of + available memory to the JVM process. + + The maximum amount of available memory to the JVM process is the minimum + of the machine's physical memory and any constraints set by the environment + (e.g. container). + + Specifying this option disables automatic use of compressed oops if + the combined result of this and other options influencing the maximum amount + of memory is larger than the range of memory addressable by compressed oops. + See `-XX:UseCompressedOops` for further information about compressed oops. + + The following example shows how to set the maximum amount of available + memory for sizing the Java heap to 2 GB: + + > `-XX:MaxRAM=2G` + ## Obsolete Java Options These `java` options are still accepted but ignored, and a warning is issued diff --git a/src/java.base/share/native/libjava/Class.c b/src/java.base/share/native/libjava/Class.c index 9fb348d9217..95e8e2c169b 100644 --- a/src/java.base/share/native/libjava/Class.c +++ b/src/java.base/share/native/libjava/Class.c @@ -95,7 +95,7 @@ Java_java_lang_Class_registerNatives(JNIEnv *env, jclass cls) JNIEXPORT jclass JNICALL Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname, - jboolean initialize, jobject loader, jclass caller) + jboolean initialize, jobject loader) { char *clname; jclass cls = 0; @@ -133,7 +133,7 @@ Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname, goto done; } - cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller); + cls = JVM_FindClassFromLoader(env, clname, initialize, loader); done: if (clname != buf) { diff --git a/src/java.base/share/native/libverify/check_code.c b/src/java.base/share/native/libverify/check_code.c index 7266ac8f93c..32df102dcb3 100644 --- a/src/java.base/share/native/libverify/check_code.c +++ b/src/java.base/share/native/libverify/check_code.c @@ -395,7 +395,8 @@ static jboolean is_superclass(context_type *, fullinfo_type); static void initialize_exception_table(context_type *); static int instruction_length(unsigned char *iptr, unsigned char *end); -static jboolean isLegalTarget(context_type *, int offset); +static jboolean isLegalOffset(context_type *, int bci, int offset); +static jboolean isLegalTarget(context_type *, int target); static void verify_constant_pool_type(context_type *, int, unsigned); static void initialize_dataflow(context_type *); @@ -1154,9 +1155,9 @@ verify_opcode_operands(context_type *context, unsigned int inumber, int offset) case JVM_OPC_goto: { /* Set the ->operand to be the instruction number of the target. */ int jump = (((signed char)(code[offset+1])) << 8) + code[offset+2]; - int target = offset + jump; - if (!isLegalTarget(context, target)) + if (!isLegalOffset(context, offset, jump)) CCerror(context, "Illegal target of jump or branch"); + int target = offset + jump; this_idata->operand.i = code_data[target]; break; } @@ -1170,9 +1171,9 @@ verify_opcode_operands(context_type *context, unsigned int inumber, int offset) int jump = (((signed char)(code[offset+1])) << 24) + (code[offset+2] << 16) + (code[offset+3] << 8) + (code[offset + 4]); - int target = offset + jump; - if (!isLegalTarget(context, target)) + if (!isLegalOffset(context, offset, jump)) CCerror(context, "Illegal target of jump or branch"); + int target = offset + jump; this_idata->operand.i = code_data[target]; break; } @@ -1211,13 +1212,16 @@ verify_opcode_operands(context_type *context, unsigned int inumber, int offset) } } saved_operand = NEW(int, keys + 2); - if (!isLegalTarget(context, offset + _ck_ntohl(lpc[0]))) + int jump = _ck_ntohl(lpc[0]); + if (!isLegalOffset(context, offset, jump)) CCerror(context, "Illegal default target in switch"); - saved_operand[keys + 1] = code_data[offset + _ck_ntohl(lpc[0])]; + int target = offset + jump; + saved_operand[keys + 1] = code_data[target]; for (k = keys, lptr = &lpc[3]; --k >= 0; lptr += delta) { - int target = offset + _ck_ntohl(lptr[0]); - if (!isLegalTarget(context, target)) + jump = _ck_ntohl(lptr[0]); + if (!isLegalOffset(context, offset, jump)) CCerror(context, "Illegal branch in tableswitch"); + target = offset + jump; saved_operand[k + 1] = code_data[target]; } saved_operand[0] = keys + 1; /* number of successors */ @@ -1746,11 +1750,24 @@ static int instruction_length(unsigned char *iptr, unsigned char *end) /* Given the target of a branch, make sure that it's a legal target. */ static jboolean -isLegalTarget(context_type *context, int offset) +isLegalTarget(context_type *context, int target) { int code_length = context->code_length; int *code_data = context->code_data; - return (offset >= 0 && offset < code_length && code_data[offset] >= 0); + return (target >= 0 && target < code_length && code_data[target] >= 0); +} + +/* Given a bci and offset, make sure the offset is valid and the target is legal */ +static jboolean +isLegalOffset(context_type *context, int bci, int offset) +{ + int code_length = context->code_length; + int *code_data = context->code_data; + int max_offset = 65535; // JVMS 4.11 + int min_offset = -65535; + if (offset < min_offset || offset > max_offset) return JNI_FALSE; + int target = bci + offset; + return (target >= 0 && target < code_length && code_data[target] >= 0); } diff --git a/src/java.base/unix/classes/sun/nio/fs/UnixPath.java b/src/java.base/unix/classes/sun/nio/fs/UnixPath.java index 6b0cd1bbf63..b722c30db42 100644 --- a/src/java.base/unix/classes/sun/nio/fs/UnixPath.java +++ b/src/java.base/unix/classes/sun/nio/fs/UnixPath.java @@ -948,28 +948,51 @@ class UnixPath implements Path { // Obtain the stream of entries in the directory corresponding // to the path constructed thus far, and extract the entry whose - // key is equal to the key of the current element + // internal path bytes equal the internal path bytes of the current + // element, or whose key is equal to the key of the current element + boolean found = false; DirectoryStream.Filter filter = (p) -> { return true; }; + // compare path bytes until a match is found or no more entries try (DirectoryStream entries = new UnixDirectoryStream(path, dp, filter)) { - boolean found = false; for (Path entry : entries) { - UnixPath p = path.resolve(entry.getFileName()); - UnixFileAttributes attributes = null; - try { - attributes = UnixFileAttributes.get(p, false); - UnixFileKey key = attributes.fileKey(); - if (key.equals(elementKey)) { - path = path.resolve(entry); - found = true; - break; + Path name = entry.getFileName(); + if (name.compareTo(element) == 0) { + found = true; + path = path.resolve(entry); + break; + } + } + } + + // if no path match found, compare file keys + if (!found) { + try { + dp = opendir(path); + } catch (UnixException x) { + x.rethrowAsIOException(path); + } + + try (DirectoryStream entries = new UnixDirectoryStream(path, dp, filter)) { + for (Path entry : entries) { + Path name = entry.getFileName(); + UnixPath p = path.resolve(name); + UnixFileAttributes attributes = null; + try { + attributes = UnixFileAttributes.get(p, false); + UnixFileKey key = attributes.fileKey(); + if (key.equals(elementKey)) { + found = true; + path = path.resolve(entry); + break; + } + } catch (UnixException ignore) { + continue; } - } catch (UnixException ignore) { - continue; } } - // Fallback which should in theory never happen if (!found) { + // Fallback which should in theory never happen path = path.resolve(element); } } diff --git a/src/java.base/unix/native/libjava/childproc.c b/src/java.base/unix/native/libjava/childproc.c index c4b5a2d7b29..9c6334e52d2 100644 --- a/src/java.base/unix/native/libjava/childproc.c +++ b/src/java.base/unix/native/libjava/childproc.c @@ -67,24 +67,39 @@ markCloseOnExec(int fd) return 0; } +#if !defined(_AIX) + /* The /proc file system on AIX does not contain open system files + * like /dev/random. Therefore we use a different approach and do + * not need isAsciiDigit() or FD_DIR */ static int isAsciiDigit(char c) { return c >= '0' && c <= '9'; } -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - #define FD_DIR aix_fd_dir -#elif defined(_ALLBSD_SOURCE) - #define FD_DIR "/dev/fd" -#else - #define FD_DIR "/proc/self/fd" + #if defined(_ALLBSD_SOURCE) + #define FD_DIR "/dev/fd" + #else + #define FD_DIR "/proc/self/fd" + #endif #endif static int markDescriptorsCloseOnExec(void) { +#if defined(_AIX) + /* On AIX, we cannot rely on proc file system iteration to find all open files. Since + * iteration over all possible file descriptors, and subsequently closing them, can + * take a very long time, we use a bulk close via `ioctl` that is available on AIX. + * Since we hard-close, we need to make sure to keep the fail pipe file descriptor + * alive until the exec call. Therefore we mark the fail pipe fd with close on exec + * like the other OSes do, but then proceed to hard-close file descriptors beyond that. + */ + if (fcntl(FAIL_FILENO + 1, F_CLOSEM, 0) == -1 || + (markCloseOnExec(FAIL_FILENO) == -1 && errno != EBADF)) { + return -1; + } +#else DIR *dp; struct dirent *dirp; /* This function marks all file descriptors beyond stderr as CLOEXEC. @@ -93,12 +108,6 @@ markDescriptorsCloseOnExec(void) * execve. */ const int fd_from = STDERR_FILENO + 1; -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - char aix_fd_dir[32]; /* the pid has at most 19 digits */ - snprintf(aix_fd_dir, 32, "/proc/%d/fd", getpid()); -#endif - if ((dp = opendir(FD_DIR)) == NULL) return -1; @@ -114,6 +123,7 @@ markDescriptorsCloseOnExec(void) } closedir(dp); +#endif return 0; } @@ -406,6 +416,10 @@ childProcess(void *arg) /* We moved the fail pipe fd */ fail_pipe_fd = FAIL_FILENO; + /* For AIX: The code in markDescriptorsCloseOnExec() relies on the current + * semantic of this function. When this point here is reached only the + * FDs 0,1,2 and 3 are further used until the exec() or the exit(-1). */ + /* close everything */ if (markDescriptorsCloseOnExec() == -1) { /* failed, close the old way */ int max_fd = (int)sysconf(_SC_OPEN_MAX); diff --git a/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c b/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c index 60ccdfc45fc..4b5cfabebfb 100644 --- a/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c +++ b/src/java.base/unix/native/libnio/fs/UnixNativeDispatcher.c @@ -55,6 +55,7 @@ #include #ifdef __linux__ +#include // For uintXX_t types used in statx support #include #include // makedev macros #endif @@ -70,19 +71,12 @@ // by defining binary compatible statx structs in this file and // not relying on included headers. -#ifndef __GLIBC__ -// Alpine doesn't know these types, define them -typedef unsigned int __uint32_t; -typedef unsigned short __uint16_t; -typedef unsigned long int __uint64_t; -#endif - /* * Timestamp structure for the timestamps in struct statx. */ struct my_statx_timestamp { int64_t tv_sec; - __uint32_t tv_nsec; + uint32_t tv_nsec; int32_t __reserved; }; @@ -92,27 +86,27 @@ struct my_statx_timestamp { */ struct my_statx { - __uint32_t stx_mask; - __uint32_t stx_blksize; - __uint64_t stx_attributes; - __uint32_t stx_nlink; - __uint32_t stx_uid; - __uint32_t stx_gid; - __uint16_t stx_mode; - __uint16_t __statx_pad1[1]; - __uint64_t stx_ino; - __uint64_t stx_size; - __uint64_t stx_blocks; - __uint64_t stx_attributes_mask; + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t __statx_pad1[1]; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; struct my_statx_timestamp stx_atime; struct my_statx_timestamp stx_btime; struct my_statx_timestamp stx_ctime; struct my_statx_timestamp stx_mtime; - __uint32_t stx_rdev_major; - __uint32_t stx_rdev_minor; - __uint32_t stx_dev_major; - __uint32_t stx_dev_minor; - __uint64_t __statx_pad2[14]; + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + uint64_t __statx_pad2[14]; }; // statx masks, flags, constants diff --git a/src/java.base/windows/classes/java/lang/ProcessImpl.java b/src/java.base/windows/classes/java/lang/ProcessImpl.java index 7f7c1e75013..78180cce678 100644 --- a/src/java.base/windows/classes/java/lang/ProcessImpl.java +++ b/src/java.base/windows/classes/java/lang/ProcessImpl.java @@ -199,7 +199,6 @@ final class ProcessImpl extends Process { } private static final int VERIFICATION_CMD_BAT = 0; - private static final int VERIFICATION_WIN32 = 1; private static final int VERIFICATION_WIN32_SAFE = 2; // inside quotes not allowed private static final int VERIFICATION_LEGACY = 3; // See Command shell overview for documentation of special characters. @@ -384,12 +383,6 @@ final class ProcessImpl extends Process { return (upName.endsWith(".EXE") || upName.indexOf('.') < 0); } - // Old version that can be bypassed - private boolean isShellFile(String executablePath) { - String upPath = executablePath.toUpperCase(Locale.ROOT); - return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT")); - } - private String quoteString(String arg) { StringBuilder argbuf = new StringBuilder(arg.length() + 2); return argbuf.append('"').append(arg).append('"').toString(); @@ -472,12 +465,10 @@ final class ProcessImpl extends Process { // Quotation protects from interpretation of the [path] argument as // start of longer path with spaces. Quotation has no influence to // [.exe] extension heuristic. - boolean isShell = allowAmbiguousCommands ? isShellFile(executablePath) - : !isExe(executablePath); + boolean isShell = !isExe(executablePath); cmdstr = createCommandLine( // We need the extended verification procedures - isShell ? VERIFICATION_CMD_BAT - : (allowAmbiguousCommands ? VERIFICATION_WIN32 : VERIFICATION_WIN32_SAFE), + isShell ? VERIFICATION_CMD_BAT : VERIFICATION_WIN32_SAFE, quoteString(executablePath), cmd); } diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java index b1de66ac4f2..0c09a80e99e 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java @@ -96,6 +96,7 @@ class WindowsConstants { public static final int ERROR_NOT_SAME_DEVICE = 17; public static final int ERROR_NOT_READY = 21; public static final int ERROR_SHARING_VIOLATION = 32; + public static final int ERROR_NETWORK_ACCESS_DENIED = 65; public static final int ERROR_FILE_EXISTS = 80; public static final int ERROR_INVALID_PARAMETER = 87; public static final int ERROR_DISK_FULL = 112; @@ -110,6 +111,7 @@ class WindowsConstants { public static final int ERROR_PRIVILEGE_NOT_HELD = 1314; public static final int ERROR_NONE_MAPPED = 1332; public static final int ERROR_CANT_ACCESS_FILE = 1920; + public static final int ERROR_CANT_RESOLVE_FILENAME = 1921; public static final int ERROR_NOT_A_REPARSE_POINT = 4390; public static final int ERROR_INVALID_REPARSE_DATA = 4392; diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsException.java b/src/java.base/windows/classes/sun/nio/fs/WindowsException.java index f1eff69210b..532728f57ed 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsException.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, 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 @@ -76,20 +76,21 @@ class WindowsException extends Exception { } private IOException translateToIOException(String file, String other) { - // not created with last error - if (lastError() == 0) - return new IOException(errorString()); + return switch (lastError()) { + // not created with last error + case 0 -> new IOException(errorString()); - // handle specific cases - if (lastError() == ERROR_FILE_NOT_FOUND || lastError() == ERROR_PATH_NOT_FOUND) - return new NoSuchFileException(file, other, null); - if (lastError() == ERROR_FILE_EXISTS || lastError() == ERROR_ALREADY_EXISTS) - return new FileAlreadyExistsException(file, other, null); - if (lastError() == ERROR_ACCESS_DENIED) - return new AccessDeniedException(file, other, null); + // handle specific cases + case ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND + -> new NoSuchFileException(file, other, null); + case ERROR_FILE_EXISTS, ERROR_ALREADY_EXISTS + -> new FileAlreadyExistsException(file, other, null); + case ERROR_ACCESS_DENIED, ERROR_NETWORK_ACCESS_DENIED, ERROR_PRIVILEGE_NOT_HELD + -> new AccessDeniedException(file, other, null); - // fallback to the more general exception - return new FileSystemException(file, other, errorString()); + // fallback to the more general exception + default -> new FileSystemException(file, other, errorString()); + }; } void rethrowAsIOException(String file) throws IOException { diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java index 5e740ec1f4d..58e6294cc12 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java @@ -418,65 +418,178 @@ class WindowsFileSystemProvider } } + /** + * Contains the attributes of a given file system entry and the open + * handle from which they were obtained. The handle must remain open + * until the volume serial number and file index of the attributes + * are no longer needed for comparison with other attributes. + * + * @param attrs the file system entry attributes + * @param handle the open Windows file handle + */ + private record EntryAttributes(WindowsFileAttributes attrs, long handle) { + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof EntryAttributes other) { + WindowsFileAttributes oattrs = other.attrs(); + return oattrs.volSerialNumber() == attrs.volSerialNumber() && + oattrs.fileIndexHigh() == attrs.fileIndexHigh() && + oattrs.fileIndexLow() == attrs.fileIndexLow(); + } + return false; + } + + public int hashCode() { + return attrs.volSerialNumber() + + attrs.fileIndexHigh() + attrs.fileIndexLow(); + } + } + + /** + * Returns the attributes of the file located by the given path if it is a + * symbolic link. The handle contained in the returned value must be closed + * once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no link is found + */ + private EntryAttributes linkAttributes(WindowsPath path) + throws WindowsException + { + long h = INVALID_HANDLE_VALUE; + try { + h = path.openForReadAttributeAccess(false); + } catch (WindowsException x) { + if (x.lastError() != ERROR_FILE_NOT_FOUND && + x.lastError() != ERROR_PATH_NOT_FOUND) + throw x; + return null; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } finally { + if (attrs == null || !attrs.isSymbolicLink()) { + CloseHandle(h); + return null; + } + } + + return new EntryAttributes(attrs, h); + } + + /** + * Returns the attributes of the last symbolic link encountered in the + * specified path. Links are not resolved in the path taken as a whole, + * but rather the first link is followed, then its target, and so on, + * until no more links are encountered. The handle contained in the + * returned value must be closed once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no links are found + * @throws FileSystemLoopException if a symbolic link cycle is encountered + */ + private EntryAttributes lastLinkAttributes(WindowsPath path) + throws IOException, WindowsException + { + var linkAttrs = new LinkedHashSet(); + try { + while (path != null) { + EntryAttributes linkAttr = linkAttributes(path); + if (linkAttr == null) + break; + + if (!linkAttrs.add(linkAttr)) { + // the element was not added to the set so close its handle + // here as it would not be closed in the finally block + CloseHandle(linkAttr.handle()); + throw new FileSystemLoopException(path.toString()); + } + + String target = WindowsLinkSupport.readLink(path, linkAttr.handle()); + path = WindowsPath.parse(path.getFileSystem(), target); + } + + if (!linkAttrs.isEmpty()) + return linkAttrs.removeLast(); + } finally { + linkAttrs.stream().forEach(la -> CloseHandle(la.handle())); + } + + return null; + } + + /** + * Returns the attributes of the file located by the supplied parameter + * with all symbolic links in its path resolved. If the file located by + * the resolved path does not exist, then null is returned. The handle + * contained in the returned value must be closed once the attributes + * are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if the real path does not exist + */ + private EntryAttributes realPathAttributes(WindowsPath path) + throws WindowsException + { + long h; + try { + h = path.openForReadAttributeAccess(true); + } catch (WindowsException x) { + if (x.lastError() == ERROR_FILE_NOT_FOUND || + x.lastError() == ERROR_PATH_NOT_FOUND || + x.lastError() == ERROR_CANT_RESOLVE_FILENAME) + return null; + + throw x; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } catch (WindowsException x) { + CloseHandle(h); + throw x; + } + + return new EntryAttributes(attrs, h); + } + @Override public boolean isSameFile(Path obj1, Path obj2) throws IOException { + // toWindowsPath verifies its argument is a non-null WindowsPath WindowsPath file1 = WindowsPath.toWindowsPath(obj1); if (file1.equals(obj2)) return true; if (obj2 == null) throw new NullPointerException(); - if (!(obj2 instanceof WindowsPath)) + if (!(obj2 instanceof WindowsPath file2)) return false; - WindowsPath file2 = (WindowsPath)obj2; - // open both files and see if they are the same - long h1 = 0L; + EntryAttributes attrs1 = null; + EntryAttributes attrs2 = null; + WindowsPath pathForException = file1; try { - h1 = file1.openForReadAttributeAccess(true); + if ((attrs1 = realPathAttributes(file1)) != null || + (attrs1 = lastLinkAttributes(file1)) != null) { + pathForException = file2; + if ((attrs2 = realPathAttributes(file2)) != null || + (attrs2 = lastLinkAttributes(file2)) != null) + return attrs1.equals(attrs2); + } } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file1); - } - - // if file1 does not exist, it cannot equal file2 - if (h1 == 0L) - return false; - - try { - WindowsFileAttributes attrs1 = null; - try { - attrs1 = WindowsFileAttributes.readAttributes(h1); - } catch (WindowsException x) { - x.rethrowAsIOException(file1); - } - long h2 = 0L; - try { - h2 = file2.openForReadAttributeAccess(true); - } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file2); - } - - // if file2 does not exist, it cannot equal file1, which does - if (h2 == 0L) - return false; - - try { - WindowsFileAttributes attrs2 = null; - try { - attrs2 = WindowsFileAttributes.readAttributes(h2); - } catch (WindowsException x) { - x.rethrowAsIOException(file2); - } - return WindowsFileAttributes.isSameFile(attrs1, attrs2); - } finally { - CloseHandle(h2); - } + x.rethrowAsIOException(pathForException); } finally { - CloseHandle(h1); + if (attrs1 != null) { + CloseHandle(attrs1.handle()); + if (attrs2 != null) + CloseHandle(attrs2.handle()); + } } + + return false; } @Override diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java index a5adfb73ebc..fc79153914d 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java @@ -84,7 +84,7 @@ class WindowsLinkSupport { x.rethrowAsIOException(path); } try { - return readLinkImpl(path, handle); + return readLink(path, handle); } finally { CloseHandle(handle); } @@ -289,9 +289,7 @@ class WindowsLinkSupport { * Returns target of a symbolic link given the handle of an open file * (that should be a link). */ - private static String readLinkImpl(WindowsPath path, long handle) - throws IOException - { + static String readLink(WindowsPath path, long handle) throws IOException { int size = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; try (NativeBuffer buffer = NativeBuffers.getNativeBuffer(size)) { try { diff --git a/src/java.base/windows/native/libjava/WinNTFileSystem_md.c b/src/java.base/windows/native/libjava/WinNTFileSystem_md.c index ccf89eb71e0..974d5c11d7e 100644 --- a/src/java.base/windows/native/libjava/WinNTFileSystem_md.c +++ b/src/java.base/windows/native/libjava/WinNTFileSystem_md.c @@ -60,7 +60,6 @@ Java_java_io_WinNTFileSystem_initIDs(JNIEnv *env, jclass cls) /* -- Path operations -- */ extern int wcanonicalize(const WCHAR *path, WCHAR *out, int len); -extern int wcanonicalizeWithPrefix(const WCHAR *canonicalPrefix, const WCHAR *pathWithCanonicalPrefix, WCHAR *out, int len); /** * Retrieves the fully resolved (final) path for the given path or NULL @@ -296,41 +295,6 @@ Java_java_io_WinNTFileSystem_canonicalize0(JNIEnv *env, jobject this, } -JNIEXPORT jstring JNICALL -Java_java_io_WinNTFileSystem_canonicalizeWithPrefix0(JNIEnv *env, jobject this, - jstring canonicalPrefixString, - jstring pathWithCanonicalPrefixString) -{ - jstring rv = NULL; - WCHAR canonicalPath[MAX_PATH_LENGTH]; - WITH_UNICODE_STRING(env, canonicalPrefixString, canonicalPrefix) { - WITH_UNICODE_STRING(env, pathWithCanonicalPrefixString, pathWithCanonicalPrefix) { - int len = (int)wcslen(canonicalPrefix) + MAX_PATH; - if (len > MAX_PATH_LENGTH) { - WCHAR *cp = (WCHAR*)malloc(len * sizeof(WCHAR)); - if (cp != NULL) { - if (wcanonicalizeWithPrefix(canonicalPrefix, - pathWithCanonicalPrefix, - cp, len) >= 0) { - rv = (*env)->NewString(env, cp, (jsize)wcslen(cp)); - } - free(cp); - } else { - JNU_ThrowOutOfMemoryError(env, "native memory allocation failed"); - } - } else if (wcanonicalizeWithPrefix(canonicalPrefix, - pathWithCanonicalPrefix, - canonicalPath, MAX_PATH_LENGTH) >= 0) { - rv = (*env)->NewString(env, canonicalPath, (jsize)wcslen(canonicalPath)); - } - } END_UNICODE_STRING(env, pathWithCanonicalPrefix); - } END_UNICODE_STRING(env, canonicalPrefix); - if (rv == NULL && !(*env)->ExceptionCheck(env)) { - JNU_ThrowIOExceptionWithLastError(env, "Bad pathname"); - } - return rv; -} - JNIEXPORT jstring JNICALL Java_java_io_WinNTFileSystem_getFinalPath0(JNIEnv* env, jobject this, jstring pathname) { jstring rv = NULL; diff --git a/src/java.base/windows/native/libjava/canonicalize_md.c b/src/java.base/windows/native/libjava/canonicalize_md.c index ecfdf63d091..3719ec75d11 100644 --- a/src/java.base/windows/native/libjava/canonicalize_md.c +++ b/src/java.base/windows/native/libjava/canonicalize_md.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -41,7 +41,7 @@ /* We should also include jdk_util.h here, for the prototype of JDK_Canonicalize. This isn't possible though because canonicalize_md.c is as well used in different contexts within Oracle. - */ +*/ #include "io_util_md.h" /* Copy bytes to dst, not going past dend; return dst + number of bytes copied, @@ -139,7 +139,8 @@ lastErrorReportable() || (errval == ERROR_ACCESS_DENIED) || (errval == ERROR_NETWORK_UNREACHABLE) || (errval == ERROR_NETWORK_ACCESS_DENIED) - || (errval == ERROR_NO_MORE_FILES)) { + || (errval == ERROR_NO_MORE_FILES) + || (errval == ERROR_NETNAME_DELETED)) { return 0; } return 1; @@ -183,7 +184,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) /* Copy prefix, assuming path is absolute */ c = src[0]; if (((c <= L'z' && c >= L'a') || (c <= L'Z' && c >= L'A')) - && (src[1] == L':') && (src[2] == L'\\')) { + && (src[1] == L':') && (src[2] == L'\\')) { /* Drive specifier */ *src = towupper(*src); /* Canonicalize drive letter */ if (!(dst = wcp(dst, dend, L'\0', src, src + 2))) { @@ -244,9 +245,9 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) continue; } else { if (!lastErrorReportable()) { - if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ - goto err; - } + if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ + goto err; + } break; } else { goto err; @@ -255,7 +256,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) } if (dst >= dend) { - errno = ENAMETOOLONG; + errno = ENAMETOOLONG; goto err; } *dst = L'\0'; @@ -267,64 +268,6 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) return -1; } -/* Convert a pathname to canonical form. The input prefix is assumed - to be in canonical form already, and the trailing filename must not - contain any wildcard, dot/double dot, or other "tricky" characters - that are rejected by the canonicalize() routine above. This - routine is present to allow the canonicalization prefix cache to be - used while still returning canonical names with the correct - capitalization. */ -int -wcanonicalizeWithPrefix(WCHAR *canonicalPrefix, WCHAR *pathWithCanonicalPrefix, WCHAR *result, int size) -{ - WIN32_FIND_DATAW fd; - HANDLE h; - WCHAR *src, *dst, *dend; - WCHAR *pathbuf; - int pathlen; - - src = pathWithCanonicalPrefix; - dst = result; /* Place results here */ - dend = dst + size; /* Don't go to or past here */ - - - if ((pathlen=(int)wcslen(pathWithCanonicalPrefix)) > MAX_PATH - 1) { - pathbuf = getPrefixed(pathWithCanonicalPrefix, pathlen); - h = FindFirstFileW(pathbuf, &fd); /* Look up prefix */ - free(pathbuf); - } else - h = FindFirstFileW(pathWithCanonicalPrefix, &fd); /* Look up prefix */ - if (h != INVALID_HANDLE_VALUE) { - /* Lookup succeeded; append true name to result and continue */ - FindClose(h); - if (!(dst = wcp(dst, dend, L'\0', - canonicalPrefix, - canonicalPrefix + wcslen(canonicalPrefix)))) { - return -1; - } - if (!(dst = wcp(dst, dend, L'\\', - fd.cFileName, - fd.cFileName + wcslen(fd.cFileName)))) { - return -1; - } - } else { - if (!lastErrorReportable()) { - if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))) { - return -1; - } - } else { - return -1; - } - } - - if (dst >= dend) { - errno = ENAMETOOLONG; - return -1; - } - *dst = L'\0'; - return 0; -} - /* Non-Wide character version of canonicalize. Converts to wchar and delegates to wcanonicalize. */ JNIEXPORT int @@ -366,7 +309,7 @@ JDK_Canonicalize(const char *orig, char *out, int len) { // Change return value to success. ret = 0; -finish: + finish: free(wresult); free(wpath); diff --git a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java index c3ed089472b..45fcdfd2c0c 100644 --- a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java +++ b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -75,4 +75,16 @@ public final class CPrinterGraphics extends ProxyGraphics2D { // needToCopyBgColorImage, is private instead of protected!) return getDelegate().drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); } + + @Override + public void drawString(String str, int x, int y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } } diff --git a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m index 9da3b6648fb..f1c12585728 100644 --- a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m +++ b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -457,15 +457,17 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers } } if (positions != NULL) { + + CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); + CGPoint prev; prev.x = positions[0]; prev.y = positions[1]; + prev = CGPointApplyAffineTransform(prev, invTx); // take the first point, and move the context to that location CGContextTranslateCTM(qsdo->cgRef, prev.x, prev.y); - CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); - // for each position, figure out the advance (since CG won't take positions directly) size_t i; for (i = 0; i < length - 1; i++) @@ -476,7 +478,7 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers pt.y = positions[i2+1]; pt = CGPointApplyAffineTransform(pt, invTx); advances[i].width = pt.x - prev.x; - advances[i].height = -(pt.y - prev.y); // negative to translate to device space + advances[i].height = pt.y - prev.y; prev.x = pt.x; prev.y = pt.y; } diff --git a/src/java.desktop/share/classes/com/sun/beans/WildcardTypeImpl.java b/src/java.desktop/share/classes/com/sun/beans/WildcardTypeImpl.java index 28e316c90ec..ebbc8d2cb26 100644 --- a/src/java.desktop/share/classes/com/sun/beans/WildcardTypeImpl.java +++ b/src/java.desktop/share/classes/com/sun/beans/WildcardTypeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -73,6 +73,7 @@ final class WildcardTypeImpl implements WildcardType { * @return an array of types representing * the upper bound(s) of this type variable */ + @Override public Type[] getUpperBounds() { return this.upperBounds.clone(); } @@ -87,6 +88,7 @@ final class WildcardTypeImpl implements WildcardType { * @return an array of types representing * the lower bound(s) of this type variable */ + @Override public Type[] getLowerBounds() { return this.lowerBounds.clone(); } diff --git a/src/java.desktop/share/classes/com/sun/beans/decoder/NullElementHandler.java b/src/java.desktop/share/classes/com/sun/beans/decoder/NullElementHandler.java index f865535e4fb..d5ac5368f9a 100644 --- a/src/java.desktop/share/classes/com/sun/beans/decoder/NullElementHandler.java +++ b/src/java.desktop/share/classes/com/sun/beans/decoder/NullElementHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, 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 @@ -61,6 +61,7 @@ class NullElementHandler extends ElementHandler implements ValueObject { * * @return {@code null} by default */ + @Override public Object getValue() { return null; } @@ -70,6 +71,7 @@ class NullElementHandler extends ElementHandler implements ValueObject { * * @return {@code false} always */ + @Override public final boolean isVoid() { return false; } diff --git a/src/java.desktop/share/classes/com/sun/beans/decoder/ValueObjectImpl.java b/src/java.desktop/share/classes/com/sun/beans/decoder/ValueObjectImpl.java index 6fa46c93fa8..54c73381191 100644 --- a/src/java.desktop/share/classes/com/sun/beans/decoder/ValueObjectImpl.java +++ b/src/java.desktop/share/classes/com/sun/beans/decoder/ValueObjectImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, 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 @@ -72,6 +72,7 @@ final class ValueObjectImpl implements ValueObject { * * @return the result of method execution */ + @Override public Object getValue() { return this.value; } @@ -82,6 +83,7 @@ final class ValueObjectImpl implements ValueObject { * @return {@code true} if value should be ignored, * {@code false} otherwise */ + @Override public boolean isVoid() { return this.isVoid; } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/BooleanEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/BooleanEditor.java index 69aca3238c9..79900b5deb1 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/BooleanEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/BooleanEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class BooleanEditor extends PropertyEditorSupport { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) @@ -41,6 +42,7 @@ public class BooleanEditor extends PropertyEditorSupport { : "null"; } + @Override public String getAsText() { Object value = getValue(); return (value instanceof Boolean) @@ -48,6 +50,7 @@ public class BooleanEditor extends PropertyEditorSupport { : null; } + @Override public void setAsText(String text) throws java.lang.IllegalArgumentException { if (text == null) { setValue(null); @@ -60,6 +63,7 @@ public class BooleanEditor extends PropertyEditorSupport { } } + @Override public String[] getTags() { return new String[] {getValidName(true), getValidName(false)}; } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/ByteEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/ByteEditor.java index 2f4f342774f..fe927fda74d 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/ByteEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/ByteEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class ByteEditor extends NumberEditor { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) @@ -41,6 +42,7 @@ public class ByteEditor extends NumberEditor { : "null"; } + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Byte.decode(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/ColorEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/ColorEditor.java index 3c3207ccd15..a5cf00923dd 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/ColorEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/ColorEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -79,16 +79,19 @@ public class ColorEditor extends Panel implements PropertyEditor { resize(ourWidth,40); } + @Override public void setValue(Object o) { Color c = (Color)o; changeColor(c); } + @Override @SuppressWarnings("deprecation") public Dimension preferredSize() { return new Dimension(ourWidth, 40); } + @Override @SuppressWarnings("deprecation") public boolean keyUp(Event e, int key) { if (e.target == text) { @@ -101,6 +104,7 @@ public class ColorEditor extends Panel implements PropertyEditor { return (false); } + @Override public void setAsText(String s) throws java.lang.IllegalArgumentException { if (s == null) { changeColor(null); @@ -124,6 +128,7 @@ public class ColorEditor extends Panel implements PropertyEditor { } + @Override @SuppressWarnings("deprecation") public boolean action(Event e, Object arg) { if (e.target == chooser) { @@ -132,6 +137,7 @@ public class ColorEditor extends Panel implements PropertyEditor { return false; } + @Override public String getJavaInitializationString() { return (this.color != null) ? "new java.awt.Color(" + this.color.getRGB() + ",true)" @@ -165,14 +171,17 @@ public class ColorEditor extends Panel implements PropertyEditor { support.firePropertyChange("", null, null); } + @Override public Object getValue() { return color; } + @Override public boolean isPaintable() { return true; } + @Override public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) { Color oldColor = gfx.getColor(); gfx.setColor(Color.black); @@ -182,28 +191,34 @@ public class ColorEditor extends Panel implements PropertyEditor { gfx.setColor(oldColor); } + @Override public String getAsText() { return (this.color != null) ? this.color.getRed() + "," + this.color.getGreen() + "," + this.color.getBlue() : null; } + @Override public String[] getTags() { return null; } + @Override public java.awt.Component getCustomEditor() { return this; } + @Override public boolean supportsCustomEditor() { return true; } + @Override public void addPropertyChangeListener(PropertyChangeListener l) { support.addPropertyChangeListener(l); } + @Override public void removePropertyChangeListener(PropertyChangeListener l) { support.removePropertyChangeListener(l); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/DoubleEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/DoubleEditor.java index 55d5a0528a4..3803cca7d7c 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/DoubleEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/DoubleEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class DoubleEditor extends NumberEditor { + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Double.valueOf(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/EnumEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/EnumEditor.java index b7f5ada0d1f..b5316a04d65 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/EnumEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/EnumEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -63,10 +63,12 @@ public final class EnumEditor implements PropertyEditor { } } + @Override public Object getValue() { return this.value; } + @Override public void setValue( Object value ) { if ( ( value != null ) && !this.type.isInstance( value ) ) { throw new IllegalArgumentException( "Unsupported value: " + value ); @@ -92,12 +94,14 @@ public final class EnumEditor implements PropertyEditor { } } + @Override public String getAsText() { return ( this.value != null ) ? ( ( Enum )this.value ).name() : null; } + @Override public void setAsText( String text ) { @SuppressWarnings("unchecked") Object tmp = ( text != null ) @@ -106,10 +110,12 @@ public final class EnumEditor implements PropertyEditor { setValue(tmp); } + @Override public String[] getTags() { return this.tags.clone(); } + @Override public String getJavaInitializationString() { String name = getAsText(); return ( name != null ) @@ -117,27 +123,33 @@ public final class EnumEditor implements PropertyEditor { : "null"; } + @Override public boolean isPaintable() { return false; } + @Override public void paintValue( Graphics gfx, Rectangle box ) { } + @Override public boolean supportsCustomEditor() { return false; } + @Override public Component getCustomEditor() { return null; } + @Override public void addPropertyChangeListener( PropertyChangeListener listener ) { synchronized ( this.listeners ) { this.listeners.add( listener ); } } + @Override public void removePropertyChangeListener( PropertyChangeListener listener ) { synchronized ( this.listeners ) { this.listeners.remove( listener ); diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/FloatEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/FloatEditor.java index 4723c489cc0..5820c00d82e 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/FloatEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/FloatEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class FloatEditor extends NumberEditor { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) @@ -41,6 +42,7 @@ public class FloatEditor extends NumberEditor { : "null"; } + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Float.valueOf(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/FontEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/FontEditor.java index cf2fdd26307..26d4ab2b182 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/FontEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/FontEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -78,11 +78,13 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { } + @Override @SuppressWarnings("deprecation") public Dimension preferredSize() { return new Dimension(300, 40); } + @Override public void setValue(Object o) { font = (Font) o; if (this.font == null) @@ -130,10 +132,12 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { support.firePropertyChange("", null, null); } + @Override public Object getValue() { return (font); } + @Override public String getJavaInitializationString() { if (this.font == null) return "null"; @@ -142,6 +146,7 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { font.getStyle() + ", " + font.getSize() + ")"; } + @Override @SuppressWarnings("deprecation") public boolean action(Event e, Object arg) { String family = familyChoser.getSelectedItem(); @@ -158,10 +163,12 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { } + @Override public boolean isPaintable() { return true; } + @Override public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) { // Silent noop. Font oldFont = gfx.getFont(); @@ -172,6 +179,7 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { gfx.setFont(oldFont); } + @Override public String getAsText() { if (this.font == null) { return null; @@ -195,26 +203,32 @@ public class FontEditor extends Panel implements java.beans.PropertyEditor { return sb.toString(); } + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Font.decode(text)); } + @Override public String[] getTags() { return null; } + @Override public java.awt.Component getCustomEditor() { return this; } + @Override public boolean supportsCustomEditor() { return true; } + @Override public void addPropertyChangeListener(PropertyChangeListener l) { support.addPropertyChangeListener(l); } + @Override public void removePropertyChangeListener(PropertyChangeListener l) { support.removePropertyChangeListener(l); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/IntegerEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/IntegerEditor.java index 066b7143ac6..65b4d1dcf19 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/IntegerEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/IntegerEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -35,6 +35,7 @@ import java.beans.*; public class IntegerEditor extends NumberEditor { + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Integer.decode(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/LongEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/LongEditor.java index 3a8efbba53c..ed4d12ac505 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/LongEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/LongEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class LongEditor extends NumberEditor { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) @@ -41,6 +42,7 @@ public class LongEditor extends NumberEditor { : "null"; } + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Long.decode(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/NumberEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/NumberEditor.java index 9097546d2e0..3c0c5bb6c9f 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/NumberEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/NumberEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public abstract class NumberEditor extends PropertyEditorSupport { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/ShortEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/ShortEditor.java index cf82eef215d..6be5b14b90f 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/ShortEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/ShortEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -35,6 +35,7 @@ import java.beans.*; public class ShortEditor extends NumberEditor { + @Override public String getJavaInitializationString() { Object value = getValue(); return (value != null) @@ -42,6 +43,7 @@ public class ShortEditor extends NumberEditor { : "null"; } + @Override public void setAsText(String text) throws IllegalArgumentException { setValue((text == null) ? null : Short.decode(text)); } diff --git a/src/java.desktop/share/classes/com/sun/beans/editors/StringEditor.java b/src/java.desktop/share/classes/com/sun/beans/editors/StringEditor.java index 2f1cde46ea0..b064ccbddbb 100644 --- a/src/java.desktop/share/classes/com/sun/beans/editors/StringEditor.java +++ b/src/java.desktop/share/classes/com/sun/beans/editors/StringEditor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -30,6 +30,7 @@ import java.beans.*; public class StringEditor extends PropertyEditorSupport { + @Override public String getJavaInitializationString() { Object value = getValue(); if (value == null) @@ -67,6 +68,7 @@ public class StringEditor extends PropertyEditorSupport { return sb.toString(); } + @Override public void setAsText(String text) { setValue(text); } diff --git a/src/java.desktop/share/classes/com/sun/beans/infos/ComponentBeanInfo.java b/src/java.desktop/share/classes/com/sun/beans/infos/ComponentBeanInfo.java index 1514b005074..39d7cbb2146 100644 --- a/src/java.desktop/share/classes/com/sun/beans/infos/ComponentBeanInfo.java +++ b/src/java.desktop/share/classes/com/sun/beans/infos/ComponentBeanInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, 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 @@ -34,6 +34,7 @@ import java.beans.*; public class ComponentBeanInfo extends SimpleBeanInfo { private static final Class beanClass = java.awt.Component.class; + @Override public PropertyDescriptor[] getPropertyDescriptors() { try { PropertyDescriptor diff --git a/src/java.desktop/share/classes/com/sun/beans/util/Cache.java b/src/java.desktop/share/classes/com/sun/beans/util/Cache.java index 2cb21791416..58151e3a56f 100644 --- a/src/java.desktop/share/classes/com/sun/beans/util/Cache.java +++ b/src/java.desktop/share/classes/com/sun/beans/util/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -405,11 +405,13 @@ public abstract class Cache { */ public static enum Kind { STRONG { + @Override Ref create(Object owner, T value, ReferenceQueue queue) { return new Strong<>(owner, value); } }, SOFT { + @Override Ref create(Object owner, T referent, ReferenceQueue queue) { return (referent == null) ? new Strong<>(owner, referent) @@ -417,6 +419,7 @@ public abstract class Cache { } }, WEAK { + @Override Ref create(Object owner, T referent, ReferenceQueue queue) { return (referent == null) ? new Strong<>(owner, referent) @@ -463,6 +466,7 @@ public abstract class Cache { * * @return the owner of the reference or {@code null} if the owner is unknown */ + @Override public Object getOwner() { return this.owner; } @@ -472,6 +476,7 @@ public abstract class Cache { * * @return the referred object */ + @Override public T getReferent() { return this.referent; } @@ -481,6 +486,7 @@ public abstract class Cache { * * @return {@code true} if the referred object was collected */ + @Override public boolean isStale() { return false; } @@ -488,6 +494,7 @@ public abstract class Cache { /** * Marks this reference as removed from the cache. */ + @Override public void removeOwner() { this.owner = null; } @@ -522,6 +529,7 @@ public abstract class Cache { * * @return the owner of the reference or {@code null} if the owner is unknown */ + @Override public Object getOwner() { return this.owner; } @@ -531,6 +539,7 @@ public abstract class Cache { * * @return the referred object or {@code null} if it was collected */ + @Override public T getReferent() { return get(); } @@ -540,6 +549,7 @@ public abstract class Cache { * * @return {@code true} if the referred object was collected */ + @Override public boolean isStale() { return null == get(); } @@ -547,6 +557,7 @@ public abstract class Cache { /** * Marks this reference as removed from the cache. */ + @Override public void removeOwner() { this.owner = null; } @@ -581,6 +592,7 @@ public abstract class Cache { * * @return the owner of the reference or {@code null} if the owner is unknown */ + @Override public Object getOwner() { return this.owner; } @@ -590,6 +602,7 @@ public abstract class Cache { * * @return the referred object or {@code null} if it was collected */ + @Override public T getReferent() { return get(); } @@ -599,6 +612,7 @@ public abstract class Cache { * * @return {@code true} if the referred object was collected */ + @Override public boolean isStale() { return null == get(); } @@ -606,6 +620,7 @@ public abstract class Cache { /** * Marks this reference as removed from the cache. */ + @Override public void removeOwner() { this.owner = null; } diff --git a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java index 91e0f8dc54d..552ef870dbe 100644 --- a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java +++ b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java @@ -146,11 +146,15 @@ public class SwingUtilities3 { } public static void applyInsets(Rectangle rect, Insets insets) { + applyInsets(rect, insets, true); + } + + public static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { if (insets != null) { - rect.x += insets.left; + rect.x += leftToRight ? insets.left : insets.right; rect.y += insets.top; - rect.width -= (insets.right + rect.x); - rect.height -= (insets.bottom + rect.y); + rect.width -= (insets.left + insets.right); + rect.height -= (insets.top + insets.bottom); } } diff --git a/src/java.desktop/share/classes/java/awt/Component.java b/src/java.desktop/share/classes/java/awt/Component.java index e78cab2a14c..e48255aaf00 100644 --- a/src/java.desktop/share/classes/java/awt/Component.java +++ b/src/java.desktop/share/classes/java/awt/Component.java @@ -6287,7 +6287,7 @@ public abstract class Component implements ImageObserver, MenuContainer, * and paint (and update) events. * For mouse move events the last event is always returned, causing * intermediate moves to be discarded. For paint events, the new - * event is coalesced into a complex {@code RepaintArea} in the peer. + * event is coalesced into a complex repaint area in the peer. * The new {@code AWTEvent} is always returned. * * @param existingEvent the event already on the {@code EventQueue} diff --git a/src/java.desktop/share/classes/java/awt/GridBagConstraints.java b/src/java.desktop/share/classes/java/awt/GridBagConstraints.java index 30f8bc4bf50..0008f5ac780 100644 --- a/src/java.desktop/share/classes/java/awt/GridBagConstraints.java +++ b/src/java.desktop/share/classes/java/awt/GridBagConstraints.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, 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 @@ -575,7 +575,7 @@ public class GridBagConstraints implements Cloneable, java.io.Serializable { private static final long serialVersionUID = -1000070633030801713L; /** - * Creates a {@code GridBagConstraint} object with + * Creates a {@code GridBagConstraints} object with * all of its fields set to their default value. */ public GridBagConstraints () { diff --git a/src/java.desktop/share/classes/java/awt/Robot.java b/src/java.desktop/share/classes/java/awt/Robot.java index 957e30126e1..e91cc582c4c 100644 --- a/src/java.desktop/share/classes/java/awt/Robot.java +++ b/src/java.desktop/share/classes/java/awt/Robot.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, 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 @@ -717,8 +717,8 @@ public class Robot { * Sleeps for the specified time. *

      * If the invoking thread is interrupted while waiting, then it will return - * immediately with the interrupt status set. If the interrupted status is - * already set, this method returns immediately with the interrupt status + * immediately with the interrupted status set. If the interrupted status is + * already set, this method returns immediately with the interrupted status * set. * * @apiNote It is recommended to avoid calling this method on @@ -736,7 +736,7 @@ public class Robot { try { Thread.sleep(ms); } catch (final InterruptedException ignored) { - thread.interrupt(); // Preserve interrupt status + thread.interrupt(); // Preserve interrupted status } } } diff --git a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java index 8bf09195627..0c2902ba74a 100644 --- a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java +++ b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java @@ -41,6 +41,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; @@ -1549,33 +1550,19 @@ public sealed class ICC_Profile implements Serializable private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); - - String csName = (String) s.readObject(); - byte[] data = (byte[]) s.readObject(); - - int cspace = 0; // ColorSpace.CS_* constant if known - boolean isKnownPredefinedCS = false; - if (csName != null) { - isKnownPredefinedCS = true; - if (csName.equals("CS_sRGB")) { - cspace = ColorSpace.CS_sRGB; - } else if (csName.equals("CS_CIEXYZ")) { - cspace = ColorSpace.CS_CIEXYZ; - } else if (csName.equals("CS_PYCC")) { - cspace = ColorSpace.CS_PYCC; - } else if (csName.equals("CS_GRAY")) { - cspace = ColorSpace.CS_GRAY; - } else if (csName.equals("CS_LINEAR_RGB")) { - cspace = ColorSpace.CS_LINEAR_RGB; - } else { - isKnownPredefinedCS = false; - } - } - - if (isKnownPredefinedCS) { - resolvedDeserializedProfile = getInstance(cspace); - } else { - resolvedDeserializedProfile = getInstance(data); + try { + String csName = (String) s.readObject(); + byte[] data = (byte[]) s.readObject(); + resolvedDeserializedProfile = switch (csName) { + case "CS_sRGB" -> getInstance(ColorSpace.CS_sRGB); + case "CS_CIEXYZ" -> getInstance(ColorSpace.CS_CIEXYZ); + case "CS_PYCC" -> getInstance(ColorSpace.CS_PYCC); + case "CS_GRAY" -> getInstance(ColorSpace.CS_GRAY); + case "CS_LINEAR_RGB" -> getInstance(ColorSpace.CS_LINEAR_RGB); + case null, default -> getInstance(data); + }; + } catch (ClassCastException | IllegalArgumentException e) { + throw new InvalidObjectException("Invalid ICC Profile Data", e); } } diff --git a/src/java.desktop/share/classes/java/awt/font/NumericShaper.java b/src/java.desktop/share/classes/java/awt/font/NumericShaper.java index ae507036112..99b59cc2e0e 100644 --- a/src/java.desktop/share/classes/java/awt/font/NumericShaper.java +++ b/src/java.desktop/share/classes/java/awt/font/NumericShaper.java @@ -346,6 +346,19 @@ public final class NumericShaper implements java.io.Serializable { return index < NUM_KEYS ? Range.values()[index] : null; } + private static int toRangeHash(Set ranges) { + int m = 0; + for (Range range : ranges) { + int index = range.ordinal(); + if (index < NUM_KEYS) { + m |= 1 << index; + } else { + m |= (1 << NUM_KEYS) + index; + } + } + return m; + } + private static int toRangeMask(Set ranges) { int m = 0; for (Range range : ranges) { @@ -576,7 +589,7 @@ public final class NumericShaper implements java.io.Serializable { // and a linear probe is ok. private static int ctCache = 0; - private static int ctCacheLimit = contexts.length - 2; + private static final int ctCacheLimit = contexts.length - 2; // warning, synchronize access to this as it modifies state private static int getContextKey(char c) { @@ -1510,6 +1523,9 @@ public final class NumericShaper implements java.io.Serializable { private NumericShaper(int key, int mask) { this.key = key; this.mask = mask; + if (((this.mask & ARABIC) != 0) && ((this.mask & EASTERN_ARABIC) != 0)) { + this.mask &= ~ARABIC; + } } private NumericShaper(Range defaultContext, Set ranges) { @@ -1795,15 +1811,7 @@ public final class NumericShaper implements java.io.Serializable { * @see java.lang.Object#hashCode */ public int hashCode() { - int hash = mask; - if (rangeSet != null) { - // Use the CONTEXTUAL_MASK bit only for the enum-based - // NumericShaper. A deserialized NumericShaper might have - // bit masks. - hash &= CONTEXTUAL_MASK; - hash ^= rangeSet.hashCode(); - } - return hash; + return (rangeSet != null) ? Range.toRangeHash(rangeSet) : (mask & ~CONTEXTUAL_MASK); } /** diff --git a/src/java.desktop/share/classes/java/awt/font/TextLayout.java b/src/java.desktop/share/classes/java/awt/font/TextLayout.java index 74382b4bd83..5e8f3040055 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLayout.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -256,7 +256,6 @@ public final class TextLayout implements Cloneable { */ private boolean cacheIsValid = false; - // This value is obtained from an attribute, and constrained to the // interval [0,1]. If 0, the layout cannot be justified. private float justifyRatio; @@ -367,6 +366,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Font font, FontRenderContext frc) { @@ -378,8 +378,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null string passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } Map attributes = null; @@ -415,6 +415,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Map attributes, FontRenderContext frc) @@ -427,8 +428,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null map passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } char[] text = string.toCharArray(); @@ -499,6 +500,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(AttributedCharacterIterator text, FontRenderContext frc) { @@ -506,14 +508,13 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null iterator passed to TextLayout constructor."); } - int start = text.getBeginIndex(); - int limit = text.getEndIndex(); - if (start == limit) { - throw new IllegalArgumentException("Zero length iterator passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } + int start = text.getBeginIndex(); + int limit = text.getEndIndex(); int len = limit - start; - text.first(); char[] chars = new char[len]; int n = 0; for (char c = text.first(); @@ -1125,7 +1126,12 @@ public final class TextLayout implements Cloneable { float top1X, top2X; float bottom1X, bottom2X; - if (caret == 0 || caret == characterCount) { + if (caret == 0 && characterCount == 0) { + + top1X = top2X = 0; + bottom1X = bottom2X = 0; + + } else if (caret == 0 || caret == characterCount) { float pos; int logIndex; @@ -1143,8 +1149,8 @@ public final class TextLayout implements Cloneable { pos += angle * shift; top1X = top2X = pos + angle*textLine.getCharAscent(logIndex); bottom1X = bottom2X = pos - angle*textLine.getCharDescent(logIndex); - } - else { + + } else { { int logIndex = textLine.visualToLogical(caret-1); @@ -1884,7 +1890,6 @@ public final class TextLayout implements Cloneable { Shape[] result = new Shape[2]; TextHitInfo hit = TextHitInfo.afterOffset(offset); - int hitCaret = hitToCaret(hit); LayoutPathImpl lp = textLine.getLayoutPath(); @@ -2180,12 +2185,16 @@ public final class TextLayout implements Cloneable { checkTextHit(firstEndpoint); checkTextHit(secondEndpoint); - if(bounds == null) { - throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); + if (bounds == null) { + throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int firstCaret = hitToCaret(firstEndpoint); int secondCaret = hitToCaret(secondEndpoint); @@ -2194,8 +2203,9 @@ public final class TextLayout implements Cloneable { if (firstCaret == 0 || secondCaret == 0) { GeneralPath ls = leftShape(bounds); - if (!ls.getBounds().isEmpty()) + if (!ls.getBounds().isEmpty()) { result.append(ls, false); + } } if (firstCaret == characterCount || secondCaret == characterCount) { @@ -2282,12 +2292,16 @@ public final class TextLayout implements Cloneable { secondEndpoint = t; } - if(firstEndpoint < 0 || secondEndpoint > characterCount) { + if (firstEndpoint < 0 || secondEndpoint > characterCount) { throw new IllegalArgumentException("Range is invalid in TextLayout.getLogicalHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int[] carets = new int[10]; // would this ever not handle all cases? int count = 0; diff --git a/src/java.desktop/share/classes/java/awt/font/TextLine.java b/src/java.desktop/share/classes/java/awt/font/TextLine.java index 681fcd90083..3464c1626c6 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLine.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLine.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -836,7 +836,7 @@ final class TextLine { } if (result == null) { - result = new Rectangle2D.Float(Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + result = new Rectangle2D.Float(0, 0, 0, 0); } return result; @@ -844,6 +844,10 @@ final class TextLine { public Rectangle2D getItalicBounds() { + if (fComponents.length == 0) { + return new Rectangle2D.Float(0, 0, 0, 0); + } + float left = Float.MAX_VALUE, right = -Float.MAX_VALUE; float top = Float.MAX_VALUE, bottom = -Float.MAX_VALUE; @@ -927,7 +931,7 @@ final class TextLine { // dlf: get baseRot from font for now??? if (!requiresBidi) { - requiresBidi = Bidi.requiresBidi(chars, 0, chars.length); + requiresBidi = Bidi.requiresBidi(chars, 0, characterCount); } if (requiresBidi) { @@ -935,7 +939,7 @@ final class TextLine { ? Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT : values.getRunDirection(); - bidi = new Bidi(chars, 0, embs, 0, chars.length, bidiflags); + bidi = new Bidi(chars, 0, embs, 0, characterCount, bidiflags); if (!bidi.isLeftToRight()) { levels = BidiUtils.getLevels(bidi); int[] charsVtoL = BidiUtils.createVisualToLogicalMap(levels); @@ -945,13 +949,11 @@ final class TextLine { } Decoration decorator = Decoration.getDecoration(values); - int layoutFlags = 0; // no extra info yet, bidi determines run and line direction TextLabelFactory factory = new TextLabelFactory(frc, chars, bidi, layoutFlags); TextLineComponent[] components = new TextLineComponent[1]; - - components = createComponentsOnRun(0, chars.length, + components = createComponentsOnRun(0, characterCount, chars, charsLtoV, levels, factory, font, lm, @@ -972,7 +974,7 @@ final class TextLine { } return new TextLine(frc, components, lm.baselineOffsets, - chars, 0, chars.length, charsLtoV, levels, isDirectionLTR); + chars, 0, characterCount, charsLtoV, levels, isDirectionLTR); } private static TextLineComponent[] expandArray(TextLineComponent[] orig) { diff --git a/src/java.desktop/share/classes/java/awt/image/BandedSampleModel.java b/src/java.desktop/share/classes/java/awt/image/BandedSampleModel.java index bd955e35870..bad9abc6130 100644 --- a/src/java.desktop/share/classes/java/awt/image/BandedSampleModel.java +++ b/src/java.desktop/share/classes/java/awt/image/BandedSampleModel.java @@ -141,12 +141,9 @@ public final class BandedSampleModel extends ComponentSampleModel * @param h the height of the resulting {@code BandedSampleModel} * @return a new {@code BandedSampleModel} with the specified * width and height. - * @throws IllegalArgumentException if {@code w} or - * {@code h} equals either - * {@code Integer.MAX_VALUE} or - * {@code Integer.MIN_VALUE} - * @throws IllegalArgumentException if {@code dataType} is not - * one of the supported data types + * @throws IllegalArgumentException if the product of {@code w} + * and {@code h} is greater than {@code Integer.MAX_VALUE} + * or {@code w} or {@code h} is not greater than 0. */ public SampleModel createCompatibleSampleModel(int w, int h) { int[] bandOffs; @@ -172,8 +169,8 @@ public final class BandedSampleModel extends ComponentSampleModel * of the original BandedSampleModel/DataBuffer combination. * @throws RasterFormatException if the number of bands is greater than * the number of banks in this sample model. - * @throws IllegalArgumentException if {@code dataType} is not - * one of the supported data types + * @throws IllegalArgumentException if the number of bands is not greater than 0 + * @throws ArrayIndexOutOfBoundsException if any of the bank indices is out of bounds */ public SampleModel createSubsetSampleModel(int[] bands) { if (bands.length > bankIndices.length) diff --git a/src/java.desktop/share/classes/java/awt/image/LookupOp.java b/src/java.desktop/share/classes/java/awt/image/LookupOp.java index 5d11f78e76d..c8d8a703a3f 100644 --- a/src/java.desktop/share/classes/java/awt/image/LookupOp.java +++ b/src/java.desktop/share/classes/java/awt/image/LookupOp.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.desktop/share/classes/java/awt/image/renderable/ContextualRenderedImageFactory.java b/src/java.desktop/share/classes/java/awt/image/renderable/ContextualRenderedImageFactory.java index 94a2aa14bf5..8df3895a021 100644 --- a/src/java.desktop/share/classes/java/awt/image/renderable/ContextualRenderedImageFactory.java +++ b/src/java.desktop/share/classes/java/awt/image/renderable/ContextualRenderedImageFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2008, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -41,7 +41,7 @@ import java.awt.image.RenderedImage; * ContextualRenderedImageFactory provides an interface for the * functionality that may differ between instances of * RenderableImageOp. Thus different operations on RenderableImages - * may be performed by a single class such as RenderedImageOp through + * may be performed by a single class such as RenderableImageOp through * the use of multiple instances of ContextualRenderedImageFactory. * The name ContextualRenderedImageFactory is commonly shortened to * "CRIF." diff --git a/src/java.desktop/share/classes/java/awt/image/renderable/RenderableImageOp.java b/src/java.desktop/share/classes/java/awt/image/renderable/RenderableImageOp.java index 19bf3b39323..f2f0d458ba1 100644 --- a/src/java.desktop/share/classes/java/awt/image/renderable/RenderableImageOp.java +++ b/src/java.desktop/share/classes/java/awt/image/renderable/RenderableImageOp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -58,7 +58,7 @@ public class RenderableImageOp implements RenderableImage { /** - * Constructs a RenderedImageOp given a + * Constructs a {@code RenderableImageOp} given a * ContextualRenderedImageFactory object, and * a ParameterBlock containing RenderableImage sources and other * parameters. Any RenderedImage sources referenced by the diff --git a/src/java.desktop/share/classes/java/beans/Beans.java b/src/java.desktop/share/classes/java/beans/Beans.java index 313bfe98515..a95aeb45cbb 100644 --- a/src/java.desktop/share/classes/java/beans/Beans.java +++ b/src/java.desktop/share/classes/java/beans/Beans.java @@ -64,6 +64,22 @@ public class Beans { *

      * Instantiate a JavaBean. *

      + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

      + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

      + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

      + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * @param cls the class-loader from which we should create * the bean. If this is null, then the system @@ -84,6 +100,22 @@ public class Beans { *

      * Instantiate a JavaBean. *

      + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

      + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

      + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

      + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * * @param cls the class-loader from which we should create diff --git a/src/java.desktop/share/classes/javax/swing/JSplitPane.java b/src/java.desktop/share/classes/javax/swing/JSplitPane.java index f21b2b3339a..86a1a2495c7 100644 --- a/src/java.desktop/share/classes/javax/swing/JSplitPane.java +++ b/src/java.desktop/share/classes/javax/swing/JSplitPane.java @@ -371,24 +371,31 @@ public class JSplitPane extends JComponent implements Accessible public void setComponentOrientation(ComponentOrientation orientation) { ComponentOrientation curOrn = this.getComponentOrientation(); super.setComponentOrientation(orientation); + Component comp = null; if (!orientation.equals(curOrn)) { Component leftComponent = this.getLeftComponent(); Component rightComponent = this.getRightComponent(); if (!this.getComponentOrientation().isLeftToRight()) { if (rightComponent != null) { - setLeftComponent(rightComponent); - } - if (leftComponent != null) { - setRightComponent(leftComponent); + comp = this.leftComponent; + this.leftComponent = this.rightComponent; + this.rightComponent = comp; + } else if (leftComponent != null) { + comp = this.rightComponent; + this.rightComponent = this.leftComponent; + this.leftComponent = comp; } } else { if (leftComponent != null) { - setLeftComponent(leftComponent); + this.leftComponent = rightComponent; } if (rightComponent != null) { - setRightComponent(rightComponent); + this.rightComponent = leftComponent; } } + firePropertyChange(ORIENTATION_PROPERTY, curOrn, orientation); + this.revalidate(); + this.repaint(); } } diff --git a/src/java.desktop/share/classes/javax/swing/ScrollPaneLayout.java b/src/java.desktop/share/classes/javax/swing/ScrollPaneLayout.java index 0b8d8576f14..50cb25fe4f8 100644 --- a/src/java.desktop/share/classes/javax/swing/ScrollPaneLayout.java +++ b/src/java.desktop/share/classes/javax/swing/ScrollPaneLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -38,8 +38,8 @@ import java.io.Serializable; /** - * The layout manager used by JScrollPane. - * JScrollPaneLayout is + * The layout manager used by {@code JScrollPane}. + * {@code ScrollPaneLayout} is * responsible for nine components: a viewport, two scrollbars, * a row header, a column header, and four "corner" components. *

      diff --git a/src/java.desktop/share/classes/javax/swing/SwingUtilities.java b/src/java.desktop/share/classes/javax/swing/SwingUtilities.java index ebf39ac0283..4633e9c4756 100644 --- a/src/java.desktop/share/classes/javax/swing/SwingUtilities.java +++ b/src/java.desktop/share/classes/javax/swing/SwingUtilities.java @@ -49,10 +49,6 @@ import sun.awt.AWTAccessor.MouseEventAccessor; */ public class SwingUtilities implements SwingConstants { - // These states are system-wide, rather than AppContext wide. - private static boolean canAccessEventQueue = false; - private static boolean eventQueueTested = false; - /** * Indicates if we should change the drop target when a * {@code TransferHandler} is set. diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java index d361906b291..348d58bab21 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java @@ -682,7 +682,7 @@ public class BasicMenuItemUI extends MenuItemUI g.setFont(mi.getFont()); Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); - applyInsets(viewRect, mi.getInsets()); + SwingUtilities3.applyInsets(viewRect, mi.getInsets()); MenuItemLayoutHelper lh = new MenuItemLayoutHelper(mi, checkIcon, arrowIcon, viewRect, defaultTextIconGap, acceleratorDelimiter, @@ -741,10 +741,6 @@ public class BasicMenuItemUI extends MenuItemUI SwingUtilities3.paintArrowIcon(g, lh, lr, foreground); } - private void applyInsets(Rectangle rect, Insets insets) { - SwingUtilities3.applyInsets(rect, insets); - } - /** * Draws the background of the menu item. * diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java index e518f509c5a..6fab795e36c 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -377,6 +377,7 @@ public class BasicPopupMenuUI extends PopupMenuUI { } else if (item.isEnabled()) { // we have a menu item manager.clearSelectedPath(); + sun.awt.SunToolkit.consumeNextKeyTyped(e); item.doClick(); } e.consume(); diff --git a/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java b/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java index 41d90d31dd4..7b9a23aec5d 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -2291,8 +2291,11 @@ public class MetalLookAndFeel extends BasicLookAndFeel setUpdatePending(true); Runnable uiUpdater = new Runnable() { public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java index 95a9aed981a..0a0b25f9383 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, 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 @@ -41,6 +41,7 @@ import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicHTML; import javax.swing.text.View; +import com.sun.java.swing.SwingUtilities3; import sun.swing.MenuItemLayoutHelper; import sun.swing.SwingUtilities2; @@ -552,15 +553,6 @@ public class SynthGraphicsUtils { return result; } - static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { - if (insets != null) { - rect.x += (leftToRight ? insets.left : insets.right); - rect.y += insets.top; - rect.width -= (leftToRight ? insets.right : insets.left) + rect.x; - rect.height -= (insets.bottom + rect.y); - } - } - static void paint(SynthContext context, SynthContext accContext, Graphics g, Icon checkIcon, Icon arrowIcon, String acceleratorDelimiter, int defaultTextIconGap, String propertyPrefix) { @@ -570,7 +562,7 @@ public class SynthGraphicsUtils { Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); boolean leftToRight = SynthLookAndFeel.isLeftToRight(mi); - applyInsets(viewRect, mi.getInsets(), leftToRight); + SwingUtilities3.applyInsets(viewRect, mi.getInsets(), leftToRight); SynthMenuItemLayoutHelper lh = new SynthMenuItemLayoutHelper( context, accContext, mi, checkIcon, arrowIcon, viewRect, diff --git a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java index 6cb4803c65f..700e3ed8a38 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthLookAndFeel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, 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 @@ -909,8 +909,11 @@ public class SynthLookAndFeel extends BasicLookAndFeel { Runnable uiUpdater = new Runnable() { @Override public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/classes/javax/swing/text/GlyphView.java b/src/java.desktop/share/classes/javax/swing/text/GlyphView.java index 792d99fd7db..ef9d1bbe1ea 100644 --- a/src/java.desktop/share/classes/javax/swing/text/GlyphView.java +++ b/src/java.desktop/share/classes/javax/swing/text/GlyphView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, 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 @@ -391,7 +391,7 @@ public class GlyphView extends View implements TabableView, Cloneable { } if (bg != null) { g.setColor(bg); - g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); + g.fillRect(alloc.x, alloc.y, alloc.width, (int)painter.getHeight(this)); } if (c instanceof JTextComponent) { JTextComponent tc = (JTextComponent) c; diff --git a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java index d245f33b8af..7a41bd2e7f5 100644 --- a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java +++ b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -119,7 +119,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } @@ -145,7 +145,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java index 2ce0cd4672c..9d2c8dc2a72 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, 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 @@ -144,7 +144,9 @@ final class Curve { // finds points where the first and second derivative are // perpendicular. This happens when g(t) = f'(t)*f''(t) == 0 (where // * is a dot product). Unfortunately, we have to solve a cubic. - private int perpendiculardfddf(final double[] pts, final int off) { + private int perpendiculardfddf(final double[] pts, final int off, + final double A, final double B) + { assert pts.length >= off + 4; // these are the coefficients of some multiple of g(t) (not g(t), @@ -155,7 +157,7 @@ final class Curve { final double c = 2.0d * (dax * cx + day * cy) + dbx * dbx + dby * dby; final double d = dbx * cx + dby * cy; - return Helpers.cubicRootsInAB(a, b, c, d, pts, off, 0.0d, 1.0d); + return Helpers.cubicRootsInAB(a, b, c, d, pts, off, A, B); } // Tries to find the roots of the function ROC(t)-w in [0, 1). It uses @@ -171,35 +173,43 @@ final class Curve { // at most 4 sub-intervals of (0,1). ROC has asymptotes at inflection // points, so roc-w can have at least 6 roots. This shouldn't be a // problem for what we're trying to do (draw a nice looking curve). - int rootsOfROCMinusW(final double[] roots, final int off, final double w2, final double err) { + int rootsOfROCMinusW(final double[] roots, final int off, final double w2, + final double A, final double B) + { // no OOB exception, because by now off<=6, and roots.length >= 10 assert off <= 6 && roots.length >= 10; int ret = off; - final int end = off + perpendiculardfddf(roots, off); + final int end = off + perpendiculardfddf(roots, off, A, B); + Helpers.isort(roots, off, end); roots[end] = 1.0d; // always check interval end points - double t0 = 0.0d, ft0 = ROCsq(t0) - w2; + double t0 = 0.0d; + double ft0 = eliminateInf(ROCsq(t0) - w2); + double t1, ft1; for (int i = off; i <= end; i++) { - double t1 = roots[i], ft1 = ROCsq(t1) - w2; + t1 = roots[i]; + ft1 = eliminateInf(ROCsq(t1) - w2); if (ft0 == 0.0d) { roots[ret++] = t0; } else if (ft1 * ft0 < 0.0d) { // have opposite signs // (ROC(t)^2 == w^2) == (ROC(t) == w) is true because // ROC(t) >= 0 for all t. - roots[ret++] = falsePositionROCsqMinusX(t0, t1, w2, err); + roots[ret++] = falsePositionROCsqMinusX(t0, t1, ft0, ft1, w2, A); // A = err } t0 = t1; ft0 = ft1; } - return ret - off; } - private static double eliminateInf(final double x) { - return (x == Double.POSITIVE_INFINITY ? Double.MAX_VALUE : - (x == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : x)); + private final static double MAX_ROC_SQ = 1e20; + + private static double eliminateInf(final double x2) { + // limit the value of x to avoid numerical problems (smaller step): + // must handle NaN and +Infinity: + return (x2 <= MAX_ROC_SQ) ? x2 : MAX_ROC_SQ; } // A slight modification of the false position algorithm on wikipedia. @@ -210,17 +220,18 @@ final class Curve { // and turn out. Same goes for the newton's method // algorithm in Helpers.java private double falsePositionROCsqMinusX(final double t0, final double t1, + final double ft0, final double ft1, final double w2, final double err) { final int iterLimit = 100; int side = 0; - double t = t1, ft = eliminateInf(ROCsq(t) - w2); - double s = t0, fs = eliminateInf(ROCsq(s) - w2); + double s = t0, fs = eliminateInf(ft0); + double t = t1, ft = eliminateInf(ft1); double r = s, fr; - for (int i = 0; i < iterLimit && Math.abs(t - s) > err * Math.abs(t + s); i++) { + for (int i = 0; i < iterLimit && Math.abs(t - s) > err; i++) { r = (fs * t - ft * s) / (fs - ft); - fr = ROCsq(r) - w2; + fr = eliminateInf(ROCsq(r) - w2); if (sameSign(fr, ft)) { ft = fr; t = r; if (side < 0) { @@ -241,7 +252,7 @@ final class Curve { break; } } - return r; + return (Math.abs(ft) <= Math.abs(fs)) ? t : s; } private static boolean sameSign(final double x, final double y) { @@ -256,9 +267,9 @@ final class Curve { final double dy = t * (t * day + dby) + cy; final double ddx = 2.0d * dax * t + dbx; final double ddy = 2.0d * day * t + dby; - final double dx2dy2 = dx * dx + dy * dy; - final double ddx2ddy2 = ddx * ddx + ddy * ddy; - final double ddxdxddydy = ddx * dx + ddy * dy; - return dx2dy2 * ((dx2dy2 * dx2dy2) / (dx2dy2 * ddx2ddy2 - ddxdxddydy * ddxdxddydy)); + final double dx2dy2 = dx * dx + dy * dy; // positive + final double dxddyddxdy = dx * ddy - dy * ddx; + // may return +Infinity if dxddyddxdy = 0 or NaN if 0/0: + return (dx2dy2 * dx2dy2 * dx2dy2) / (dxddyddxdy * dxddyddxdy); // both positive } } diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java index 66eb9334e86..f829872a8a8 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java @@ -564,7 +564,7 @@ public final class DMarlinRenderingEngine extends RenderingEngine } private static boolean nearZero(final double num) { - return Math.abs(num) < 2.0d * Math.ulp(num); + return Math.abs(num) < 2.0d * Helpers.ulp(num); } abstract static class NormalizingPathIterator implements PathIterator { diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java index 0aed05ab506..926533cdb2b 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, 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 @@ -31,12 +31,19 @@ import sun.java2d.marlin.stats.StatLong; final class Helpers implements MarlinConst { + private final static double T_ERR = 1e-4; + private final static double T_A = T_ERR; + private final static double T_B = 1.0 - T_ERR; + private static final double EPS = 1e-9d; private Helpers() { throw new Error("This is a non instantiable class"); } + /** use lower precision like former Pisces and Marlin (float-precision) */ + static double ulp(final double value) { return Math.ulp((float)value); } + static boolean within(final double x, final double y) { return within(x, y, EPS); } @@ -322,10 +329,10 @@ final class Helpers implements MarlinConst { // now we must subdivide at points where one of the offset curves will have // a cusp. This happens at ts where the radius of curvature is equal to w. - ret += c.rootsOfROCMinusW(ts, ret, w2, 0.0001d); + ret += c.rootsOfROCMinusW(ts, ret, w2, T_A, T_B); - ret = filterOutNotInAB(ts, 0, ret, 0.0001d, 0.9999d); - isort(ts, ret); + ret = filterOutNotInAB(ts, 0, ret, T_A, T_B); + isort(ts, 0, ret); return ret; } @@ -354,7 +361,7 @@ final class Helpers implements MarlinConst { if ((outCodeOR & OUTCODE_BOTTOM) != 0) { ret += curve.yPoints(ts, ret, clipRect[1]); } - isort(ts, ret); + isort(ts, 0, ret); return ret; } @@ -374,11 +381,11 @@ final class Helpers implements MarlinConst { } } - static void isort(final double[] a, final int len) { - for (int i = 1, j; i < len; i++) { + static void isort(final double[] a, final int off, final int len) { + for (int i = off + 1, j; i < len; i++) { final double ai = a[i]; j = i - 1; - for (; j >= 0 && a[j] > ai; j--) { + for (; j >= off && a[j] > ai; j--) { a[j + 1] = a[j]; } a[j + 1] = ai; diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java index 59f93ed7d6d..1c257bc13d9 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, 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 @@ -886,8 +886,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, // in which case ignore if p1 == p2 - final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Math.ulp(y2)); - final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Math.ulp(y4)); + final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Helpers.ulp(y2)); + final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Helpers.ulp(y4)); if (p1eqp2 && p3eqp4) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); @@ -905,7 +905,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx1 * dx1 + dy1 * dy1; final double l4sq = dx4 * dx4 + dy4 * dy4; - if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); } @@ -1078,8 +1078,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // equal if they're very close to each other. // if p1 == p2 or p2 == p3: draw line from p1->p3 - final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Math.ulp(y2)); - final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Math.ulp(y3)); + final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Helpers.ulp(y2)); + final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Helpers.ulp(y3)); if (p1eqp2 || p2eqp3) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); @@ -1091,7 +1091,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx12 * dx12 + dy12 * dy12; final double l3sq = dx23 * dx23 + dy23 * dy23; - if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); } diff --git a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java index b657e434d2f..949a914990b 100644 --- a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java +++ b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, 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 @@ -73,8 +73,8 @@ public class OutlineTextRenderer implements TextPipe { public void drawString(SunGraphics2D g2d, String str, double x, double y) { - if ("".equals(str)) { - return; // TextLayout constructor throws IAE on "". + if (str.length() == 0) { + return; } TextLayout tl = new TextLayout(str, g2d.getFont(), g2d.getFontRenderContext()); diff --git a/src/java.desktop/share/classes/sun/print/PrintJob2D.java b/src/java.desktop/share/classes/sun/print/PrintJob2D.java index 8128dae1057..ee0caaa54fe 100644 --- a/src/java.desktop/share/classes/sun/print/PrintJob2D.java +++ b/src/java.desktop/share/classes/sun/print/PrintJob2D.java @@ -79,7 +79,12 @@ public class PrintJob2D extends PrintJob { * needs to implement PrintGraphics, so we wrap * the Graphics2D instance. */ - return new ProxyPrintGraphics(printJobDelegate.getGraphics(), this); + Graphics g = printJobDelegate.getGraphics(); + if (g == null) { // PrintJob.end() has been called. + return null; + } else { + return new ProxyPrintGraphics(g, this); + } } /** diff --git a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java index 2a6d45e2ba8..754af87b94e 100644 --- a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java +++ b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java @@ -2470,13 +2470,16 @@ public abstract class RasterPrinterJob extends PrinterJob { g.setPaint(Color.black); } - /* On-screen drawString renders most control chars as the missing glyph - * and have the non-zero advance of that glyph. - * Exceptions are \t, \n and \r which are considered zero-width. - * This is a utility method used by subclasses to remove them so we - * don't have to worry about platform or font specific handling of them. + /** + * Removes ignorable whitespace from the specified text, so that there + * is no need for platform-specific or font-specific custom whitespace + * handling, and so that these characters are not treated like control + * characters which are printed as the missing glyph. + * + * @param s the text to process + * @return the input text, with ignorable whitespace (if any) removed */ - protected String removeControlChars(String s) { + public static String removeControlChars(String s) { char[] in_chars = s.toCharArray(); int len = in_chars.length; char[] out_chars = new char[len]; diff --git a/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java b/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java index 86d240a91fc..8af75f98b83 100644 --- a/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java +++ b/src/java.desktop/share/classes/sun/swing/plaf/DesktopProperty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, 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 @@ -214,8 +214,11 @@ public class DesktopProperty implements UIDefaults.ActiveValue { setUpdatePending(true); Runnable uiUpdater = new Runnable() { public void run() { - updateAllUIs(); - setUpdatePending(false); + try { + updateAllUIs(); + } finally { + setUpdatePending(false); + } } }; SwingUtilities.invokeLater(uiUpdater); diff --git a/src/java.desktop/share/conf/psfontj2d.properties b/src/java.desktop/share/conf/psfontj2d.properties index 9efe8864428..8030a82bc4f 100644 --- a/src/java.desktop/share/conf/psfontj2d.properties +++ b/src/java.desktop/share/conf/psfontj2d.properties @@ -59,7 +59,6 @@ avantgarde_book_oblique=avantgarde_book_oblique avantgarde_demi_oblique=avantgarde_demi_oblique # itcavantgarde=avantgarde_book -itcavantgarde=avantgarde_book itcavantgarde_demi=avantgarde_demi itcavantgarde_oblique=avantgarde_book_oblique itcavantgarde_demi_oblique=avantgarde_demi_oblique diff --git a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c index cbdad61f78e..4f2cfca8dd0 100644 --- a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c +++ b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c @@ -279,7 +279,9 @@ SplashDecodeGif(Splash * splash, GifFileType * gif) ImageRect dstRect; rgbquad_t fillColor = 0; // 0 is transparent - if (transparentColor < 0) { + if (colorMap && + colorMap->Colors && + transparentColor < 0) { fillColor= MAKE_QUAD_GIF( colorMap->Colors[gif->SBackGroundColor], 0xff); } diff --git a/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java b/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java index d96894660b9..f9445a78ede 100644 --- a/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java +++ b/src/java.desktop/unix/classes/sun/print/PrintServiceLookupProvider.java @@ -876,12 +876,16 @@ public final class PrintServiceLookupProvider extends PrintServiceLookup FileReader reader = new FileReader(f); bufferedReader = new BufferedReader(reader); String line; + results = new ArrayList<>(); while ((line = bufferedReader.readLine()) != null) { results.add(line); } } - } finally { + } catch (Exception e) { + // Print exception for tracking printer command errors + IPPPrintService.debug_println("Printer command error: " + e); + } finally { f.delete(); // promptly close all streams. if (bufferedReader != null) { diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c index 1f73a3256b0..5985dd93226 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c @@ -1268,11 +1268,15 @@ Java_sun_awt_X11GraphicsDevice_pGetBounds(JNIEnv *env, jobject this, jint screen xinInfo[screen].width, xinInfo[screen].height); XFree(xinInfo); + if (!bounds) { + return NULL; + } } } else { jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); if (exceptionClass != NULL) { (*env)->ThrowNew(env, exceptionClass, "Illegal screen index"); + return NULL; } } } diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c index 916880873c6..e5b2dfa6db9 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c @@ -276,10 +276,7 @@ GtkApi* gtk3_load(JNIEnv *env, const char* lib_name) fp_gtk_check_version = dl_symbol("gtk_check_version"); /* GLib */ - fp_glib_check_version = dlsym(gtk3_libhandle, "glib_check_version"); - if (!fp_glib_check_version) { - dlerror(); - } + fp_glib_check_version = dl_symbol("glib_check_version"); fp_g_free = dl_symbol("g_free"); fp_g_object_unref = dl_symbol("g_object_unref"); diff --git a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsCheckBoxMenuItemUI.java b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsCheckBoxMenuItemUI.java index f28ae2a9326..3a2578b3e0b 100644 --- a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsCheckBoxMenuItemUI.java +++ b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsCheckBoxMenuItemUI.java @@ -76,19 +76,20 @@ public final class WindowsCheckBoxMenuItemUI extends BasicCheckBoxMenuItemUI { super.paintBackground(g, menuItem, bgColor); } - /** - * Paint MenuItem. - */ + @Override protected void paintMenuItem(Graphics g, JComponent c, Icon checkIcon, Icon arrowIcon, Color background, Color foreground, int defaultTextIconGap) { if (WindowsMenuItemUI.isVistaPainting()) { - WindowsMenuItemUI.paintMenuItem(accessor, g, c, checkIcon, - arrowIcon, background, foreground, - disabledForeground, acceleratorSelectionForeground, - acceleratorForeground, defaultTextIconGap, - menuItem, getPropertyPrefix()); + WindowsMenuItemUI.paintMenuItem(accessor, g, c, + checkIcon, arrowIcon, + background, foreground, + disabledForeground, + acceleratorSelectionForeground, + acceleratorForeground, + defaultTextIconGap, + menuItem, getPropertyPrefix()); return; } super.paintMenuItem(g, c, checkIcon, arrowIcon, background, diff --git a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuItemUI.java b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuItemUI.java index a9b09085ad1..041bdb5adaa 100644 --- a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuItemUI.java +++ b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuItemUI.java @@ -29,16 +29,11 @@ import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; -import java.awt.Insets; import java.awt.Rectangle; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.util.Enumeration; -import javax.swing.AbstractButton; -import javax.swing.ButtonGroup; import javax.swing.ButtonModel; -import javax.swing.DefaultButtonModel; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JMenu; @@ -132,27 +127,6 @@ public final class WindowsMenuItemUI extends BasicMenuItemUI { menuItem.addPropertyChangeListener(changeListener); } - protected void installDefaults() { - super.installDefaults(); - String prefix = getPropertyPrefix(); - - if (acceleratorSelectionForeground == null || - acceleratorSelectionForeground instanceof UIResource) { - acceleratorSelectionForeground = - UIManager.getColor(prefix + ".acceleratorSelectionForeground"); - } - if (acceleratorForeground == null || - acceleratorForeground instanceof UIResource) { - acceleratorForeground = - UIManager.getColor(prefix + ".acceleratorForeground"); - } - if (disabledForeground == null || - disabledForeground instanceof UIResource) { - disabledForeground = - UIManager.getColor(prefix + ".disabledForeground"); - } - } - /** * {@inheritDoc} */ @@ -165,15 +139,19 @@ public final class WindowsMenuItemUI extends BasicMenuItemUI { changeListener = null; } + @Override protected void paintMenuItem(Graphics g, JComponent c, Icon checkIcon, Icon arrowIcon, Color background, Color foreground, int defaultTextIconGap) { if (WindowsMenuItemUI.isVistaPainting()) { - WindowsMenuItemUI.paintMenuItem(accessor, g, c, checkIcon, - arrowIcon, background, foreground, - disabledForeground, acceleratorSelectionForeground, - acceleratorForeground, defaultTextIconGap, menuItem, + WindowsMenuItemUI.paintMenuItem(accessor, g, c, + checkIcon, arrowIcon, + background, foreground, + disabledForeground, + acceleratorSelectionForeground, + acceleratorForeground, + defaultTextIconGap, menuItem, getPropertyPrefix()); return; } @@ -182,12 +160,16 @@ public final class WindowsMenuItemUI extends BasicMenuItemUI { } static void paintMenuItem(WindowsMenuItemUIAccessor accessor, Graphics g, - JComponent c, Icon checkIcon, Icon arrowIcon, + JComponent c, + Icon checkIcon, Icon arrowIcon, Color background, Color foreground, Color disabledForeground, Color acceleratorSelectionForeground, Color acceleratorForeground, - int defaultTextIconGap, JMenuItem menuItem, String prefix) { + int defaultTextIconGap, JMenuItem menuItem, + String prefix) { + assert c == menuItem : "menuItem passed as 'c' must be the same"; + // Save original graphics font and color Font holdf = g.getFont(); Color holdc = g.getColor(); diff --git a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuUI.java b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuUI.java index 754b394d4ac..130b09227cc 100644 --- a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuUI.java +++ b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsMenuUI.java @@ -131,18 +131,20 @@ public final class WindowsMenuUI extends BasicMenuUI { hotTrackingOn = (obj instanceof Boolean) ? (Boolean)obj : true; } - /** - * Paint MenuItem. - */ + @Override protected void paintMenuItem(Graphics g, JComponent c, - Icon checkIcon, Icon arrowIcon, - Color background, Color foreground, - int defaultTextIconGap) { + Icon checkIcon, Icon arrowIcon, + Color background, Color foreground, + int defaultTextIconGap) { + assert c == menuItem : "menuItem passed as 'c' must be the same"; if (WindowsMenuItemUI.isVistaPainting()) { - WindowsMenuItemUI.paintMenuItem(accessor, g, c, checkIcon, arrowIcon, + WindowsMenuItemUI.paintMenuItem(accessor, g, c, + checkIcon, arrowIcon, background, foreground, - disabledForeground, acceleratorSelectionForeground, - acceleratorForeground, defaultTextIconGap, menuItem, + disabledForeground, + acceleratorSelectionForeground, + acceleratorForeground, + defaultTextIconGap, menuItem, getPropertyPrefix()); return; } diff --git a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsRadioButtonMenuItemUI.java b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsRadioButtonMenuItemUI.java index 06ef5db23a1..78768c29ab3 100644 --- a/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsRadioButtonMenuItemUI.java +++ b/src/java.desktop/windows/classes/com/sun/java/swing/plaf/windows/WindowsRadioButtonMenuItemUI.java @@ -76,19 +76,20 @@ public final class WindowsRadioButtonMenuItemUI extends BasicRadioButtonMenuItem super.paintBackground(g, menuItem, bgColor); } - /** - * Paint MenuItem. - */ + @Override protected void paintMenuItem(Graphics g, JComponent c, Icon checkIcon, Icon arrowIcon, Color background, Color foreground, int defaultTextIconGap) { if (WindowsMenuItemUI.isVistaPainting()) { - WindowsMenuItemUI.paintMenuItem(accessor, g, c, checkIcon, - arrowIcon, background, foreground, - disabledForeground, acceleratorSelectionForeground, - acceleratorForeground, defaultTextIconGap, - menuItem, getPropertyPrefix()); + WindowsMenuItemUI.paintMenuItem(accessor, g, c, + checkIcon, arrowIcon, + background, foreground, + disabledForeground, + acceleratorSelectionForeground, + acceleratorForeground, + defaultTextIconGap, + menuItem, getPropertyPrefix()); return; } super.paintMenuItem(g, c, checkIcon, arrowIcon, background, diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java index 75e8fa42da0..87b1591c0eb 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, 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 @@ -73,6 +73,7 @@ import sun.font.TrueTypeFont; import sun.print.PathGraphics; import sun.print.ProxyGraphics2D; +import sun.print.RasterPrinterJob; final class WPathGraphics extends PathGraphics { @@ -847,7 +848,7 @@ final class WPathGraphics extends PathGraphics { * removed now so the string and positions are the same length. * For other cases we need to pass glyph codes to GDI. */ - str = wPrinterJob.removeControlChars(str); + str = RasterPrinterJob.removeControlChars(str); char[] chars = str.toCharArray(); int len = chars.length; GlyphVector gv = null; diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java index e50cfcff33b..17d41036bcc 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -1202,14 +1202,6 @@ public final class WPrinterJob extends RasterPrinterJob } } - /** - * Remove control characters. - */ - @Override - protected String removeControlChars(String str) { - return super.removeControlChars(str); - } - /** * Draw the string {@code text} to the printer's * device context at the specified position. diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java index 59afff013c7..889ea56531e 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpClient.java +++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java @@ -28,9 +28,11 @@ package java.net.http; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; +import java.net.URI; import java.nio.channels.Selector; import java.net.Authenticator; import java.net.CookieHandler; @@ -59,7 +61,7 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * The {@link #newBuilder() newBuilder} method returns a builder that creates * instances of the default {@code HttpClient} implementation. * The builder can be used to configure per-client state, like: the preferred - * protocol version ( HTTP/1.1 or HTTP/2 ), whether to follow redirects, a + * protocol version ( HTTP/1.1, HTTP/2 or HTTP/3 ), whether to follow redirects, a * proxy, an authenticator, etc. Once built, an {@code HttpClient} is immutable, * and can be used to send multiple requests. * @@ -162,6 +164,59 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * prevent the resources allocated by the associated client from * being reclaimed by the garbage collector. * + *

      + * The default implementation of the {@code HttpClient} supports HTTP/1.1, + * HTTP/2, and HTTP/3. Which version of the protocol is actually used when sending + * a request can depend on multiple factors. In the case of HTTP/2, it may depend + * on an initial upgrade to succeed (when using a plain connection), or on HTTP/2 + * being successfully negotiated during the Transport Layer Security (TLS) handshake. + * + *

      If {@linkplain Version#HTTP_2 HTTP/2} is selected over a clear + * connection, and no HTTP/2 connection to the + * origin server + * already exists, the client will create a new connection and attempt an upgrade + * from HTTP/1.1 to HTTP/2. + * If the upgrade succeeds, then the response to this request will use HTTP/2. + * If the upgrade fails, then the response will be handled using HTTP/1.1. + * + *

      Other constraints may also affect the selection of protocol version. + * For example, if HTTP/2 is requested through a proxy, and if the implementation + * does not support this mode, then HTTP/1.1 may be used. + *

      + * The HTTP/3 protocol is not selected by default, but can be enabled by setting + * the {@linkplain Builder#version(Version) HttpClient preferred version} or the + * {@linkplain HttpRequest.Builder#version(Version) HttpRequest preferred version} to + * {@linkplain Version#HTTP_3 HTTP/3}. Like for HTTP/2, which protocol version is + * actually used when HTTP/3 is enabled may depend on several factors. + * {@linkplain HttpOption#H3_DISCOVERY Configuration hints} can + * be {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) provided} + * to help the {@code HttpClient} implementation decide how to establish + * and carry out the HTTP exchange when the HTTP/3 protocol is enabled. + * If no configuration hints are provided, the {@code HttpClient} will select + * one as explained in the {@link HttpOption#H3_DISCOVERY H3_DISCOVERY} + * option API documentation. + *
      Note that a request whose {@linkplain URI#getScheme() URI scheme} is not + * {@code "https"} will never be sent over HTTP/3. In this implementation, + * HTTP/3 is not used if a proxy is selected. + * + *

      + * If a concrete instance of {@link HttpClient} doesn't support sending a + * request through HTTP/3, an {@link UnsupportedProtocolVersionException} may be + * thrown, either when {@linkplain Builder#build() building} the client with + * a {@linkplain Builder#version(Version) preferred version} set to HTTP/3, + * or when attempting to send a request with {@linkplain HttpRequest.Builder#version(Version) + * HTTP/3 enabled} when {@link Http3DiscoveryMode#HTTP_3_URI_ONLY HTTP_3_URI_ONLY} + * was {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) specified}. + * This may typically happen if the {@link #sslContext() SSLContext} + * or {@link #sslParameters() SSLParameters} configured on the client instance cannot + * be used with HTTP/3. + * + * @see UnsupportedProtocolVersionException + * @see Builder#version(Version) + * @see HttpRequest.Builder#version(Version) + * @see HttpRequest.Builder#setOption(HttpOption, Object) + * @see HttpOption#H3_DISCOVERY + * * @since 11 */ public abstract class HttpClient implements AutoCloseable { @@ -320,23 +375,19 @@ public abstract class HttpClient implements AutoCloseable { public Builder followRedirects(Redirect policy); /** - * Requests a specific HTTP protocol version where possible. + * Sets the default preferred HTTP protocol version for requests + * issued by this client. * *

      If this method is not invoked prior to {@linkplain #build() * building}, then newly built clients will prefer {@linkplain * Version#HTTP_2 HTTP/2}. * - *

      If set to {@linkplain Version#HTTP_2 HTTP/2}, then each request - * will attempt to upgrade to HTTP/2. If the upgrade succeeds, then the - * response to this request will use HTTP/2 and all subsequent requests - * and responses to the same - * origin server - * will use HTTP/2. If the upgrade fails, then the response will be - * handled using HTTP/1.1 + *

      If a request doesn't have a preferred version, then + * the effective preferred version for the request is the + * client's preferred version.

      * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the implementation - * does not support this mode, then HTTP/1.1 may be used + * @implNote Some constraints may also affect the {@linkplain + * HttpClient##ProtocolVersionSelection selection of the actual protocol version}. * * @param version the requested HTTP protocol version * @return this builder @@ -439,9 +490,14 @@ public abstract class HttpClient implements AutoCloseable { * @return a new {@code HttpClient} * * @throws UncheckedIOException may be thrown if underlying IO resources required - * by the implementation cannot be allocated. For instance, + * by the implementation cannot be allocated, or if the resulting configuration + * does not satisfy the implementation requirements. For instance, * if the implementation requires a {@link Selector}, and opening - * one fails due to {@linkplain Selector#open() lack of necessary resources}. + * one fails due to {@linkplain Selector#open() lack of necessary resources}, + * or if the {@linkplain #version(Version) preferred protocol version} is not + * {@linkplain HttpClient##UnsupportedProtocolVersion supported by + * the implementation or cannot be used in this configuration}. + * */ public HttpClient build(); } @@ -525,9 +581,11 @@ public abstract class HttpClient implements AutoCloseable { * Returns the preferred HTTP protocol version for this client. The default * value is {@link HttpClient.Version#HTTP_2} * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the - * implementation does not support this mode, then HTTP/1.1 may be used + * @implNote + * The protocol version that the {@code HttpClient} eventually + * decides to use for a request is affected by various factors noted + * in {@linkplain ##ProtocolVersionSelection protocol version selection} + * section. * * @return the HTTP protocol version requested */ @@ -562,7 +620,13 @@ public abstract class HttpClient implements AutoCloseable { /** * HTTP version 2 */ - HTTP_2 + HTTP_2, + + /** + * HTTP version 3 + * @since 26 + */ + HTTP_3 } /** @@ -869,7 +933,7 @@ public abstract class HttpClient implements AutoCloseable { *

      If interrupted while waiting, this method may attempt to stop all * operations by calling {@link #shutdownNow()}. It then continues to wait * until all actively executing operations have completed. - * The interrupt status will be re-asserted before this method returns. + * The interrupted status will be re-asserted before this method returns. * *

      If already terminated, invoking this method has no effect. * diff --git a/src/java.net.http/share/classes/java/net/http/HttpOption.java b/src/java.net.http/share/classes/java/net/http/HttpOption.java new file mode 100644 index 00000000000..cbff11f71ee --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/HttpOption.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package java.net.http; + +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest.Builder; + +/** + * This interface is used to provide additional request configuration + * option hints on how an HTTP request/response exchange should + * be carried out by the {@link HttpClient} implementation. + * Request configuration option hints can be provided to an + * {@link HttpRequest} with the {@link + * Builder#setOption(HttpOption, Object) HttpRequest.Builder + * setOption} method. + * + *

      Concrete instances of this class and its subclasses are immutable. + * + * @apiNote + * In this version, the {@code HttpOption} interface is sealed and + * only allows the {@link #H3_DISCOVERY} option. However, it could be + * extended in the future to support additional options. + *

      + * The {@link #H3_DISCOVERY} option can be used to help the + * {@link HttpClient} decide how to select or establish an + * HTTP/3 connection through which to carry out an HTTP/3 + * request/response exchange. + * + * @param The {@linkplain #type() type of the option value} + * + * @since 26 + */ +public sealed interface HttpOption permits HttpRequestOptionImpl { + /** + * {@return the option name} + * + * @implSpec Different options must have different names. + */ + String name(); + + /** + * {@return the type of the value associated with the option} + * + * @apiNote Different options may have the same type. + */ + Class type(); + + /** + * An option that can be used to configure how the {@link HttpClient} will + * select or establish an HTTP/3 connection through which to carry out + * the request. If {@link Version#HTTP_3} is not selected either as + * the {@linkplain Builder#version(Version) request preferred version} + * or the {@linkplain HttpClient.Builder#version(Version) HttpClient + * preferred version} setting this option on the request has no effect. + *

      + * The {@linkplain #name() name of this option} is {@code "H3_DISCOVERY"}. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link #H3_DISCOVERY} hint. + *
      + * If no {@code H3_DISCOVERY} hint is provided, and the {@linkplain Version#HTTP_3 + * HTTP/3 version} is selected, either as {@linkplain Builder#version(Version) + * request preferred version} or {@linkplain HttpClient.Builder#version(Version) + * client preferred version}, the JDK built-in implementation will establish + * the exchange as per {@link Http3DiscoveryMode#ANY}. + *

      + * In case of {@linkplain HttpClient.Redirect redirect}, the + * {@link #H3_DISCOVERY} option, if present, is always transferred to + * the new request. + *

      + * In this implementation, HTTP/3 through proxies is not supported. + * Unless {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, if + * a {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy} is {@linkplain + * ProxySelector#select(URI) selected} for the {@linkplain HttpRequest#uri() + * request URI}, the protocol version is downgraded to HTTP/2 or + * HTTP/1.1 and the {@link #H3_DISCOVERY} option is ignored. If, on the + * other hand, {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, + * the request will fail. + * + * @see Http3DiscoveryMode + * @see Builder#setOption(HttpOption, Object) + */ + HttpOption H3_DISCOVERY = + new HttpRequestOptionImpl<>(Http3DiscoveryMode.class, "H3_DISCOVERY"); + + /** + * This enumeration can be used to help the {@link HttpClient} decide + * how an HTTP/3 exchange should be established, and can be provided + * as the value of the {@link HttpOption#H3_DISCOVERY} option + * to {@link Builder#setOption(HttpOption, Object) Builder.setOption}. + *

      + * Note that if neither the {@linkplain Builder#version(Version) request preferred + * version} nor the {@linkplain HttpClient.Builder#version(Version) client preferred + * version} is {@linkplain Version#HTTP_3 HTTP/3}, no HTTP/3 exchange will + * be established and the {@code Http3DiscoveryMode} is ignored. + * + * @since 26 + */ + enum Http3DiscoveryMode { + /** + * This instructs the {@link HttpClient} to use its own implementation + * specific algorithm to find or establish a connection for the exchange. + * Typically, if no connection was previously established with the origin + * server defined by the request URI, the {@link HttpClient} implementation + * may attempt to establish both an HTTP/3 connection over QUIC and an HTTP + * connection over TLS/TCP at the authority present in the request URI, + * and use the first that succeeds. The exchange may then be carried out with + * any of the {@linkplain Version + * three HTTP protocol versions}, depending on which method succeeded first. + * + * @implNote + * If the {@linkplain Builder#version(Version) request preferred version} is {@linkplain + * Version#HTTP_3 HTTP/3}, the {@code HttpClient} may give priority to HTTP/3 by + * attempting to establish an HTTP/3 connection, before attempting a TLS + * connection over TCP. + *

      + * When attempting an HTTP/3 connection in this mode, the {@code HttpClient} may + * use any HTTP Alternative Services + * information it may have previously obtained from the origin server. If no + * such information is available, a direct HTTP/3 connection at the authority (host, port) + * present in the {@linkplain HttpRequest#uri() request URI} will be attempted. + */ + ANY, + /** + * This instructs the {@link HttpClient} to only use the + * HTTP Alternative Services + * to find or establish an HTTP/3 connection with the origin server. + * The exchange may then be carried out with any of the {@linkplain + * Version three HTTP protocol versions}, depending on + * whether an Alternate Service record for HTTP/3 could be found, and which HTTP version + * was negotiated with the origin server, if no such record could be found. + *

      + * In this mode, requests sent to the origin server will be sent through HTTP/1.1 or HTTP/2 + * until a {@code h3} HTTP Alternative Services + * endpoint for that server is advertised to the client. Usually, an alternate service is + * advertised by a server when responding to a request, so that subsequent requests can make + * use of that alternative service. + */ + ALT_SVC, + /** + * This instructs the {@link HttpClient} to only attempt an HTTP/3 connection + * with the origin server. The connection will only succeed if the origin server + * is listening for incoming HTTP/3 connections over QUIC at the same authority (host, port) + * as defined in the {@linkplain HttpRequest#uri() request URI}. In this mode, + * HTTP Alternative Services + * are not used. + */ + HTTP_3_URI_ONLY + } + +} diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequest.java b/src/java.net.http/share/classes/java/net/http/HttpRequest.java index 84a521336b6..c56328ba4b4 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java +++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java @@ -29,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.http.HttpClient.Version; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; @@ -91,6 +92,24 @@ public abstract class HttpRequest { */ protected HttpRequest() {} + /** + * {@return the value configured on this request for the given option, if any} + * @param option a request configuration option + * @param the type of the option + * + * @see Builder#setOption(HttpOption, Object) + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + * if {@code option} is non-null, otherwise throws {@link NullPointerException}. + * + * @since 26 + */ + public Optional getOption(HttpOption option) { + Objects.requireNonNull(option); + return Optional.empty(); + } + /** * A builder of {@linkplain HttpRequest HTTP requests}. * @@ -144,14 +163,53 @@ public abstract class HttpRequest { * *

      The corresponding {@link HttpResponse} should be checked for the * version that was actually used. If the version is not set in a - * request, then the version requested will be that of the sending - * {@link HttpClient}. + * request, then the version requested will be {@linkplain + * HttpClient.Builder#version(Version) that of the sending + * {@code HttpClient}}. + * + * @implNote + * Constraints may also affect the {@linkplain HttpClient##ProtocolVersionSelection + * selection of the actual protocol version}. * * @param version the HTTP protocol version requested * @return this builder */ public Builder version(HttpClient.Version version); + /** + * Provides request configuration option hints modeled as key value pairs + * to help an {@link HttpClient} implementation decide how the + * request/response exchange should be established or carried out. + * + *

      An {@link HttpClient} implementation may decide to ignore request + * configuration option hints, or fail the request, if provided with any + * option hints that it does not understand. + *

      + * If this method is invoked twice for the same {@linkplain HttpOption + * request option}, any value previously provided to this builder for the + * corresponding option is replaced by the new value. + * If {@code null} is supplied as a value, any value previously + * provided is discarded. + * + * @implSpec + * The default implementation of this method discards the provided option + * hint and does nothing. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link HttpOption#H3_DISCOVERY} hint. + * + * @param option the request configuration option + * @param value the request configuration option value (can be null) + * + * @return this builder + * + * @see HttpRequest#getOption(HttpOption) + * + * @since 26 + */ + public default Builder setOption(HttpOption option, T value) { return this; } + /** * Adds the given name value pair to the set of headers for this request. * The given value is added to the list of values for that name. @@ -394,6 +452,8 @@ public abstract class HttpRequest { } } ); + request.getOption(HttpOption.H3_DISCOVERY) + .ifPresent(opt -> builder.setOption(HttpOption.H3_DISCOVERY, opt)); return builder; } diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java new file mode 100644 index 00000000000..f5562c7068b --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package java.net.http; + +// Package private implementation of HttpRequest options +record HttpRequestOptionImpl(Class type, String name) + implements HttpOption { + @Override + public String toString() { + return name(); + } +} diff --git a/src/java.net.http/share/classes/java/net/http/HttpResponse.java b/src/java.net.http/share/classes/java/net/http/HttpResponse.java index 52f5298452a..1889d9d7300 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java +++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java @@ -803,24 +803,66 @@ public interface HttpResponse { /** * A handler for push promises. * - *

      A push promise is a synthetic request sent by an HTTP/2 server + *

      A push promise is a synthetic request sent by an HTTP/2 or HTTP/3 server * when retrieving an initiating client-sent request. The server has * determined, possibly through inspection of the initiating request, that * the client will likely need the promised resource, and hence pushes a * synthetic push request, in the form of a push promise, to the client. The * client can choose to accept or reject the push promise request. * - *

      A push promise request may be received up to the point where the + *

      For HTTP/2, a push promise request may be received up to the point where the * response body of the initiating client-sent request has been fully * received. The delivery of a push promise response, however, is not * coordinated with the delivery of the response to the initiating - * client-sent request. + * client-sent request. These are delivered with the + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)} method. + *

      + * For HTTP/3, push promises are handled in a similar way, except that promises + * of the same resource (request URI, request headers and response body) can be + * promised multiple times, but are only delivered by the server (and this API) + * once though the method {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)}. + * Subsequent promises of the same resource, receive a notification only + * of the promise by the method {@link #notifyAdditionalPromise(HttpRequest, PushId)}. + * The same {@link PushPromiseHandler.PushId} is supplied for each of these + * notifications. Additionally, HTTP/3 push promises are not restricted to a context + * of a single initiating request. The same push promise can be delivered and then notified + * across multiple client initiated requests within the same HTTP/3 (QUIC) connection. * * @param the push promise response body type * @since 11 */ public interface PushPromiseHandler { + /** + * Represents a HTTP/3 PushID. PushIds can be shared across + * multiple client initiated requests on the same HTTP/3 connection. + * @since 26 + */ + public sealed interface PushId { + + /** + * Represents an HTTP/3 PushId. + * + * @param pushId the pushId as a long + * @param connectionLabel the {@link HttpResponse#connectionLabel()} + * of the HTTP/3 connection + * @apiNote + * The {@code connectionLabel} should be considered opaque, and ensures that + * two long pushId emitted by different connections correspond to distinct + * instances of {@code PushId}. The {@code pushId} corresponds to the + * unique push ID assigned by the server that identifies a given server + * push on that connection, as defined by + * RFC 9114, + * section 4.6 + * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * + * @since 26 + */ + record Http3PushId(long pushId, String connectionLabel) implements PushId { } + } + /** * Notification of an incoming push promise. * @@ -838,6 +880,12 @@ public interface HttpResponse { * then the push promise is rejected. The {@code acceptor} function will * throw an {@code IllegalStateException} if invoked more than once. * + *

      This method is invoked for all HTTP/2 push promises and also + * by default for the first promise of all HTTP/3 push promises. + * If {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)} + * is overridden, then this method is not directly invoked for HTTP/3 + * push promises. + * * @param initiatingRequest the initiating client-send request * @param pushPromiseRequest the synthetic push request * @param acceptor the acceptor function that must be successfully @@ -849,6 +897,67 @@ public interface HttpResponse { Function,CompletableFuture>> acceptor ); + /** + * Notification of the first occurrence of an HTTP/3 incoming push promise. + * + * Subsequent promises of the same resource (with the same PushId) are notified + * using {@link #notifyAdditionalPromise(HttpRequest, PushId) + * notifyAdditionalPromise(HttpRequest, PushId)}. + * + *

      This method is invoked once for each push promise received, up + * to the point where the response body of the initiating client-sent + * request has been fully received. + * + *

      A push promise is accepted by invoking the given {@code acceptor} + * function. The {@code acceptor} function must be passed a non-null + * {@code BodyHandler}, that is to be used to handle the promise's + * response body. The acceptor function will return a {@code + * CompletableFuture} that completes with the promise's response. + * + *

      If the {@code acceptor} function is not successfully invoked, + * then the push promise is rejected. The {@code acceptor} function will + * throw an {@code IllegalStateException} if invoked more than once. + * + * @implSpec the default implementation invokes + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)}. This allows + * {@code PushPromiseHandlers} from previous releases to handle HTTP/3 push + * promise in a reasonable way. + * + * @param initiatingRequest the client request that resulted in the promise + * @param pushPromiseRequest the promised HttpRequest from the server + * @param pushid the PushId which can be linked to subsequent notifications + * @param acceptor the acceptor function that must be successfully + * invoked to accept the push promise + * + * @since 26 + */ + public default void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function,CompletableFuture>> acceptor + ) { + applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor); + } + + /** + * Invoked for each additional HTTP/3 Push Promise. The {@code pushid} links the promise to the + * original promised {@link HttpRequest} and {@link HttpResponse}. Additional promises + * generally result from different client initiated requests. + * + * @implSpec + * The default implementation of this method does nothing. + * + * @param initiatingRequest the client initiated request which resulted in the push + * @param pushid the pushid which may have been notified previously + * + * @since 26 + */ + public default void notifyAdditionalPromise( + HttpRequest initiatingRequest, + PushId pushid + ) { + } /** * Returns a push promise handler that accumulates push promises, and @@ -915,7 +1024,7 @@ public interface HttpResponse { * * @apiNote To ensure that all resources associated with the corresponding * HTTP exchange are properly released, an implementation of {@code - * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request + * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request(long) * request} more data until one of {@link #onComplete() onComplete} or * {@link #onError(Throwable) onError} are signalled, or {@link * Flow.Subscription#cancel cancel} its {@linkplain @@ -957,7 +1066,7 @@ public interface HttpResponse { * {@snippet : * // Streams the response body to a File * HttpResponse response = client - * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html")); } + * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html"))); } * * {@snippet : * // Accumulates the response body and returns it as a byte[] @@ -1221,7 +1330,7 @@ public interface HttpResponse { * @implNote The {@code read} method of the {@code InputStream} * returned by the default implementation of this method will * throw an {@code IOException} with the {@linkplain Thread#isInterrupted() - * thread interrupt status set} if the thread is interrupted + * thread interrupted status set} if the thread is interrupted * while blocking on read. In that case, the request will also be * cancelled and the {@code InputStream} will be closed. * diff --git a/src/java.net.http/share/classes/java/net/http/StreamLimitException.java b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java new file mode 100644 index 00000000000..583b515b01b --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package java.net.http; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.net.http.HttpClient.Version; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.util.Objects; + +/** + * An exception raised when the limit imposed for stream creation on an + * HTTP connection is reached, and the client is unable to create a new + * stream. + *

      + * A {@code StreamLimitException} may be raised when attempting to send + * a new request on any {@linkplain #version() + * protocol version} that supports multiplexing on a single connection. Both + * {@linkplain HttpClient.Version#HTTP_2 HTTP/2} and {@linkplain + * HttpClient.Version#HTTP_3 HTTP/3} allow multiplexing concurrent requests + * to the same server on a single connection. Each request/response exchange + * is carried over a single stream, as defined by the corresponding + * protocol. + *

      + * Whether and when a {@code StreamLimitException} may be + * relayed to the code initiating a request/response exchange is + * implementation and protocol version dependent. + * + * @see HttpClient#send(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler, PushPromiseHandler) + * + * @since 26 + */ +public final class StreamLimitException extends IOException { + + @java.io.Serial + private static final long serialVersionUID = 2614981180406031159L; + + /** + * The version of the HTTP protocol on which the stream limit exception occurred. + * Must not be null. + * @serial + */ + private final Version version; + + /** + * Creates a new {@code StreamLimitException} + * @param version the version of the protocol on which the stream limit exception + * occurred. Must not be null. + * @param message the detailed exception message, which can be null. + */ + public StreamLimitException(final Version version, final String message) { + super(message); + this.version = Objects.requireNonNull(version); + } + + /** + * {@return the protocol version for which the exception was raised} + */ + public final Version version() { + return version; + } + + /** + * Restores the state of a {@code StreamLimitException} from the stream + * @param in the input stream + * @throws IOException if the class of a serialized object could not be found. + * @throws ClassNotFoundException if an I/O error occurs. + * @throws InvalidObjectException if {@code version} is null. + */ + @java.io.Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (version == null) { + throw new InvalidObjectException("version must not be null"); + } + } +} diff --git a/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java new file mode 100644 index 00000000000..eecc039e5d2 --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package java.net.http; + +import java.io.IOException; +import java.io.Serial; +import java.net.http.HttpClient.Builder; + +/** + * Thrown when the HTTP client doesn't support a particular HTTP version. + * @apiNote + * Typically, this exception may be thrown when attempting to + * {@linkplain Builder#build() build} an {@link java.net.http.HttpClient} + * configured to use {@linkplain java.net.http.HttpClient.Version#HTTP_3 + * HTTP version 3} by default, when the underlying {@link javax.net.ssl.SSLContext + * SSLContext} implementation does not meet the requirements for supporting + * the HttpClient's implementation of the underlying QUIC transport protocol. + * @since 26 + */ +public final class UnsupportedProtocolVersionException extends IOException { + + @Serial + private static final long serialVersionUID = 981344214212332893L; + + /** + * Constructs an {@code UnsupportedProtocolVersionException} with the given detail message. + * + * @param message The detail message; can be {@code null} + */ + public UnsupportedProtocolVersionException(String message) { + super(message); + } +} diff --git a/src/java.net.http/share/classes/java/net/http/package-info.java b/src/java.net.http/share/classes/java/net/http/package-info.java index 9958fd94da0..1b8395c2706 100644 --- a/src/java.net.http/share/classes/java/net/http/package-info.java +++ b/src/java.net.http/share/classes/java/net/http/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -26,7 +26,7 @@ /** *

      HTTP Client and WebSocket APIs

      * - *

      Provides high-level client interfaces to HTTP (versions 1.1 and 2) and + *

      Provides high-level client interfaces to HTTP (versions 1.1, 2, and 3) and * low-level client interfaces to WebSocket. The main types defined are: * *

        @@ -37,10 +37,12 @@ *
      * *

      The protocol-specific requirements are defined in the - * Hypertext Transfer Protocol - * Version 2 (HTTP/2), the + * Hypertext Transfer Protocol + * Version 3 (HTTP/3), the + * Hypertext Transfer Protocol Version 2 (HTTP/2), the + * * Hypertext Transfer Protocol (HTTP/1.1), and - * The WebSocket Protocol. + * The WebSocket Protocol. * *

      In general, asynchronous tasks execute in either the thread invoking * the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler) @@ -66,6 +68,15 @@ *

      Unless otherwise stated, {@code null} parameter values will cause methods * of all classes in this package to throw {@code NullPointerException}. * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * @spec https://www.rfc-editor.org/info/rfc7540 + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * @spec https://www.rfc-editor.org/info/rfc2616 + * RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 + * @spec https://www.rfc-editor.org/info/rfc6455 + * RFC 6455: The WebSocket Protocol + * * @since 11 */ package java.net.http; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java new file mode 100644 index 00000000000..08161bcd110 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.net.ssl.SNIServerName; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A registry for Alternate Services advertised by server endpoints. + * There is one registry per HttpClient. + */ +public final class AltServicesRegistry { + + // id and logger for debugging purposes: the id is the same for the HttpClientImpl. + private final long id; + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + // The key is the origin of the alternate service + // The value is a list of AltService records declared by the origin. + private final Map> altServices = new HashMap<>(); + // alt services which were marked invalid in context of an origin. the reason for + // them being invalid can be connection issues (for example: the alt service didn't present the + // certificate of the origin) + private final InvalidAltServices invalidAltServices = new InvalidAltServices(); + + // used while dealing with both altServices Map and the invalidAltServices Set + private final ReentrantLock registryLock = new ReentrantLock(); + + public AltServicesRegistry(long id) { + this.id = id; + } + + String dbgString() { + return "AltServicesRegistry(" + id + ")"; + } + + public static final class AltService { + // As defined in RFC-7838, section 2, formally an alternate service is a combination of + // ALPN, host and port + public record Identity(String alpn, String host, int port) { + public Identity { + Objects.requireNonNull(alpn); + Objects.requireNonNull(host); + if (port <= 0) { + throw new IllegalArgumentException("Invalid port: " + port); + } + } + + public boolean matches(AltService service) { + return equals(service.identity()); + } + + @Override + public String toString() { + return alpn + "=\"" + Origin.toAuthority(host, port) +"\""; + } + } + + private record AltServiceData(Identity id, Origin origin, Deadline deadline, + boolean persist, boolean advertised, + String authority, + boolean sameAuthorityAsOrigin) { + public String pretty() { + return "AltSvc: " + id + + "; origin=\"" + origin + "\"" + + "; deadline=" + deadline + + "; persist=" + persist + + "; advertised=" + advertised + + "; sameAuthorityAsOrigin=" + sameAuthorityAsOrigin + + ';'; + } + } + private final AltServiceData svc; + + /** + * @param id the alpn, host and port of this alternate service + * @param origin the {@link Origin} for this alternate service + * @param deadline the deadline until which this endpoint is valid + * @param persist whether that information can be persisted (we don't use this) + * @param advertised Whether or not this alt service was advertised as an alt service. + * In certain cases, an alt service is created when no origin server + * has advertised it. In those cases, this param is {@code false} + */ + private AltService(final Identity id, final Origin origin, Deadline deadline, + final boolean persist, + final boolean advertised) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + assert origin.isSecure() : "origin " + origin + " is not secure"; + deadline = deadline == null ? Deadline.MAX : deadline; + final String authority = Origin.toAuthority(id.host, id.port); + final String originAuthority = Origin.toAuthority(origin.host(), origin.port()); + // keep track of whether the authority of this alt service is same as that + // of the origin + final boolean sameAuthorityAsOrigin = authority.equals(originAuthority); + svc = new AltServiceData(id, origin, deadline, persist, advertised, + authority, sameAuthorityAsOrigin); + } + + public Identity identity() { + return svc.id; + } + + /** + * @return {@code host:port} of the alternate service + */ + public String authority() { + return svc.authority; + } + + /** + * @return {@code identity().host()} + */ + public String host() { + return svc.id.host; + } + + /** + * @return {@code identity().port()} + */ + public int port() { + return svc.id.port; + } + + public boolean isPersist() { + return svc.persist; + } + + public boolean wasAdvertised() { + return svc.advertised; + } + + public String alpn() { + return svc.id.alpn; + } + + public Origin origin() { + return svc.origin; + } + + public Deadline deadline() { + return svc.deadline; + } + + /** + * {@return true if the origin, for which this is an alternate service, has the + * same authority as this alternate service. false otherwise.} + */ + public boolean originHasSameAuthority() { + return svc.sameAuthorityAsOrigin; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AltService service)) return false; + return svc.equals(service.svc); + } + + @Override + public int hashCode() { + return svc.hashCode(); + } + + @Override + public String toString() { + return svc.pretty(); + } + + public static Optional create(final Identity id, final Origin origin, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, true)); + } + + private static Optional createUnadvertised(final Logger debug, + final Identity id, final Origin origin, + final HttpConnection conn, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + final List sniServerNames = AltSvcProcessor.getSNIServerNames(conn); + if (sniServerNames == null || sniServerNames.isEmpty()) { + if (debug.on()) { + debug.log("Skipping unadvertised altsvc creation of %s because connection %s" + + " didn't use SNI during connection establishment", id, conn); + } + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, false)); + } + + } + + // A size limited collection which keeps track of unique InvalidAltSvc instances. + // Upon reaching a pre-defined size limit, after adding newer entries, the collection + // then removes the eldest (the least recently added) entry from the collection. + // The implementation of this class is not thread safe and any concurrent access + // to instances of this class should be guarded externally. + private static final class InvalidAltServices extends LinkedHashMap { + + private static final long serialVersionUID = 2772562283544644819L; + + // we track only a reasonably small number of invalid alt services + private static final int MAX_TRACKED_INVALID_ALT_SVCS = 20; + + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > MAX_TRACKED_INVALID_ALT_SVCS; + } + + private boolean contains(final InvalidAltSvc invalidAltSvc) { + return this.containsKey(invalidAltSvc); + } + + private boolean addUnique(final InvalidAltSvc invalidAltSvc) { + if (contains(invalidAltSvc)) { + return false; + } + this.put(invalidAltSvc, null); + return true; + } + } + + // An alt-service is invalid for a particular origin + private record InvalidAltSvc(Origin origin, AltService.Identity id) { + } + + private boolean keepAltServiceFor(Origin origin, AltService svc) { + // skip invalid alt services + if (isMarkedInvalid(origin, svc.identity())) { + if (debug.on()) { + debug.log("Not registering alt-service which was previously" + + " marked invalid: " + svc); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService skipped (was previously marked invalid): " + svc); + } + return false; + } + return true; + } + + /** + * Declare a new Alternate Service endpoint for the given origin. + * + * @param origin the origin + * @param services a set of alt services for the origin + */ + public void replace(final Origin origin, final List services) { + Objects.requireNonNull(origin); + Objects.requireNonNull(services); + List added; + registryLock.lock(); + try { + // the list needs to be thread safe to ensure that we won't + // get a ConcurrentModificationException when iterating + // through the elements in list::stream(); + added = altServices.compute(origin, (key, list) -> { + Stream svcs = services.stream() + .filter(AltService.class::isInstance) // filter null + .filter((s) -> keepAltServiceFor(origin, s)); + List newList = svcs.toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + if (debug.on()) { + debug.log("parsed services: %s", services); + debug.log("resulting services: %s", added); + } + if (Log.altsvc()) { + if (added != null) { + added.forEach((svc) -> Log.logAltSvc("AltService registry updated: {0}", svc)); + } + } + } + + // should be invoked while holding registryLock + private boolean isMarkedInvalid(final Origin origin, final AltService.Identity id) { + assert registryLock.isHeldByCurrentThread() : "Thread isn't holding registry lock"; + return this.invalidAltServices.contains(new InvalidAltSvc(origin, id)); + } + + /** + * Registers an unadvertised alt service for the given origin and the alpn. + * + * @param id The alt service identity + * @param origin The origin + * @return An {@code Optional} containing the registered {@code AltService}, + * or {@link Optional#empty()} if the service was not registered. + */ + Optional registerUnadvertised(final AltService.Identity id, + final Origin origin, + final HttpConnection conn) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + registryLock.lock(); + try { + // an unadvertised alt service is registered by an origin only after a + // successful connection has completed with that alt service. This effectively means + // that we shouldn't check our "invalid alt services" collection, since a successful + // connection against the alt service implies a valid alt service. + // Additionally, we remove it from the "invalid alt services" collection for this + // origin, if at all it was part of that collection + this.invalidAltServices.remove(new InvalidAltSvc(origin, id)); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours. we use + // that same value for unadvertised alt-service(s) for an origin. + final long defaultMaxAgeInSecs = 3600 * 24; + final Deadline deadline = TimeSource.now().plusSeconds(defaultMaxAgeInSecs); + final Optional created = AltService.createUnadvertised(debug, + id, origin, conn, deadline, true); + if (created.isEmpty()) { + return Optional.empty(); + } + final AltService altSvc = created.get(); + altServices.compute(origin, (key, list) -> { + Stream old = list == null ? Stream.empty() : list.stream(); + List newList = Stream.concat(old, Stream.of(altSvc)).toList(); + return newList.isEmpty() ? null : newList; + }); + if (debug.on()) { + debug.log("Added unadvertised AltService: %s", created); + } + if (Log.altsvc()) { + Log.logAltSvc("Added unadvertised AltService: {0}", created); + } + return created; + } finally { + registryLock.unlock(); + } + } + + /** + * Clear the alternate services of the specified origin from the registry + * + * @param origin The origin whose alternate services need to be cleared + */ + public void clear(final Origin origin) { + Objects.requireNonNull(origin); + registryLock.lock(); + try { + if (Log.altsvc()) { + Log.logAltSvc("Clearing AltServices for: " + origin); + } + altServices.remove(origin); + } finally { + registryLock.unlock(); + } + } + + public void markInvalid(final AltService altService) { + Objects.requireNonNull(altService); + markInvalid(altService.origin(), altService.identity()); + } + + private void markInvalid(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + // remove this alt service from the current active set of the origin + this.altServices.computeIfPresent(origin, + (key, currentActive) -> { + assert currentActive != null; // should never be null according to spec + List newList = currentActive.stream() + .filter(Predicate.not(id::matches)).toList(); + return newList.isEmpty() ? null : newList; + + }); + // additionally keep track of this as an invalid alt service, so that it cannot be + // registered again in the future. Banning is temporary. + // Banned alt services may get removed from the set at some point due to + // implementation constraints. In which case they may get registered again + // and banned again, if connecting to the endpoint fails again. + this.invalidAltServices.addUnique(new InvalidAltSvc(origin, id)); + if (debug.on()) { + debug.log("AltService marked invalid: " + id + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService marked invalid: " + id + " for origin " + origin); + } + } finally { + registryLock.unlock(); + } + + } + + public Stream lookup(final URI uri, final String alpn) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpn); + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpn the ALPN of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and that support the given ALPN + */ + public Stream lookup(final Origin origin, final String alpn) { + return lookup(origin, Predicate.isEqual(alpn)); + } + + public Stream lookup(final URI uri, + final Predicate alpnMatcher) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpnMatcher); + } + + private boolean isExpired(AltService service, Deadline now) { + var deadline = service.deadline(); + if (now.equals(deadline) || now.isAfter(deadline)) { + // expired, remove from the list + if (debug.on()) { + debug.log("Removing expired alt-service " + service); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService has expired: {0}", service); + } + return true; + } + return false; + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpnMatcher a predicate to select particular AltService(s) based on the alpn + * of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and whose ALPN satisfies the {@code alpn} predicate. + */ + private Stream lookup(final Origin origin, + final Predicate alpnMatcher) { + if (debug.on()) debug.log("looking up alt-service for: %s", origin); + final List services; + registryLock.lock(); + try { + // we first drop any expired services + final Deadline now = TimeSource.now(); + services = altServices.compute(origin, (key, list) -> { + if (list == null) return null; + List newList = list.stream() + .filter((s) -> !isExpired(s, now)) + .toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + // the order is important - since preferred service are at the head + return services == null + ? Stream.empty() + : services.stream().sequential().filter(s -> alpnMatcher.test(s.identity().alpn())); + } + + /** + * @param altService The alternate service + * {@return true if the {@code service} is known to this registry and the + * service isn't past its max age. false otherwise} + * @throws NullPointerException if {@code service} is null + */ + public boolean isActive(final AltService altService) { + Objects.requireNonNull(altService); + return isActive(altService.origin(), altService.identity()); + } + + private boolean isActive(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + final List currentActive = this.altServices.get(origin); + if (currentActive == null) { + return false; + } + AltService svc = null; + for (AltService s : currentActive) { + if (s.identity().equals(id)) { + svc = s; + break; + } + } + if (svc == null) { + return false; + } + // verify that the service hasn't expired + final Deadline now = TimeSource.now(); + final Deadline deadline = svc.deadline(); + final boolean expired = now.equals(deadline) || now.isAfter(deadline); + if (expired) { + // remove from the registry + altServices.put(origin, currentActive.stream() + .filter(Predicate.not(svc::equals)).toList()); + if (debug.on()) { + debug.log("Removed expired alt-service " + svc + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("Removed AltService: {0}", svc); + } + return false; + } + return true; + } finally { + registryLock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java new file mode 100644 index 00000000000..b172a242346 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.AltSvcFrame; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; + +import static jdk.internal.net.http.Http3ClientProperties.ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; +import static jdk.internal.net.http.common.Alpns.isSecureALPNName; + + +/** + * Responsible for parsing the Alt-Svc values from an Alt-Svc header and/or AltSvc HTTP/2 frame. + */ +final class AltSvcProcessor { + + private static final String HEADER = "alt-svc"; + private static final Logger debug = Utils.getDebugLogger(() -> "AltSvc"); + // a special value that we return back while parsing the header values, + // indicate that all existing alternate services for a origin need to be cleared + private static final List CLEAR_ALL_ALT_SVCS = List.of(); + // whether or not alt service can be created from "localhost" origin host + private static final boolean allowLocalHostOrigin = ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; + + private static final SNIHostName LOCALHOST_SNI = new SNIHostName("localhost"); + + private record ParsedHeaderValue(String rawValue, String alpnName, String host, int port, + Map parameters) { + } + + private AltSvcProcessor() { + throw new UnsupportedOperationException("Instantiation not supported"); + } + + + /** + * Parses the alt-svc header received from origin and update + * registry with the processed values. + * + * @param response response passed on by the server + * @param client client that holds alt-svc registry + * @param request request that holds the origin details + */ + static void processAltSvcHeader(Response response, HttpClientImpl client, + HttpRequestImpl request) { + + // we don't support AltSvc from unsecure origins + if (!request.secure()) { + return; + } + if (response.statusCode == 421) { + // As per AltSvc spec (RFC-7838), section 6: + // An Alt-Svc header field in a 421 (Misdirected Request) response MUST be ignored. + return; + } + final var altSvcHeaderVal = response.headers().firstValue(HEADER); + if (altSvcHeaderVal.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("Processing alt-svc header"); + } + final HttpConnection conn = response.exchange.exchImpl.connection(); + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring alt-svc header because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + final Origin origin; + try { + origin = Origin.from(request.uri()); + } catch (IllegalArgumentException iae) { + if (debug.on()) { + debug.log("ignoring alt-svc header due to: " + iae); + } + // ignore the alt-svc + return; + } + String altSvcValue = altSvcHeaderVal.get(); + processValueAndUpdateRegistry(client, origin, altSvcValue); + } + + static void processAltSvcFrame(final int streamId, + final AltSvcFrame frame, + final HttpConnection conn, + final HttpClientImpl client) { + final String value = frame.getAltSvcValue(); + if (value == null || value.isBlank()) { + return; + } + if (!conn.isSecure()) { + // don't support alt svc from unsecure origins + return; + } + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring altSvc frame because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + debug.log("processing AltSvcFrame %s", value); + final Origin origin; + if (streamId == 0) { + // section 4, RFC-7838 - alt-svc frame on stream 0 with empty (zero length) origin + // is invalid and MUST be ignored + if (frame.getOrigin().isBlank()) { + // invalid frame, ignore it + debug.log("Ignoring invalid alt-svc frame on stream 0 of " + conn); + return; + } + // parse origin from frame.getOrigin() string which is in ASCII + // serialized form of an origin (defined in section 6.2 of RFC-6454) + final Origin parsedOrigin; + try { + parsedOrigin = Origin.fromASCIISerializedForm(frame.getOrigin()); + } catch (IllegalArgumentException iae) { + // invalid origin value, ignore the frame + debug.log("origin couldn't be parsed, ignoring invalid alt-svc frame" + + " on stream " + streamId + " of " + conn); + return; + } + // currently we do not allow an alt service to be advertised for a different origin. + // if the origin advertised in the alt-svc frame doesn't match the origin of the + // connection, then we ignore it. the RFC allows us to do that: + // RFC-7838, section 4: + // An ALTSVC frame from a server to a client on stream 0 indicates that + // the conveyed alternative service is associated with the origin + // contained in the Origin field of the frame. An association with an + // origin that the client does not consider authoritative for the + // current connection MUST be ignored. + if (!parsedOrigin.equals(conn.getOriginServer())) { + debug.log("ignoring alt-svc frame on stream 0 for origin: " + parsedOrigin + + " received on connection of origin: " + conn.getOriginServer()); + return; + } + origin = parsedOrigin; + } else { + // (section 4, RFC-7838) - for non-zero stream id, the alt-svc is for the origin of + // the stream. Additionally, an ALTSVC frame on a stream other than stream 0 containing + // non-empty "Origin" information is invalid and MUST be ignored. + if (!frame.getOrigin().isEmpty()) { + // invalid frame, ignore it + debug.log("non-empty origin in alt-svc frame on stream " + streamId + " of " + + conn + ", ignoring alt-svc frame"); + return; + } + origin = conn.getOriginServer(); + assert origin != null : "origin server is null on connection: " + conn; + } + processValueAndUpdateRegistry(client, origin, value); + } + + private static void processValueAndUpdateRegistry(HttpClientImpl client, + Origin origin, + String altSvcValue) { + final List altServices = processHeaderValue(origin, altSvcValue); + // intentional identity check + if (altServices == CLEAR_ALL_ALT_SVCS) { + // clear all existing alt services for this origin + debug.log("clearing AltServiceRegistry for " + origin); + client.registry().clear(origin); + return; + } + debug.log("AltServices: %s", altServices); + if (altServices.isEmpty()) { + return; + } + // AltService RFC-7838, section 3.1 states: + // + // When an Alt-Svc response header field is received from an origin, its + // value invalidates and replaces all cached alternative services for + // that origin. + client.registry().replace(origin, altServices); + } + + static List getSNIServerNames(final HttpConnection conn) { + final List sniServerNames = conn.getSNIServerNames(); + if (sniServerNames != null && !sniServerNames.isEmpty()) { + return sniServerNames; + } + // no SNI server name(s) were used when establishing this connection. check if + // this connection is to a loopback address and if it is then see if a configuration + // has been set to allow alt services advertised by loopback addresses to be trusted/accepted. + // if such a configuration has been set, then we return a SNIHostName for "localhost" + final InetSocketAddress addr = conn.address(); + final boolean isLoopbackAddr = addr.isUnresolved() + ? false + : conn.address.getAddress().isLoopbackAddress(); + if (!isLoopbackAddr) { + return List.of(); // no SNI server name(s) used for this connection + } + if (!allowLocalHostOrigin) { + // this is a connection to a loopback address, with no SNI server name(s) used + // during TLS handshake and the configuration doesn't allow accepting/trusting + // alt services from loopback address, so we return no SNI server name(s) for this + // connection + return List.of(); + } + // at this point, we have identified this as a loopback address and the configuration + // has been set to accept/trust alt services from loopback address, so we return a + // SNIHostname corresponding to "localhost" + return List.of(LOCALHOST_SNI); + } + + // Here are five examples of values for the Alt-Svc header: + // String svc1 = """foo=":443"; ma=2592000; persist=1""" + // String svc2 = """h3="localhost:5678""""; + // String svc3 = """bar3=":446"; ma=2592000; persist=1"""; + // String svc4 = """h3-34=":5678"; ma=2592000; persist=1"""; + // String svc5 = "%s, %s, %s, %s".formatted(svc1, svc2, svc3, svc4); + // The last one (svc5) should result in two services being registered: + // AltService[origin=https://localhost:64077/, alpn=h3, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-03-13T01:41:01.369488Z, persist=false] + // AltService[origin=https://localhost:64077/, alpn=h3-34, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-04-11T01:41:01.369912Z, persist=true] + private static List processHeaderValue(final Origin origin, + final String headerValue) { + final List altServices = new ArrayList<>(); + // multiple alternate services can be specified with comma as a delimiter + final var altSvcs = headerValue.split(","); + for (var altSvc : altSvcs) { + altSvc = altSvc.trim(); + + // each value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // As per the spec, the value "clear" is expected to be case-sensitive + if (altSvc.equals("clear")) { + return CLEAR_ALL_ALT_SVCS; + } + final ParsedHeaderValue parsed = parseAltValue(origin, altSvc); + if (parsed == null) { + // this implies the alt-svc header value couldn't be parsed and thus is malformed. + // we skip such header values. + debug.log("skipping %s", altSvc); + continue; + } + final var deadline = getValidTill(parsed.parameters().get("ma")); + final var persist = getPersist(parsed.parameters().get("persist")); + final AltService.Identity altSvcId = new AltService.Identity(parsed.alpnName(), + parsed.host(), parsed.port()); + AltService.create(altSvcId, origin, deadline, persist) + .ifPresent((altsvc) -> { + altServices.add(altsvc); + if (Log.altsvc()) { + final var s = altsvc; + Log.logAltSvc("Created AltService: {0}", s); + } else if (debug.on()) { + debug.log("Created AltService for id=%s, origin=%s%n", altSvcId, origin); + } + }); + } + return altServices; + } + + private static ParsedHeaderValue parseAltValue(final Origin origin, final String altValue) { + // header value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // find the = sign that separates the protocol-id and alt-authority + debug.log("parsing %s", altValue); + final int alternativeDelimIndex = altValue.indexOf("="); + if (alternativeDelimIndex == -1 || alternativeDelimIndex == altValue.length() - 1) { + // not a valid alt value + debug.log("no \"=\" character in %s", altValue); + return null; + } + // key is always the protocol-id. example, in 'h3="localhost:5678"; ma=23232; persist=1' + // "h3" acts as the key with '"localhost:5678"; ma=23232; persist=1' as the value + final String protocolId = altValue.substring(0, alternativeDelimIndex); + // the protocol-id can be percent encoded as per the spec, so we decode it to get the alpn name + final var alpnName = decodePotentialPercentEncoded(protocolId); + debug.log("alpn is %s in %s", alpnName, altValue); + if (!isSecureALPNName(alpnName)) { + // no reasonable assurance that the alternate service will be under the control + // of the origin (section 2.1, RFC-7838) + debug.log("alpn %s is not secure, skipping", alpnName); + return null; + } + String remaining = altValue.substring(alternativeDelimIndex + 1); + // now parse alt-authority + if (!remaining.startsWith("\"") || remaining.length() == 1) { + // we expect a quoted string for alt-authority + debug.log("no quoted authority in %s", altValue); + return null; + } + remaining = remaining.substring(1); // skip the starting double quote + final int nextDoubleQuoteIndex = remaining.indexOf("\""); + if (nextDoubleQuoteIndex == -1) { + // malformed value + debug.log("missing closing quote in %s", altValue); + return null; + } + final String altAuthority = remaining.substring(0, nextDoubleQuoteIndex); + final HostPort hostPort = getHostPort(origin, altAuthority); + if (hostPort == null) return null; // host port could not be parsed + if (nextDoubleQuoteIndex == remaining.length() - 1) { + // there's nothing more left to parse + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), Map.of()); + } + // parse the semicolon delimited parameters out of the rest of the remaining string + remaining = remaining.substring(nextDoubleQuoteIndex + 1); + final Map parameters = extractParameters(remaining); + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), parameters); + } + + private static String decodePotentialPercentEncoded(final String val) { + if (!val.contains("%")) { + return val; + } + // TODO: impl this + // In practice this method is only used for the ALPN. + // We only support h3 for now, so we do not need to + // decode percents: anything else but h3 will eventually be ignored. + return val; + } + + private static Map extractParameters(final String val) { + // As per the spec, parameters take the form of: + // *( OWS ";" OWS parameter ) + // ... + // parameter = token "=" ( token / quoted-string ) + // + // where * represents "any number of" and OWS means "optional whitespace" + + final var tokenizer = new StringTokenizer(val, ";"); + if (!tokenizer.hasMoreTokens()) { + return Map.of(); + } + Map parameters = null; + while (tokenizer.hasMoreTokens()) { + final var parameter = tokenizer.nextToken().trim(); + if (parameter.isEmpty()) { + continue; + } + final var equalSignIndex = parameter.indexOf('='); + if (equalSignIndex == -1 || equalSignIndex == parameter.length() - 1) { + // a parameter is expected to have a "=" delimiter which separates a key and a value. + // we skip parameters which don't conform to that rule + continue; + } + final var paramKey = parameter.substring(0, equalSignIndex); + final var paramValue = parameter.substring(equalSignIndex + 1); + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(paramKey, paramValue); + } + if (parameters == null) { + return Map.of(); + } + return Collections.unmodifiableMap(parameters); + } + + private record HostPort(String host, int port) {} + + private static HostPort getHostPort(Origin origin, String altAuthority) { + // The AltService spec defines an alt-authority as follows: + // + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // + // When this method is called the passed altAuthority is already stripped of the leading and trailing + // double-quotes. The value will this be of the form [uri-host]:port where uri-host is optional. + String host; int port; + try { + // Use URI to do the parsing, with a special case for optional host + URI uri = new URI("http://" + altAuthority + "/"); + host = uri.getHost(); + port = uri.getPort(); + if (host == null && port == -1) { + var auth = uri.getRawAuthority(); + if (auth.isEmpty()) return null; + if (auth.charAt(0) == ':') { + uri = new URI("http://x" + altAuthority + "/"); + if ("x".equals(uri.getHost())) { + port = uri.getPort(); + } + } + } + if (port == -1) { + debug.log("Can't parse authority: " + altAuthority); + return null; + } + String hostport; + if (host == null || host.isEmpty()) { + hostport = ":" + port; + host = origin.host(); + } else { + hostport = host + ":" + port; + } + // reject anything unexpected. altAuthority should match hostport + if (!hostport.equals(altAuthority)) { + debug.log("Authority \"%s\" doesn't match host:port \"%s\"", + altAuthority, hostport); + return null; + } + } catch (URISyntaxException x) { + debug.log("Failed to parse authority: %s - %s", + altAuthority, x); + return null; + } + return new HostPort(host, port); + } + + private static Deadline getValidTill(final String maxAge) { + // There's a detailed algorithm in RFC-7234 section 4.2.3, for calculating the age. This + // RFC section is referenced from the alternate service RFC-7838 section 3.1. + // For now though, we use "now" as the instant against which the age will be applied. + final Deadline responseGenerationInstant = TimeSource.now(); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours + final long defaultMaxAgeInSecs = 3600 * 24; + if (maxAge == null) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + try { + final long seconds = Long.parseLong(maxAge); + // negative values aren't allowed for max-age as per RFC-7234, section 1.2.1 + return seconds < 0 ? responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs) + : responseGenerationInstant.plusSeconds(seconds); + } catch (NumberFormatException nfe) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + } + + private static boolean getPersist(final String persist) { + // AltService RFC-7838, section 3.1, states: + // + // This specification only defines a single value for "persist". + // Clients MUST ignore "persist" parameters with values other than "1". + // + return "1".equals(persist); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java new file mode 100644 index 00000000000..bb74ac3d6e9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; + +/** + * An {@link Iterable} clone supporting checked exceptions. + * + * @param the type of elements returned by the produced iterators + */ +@FunctionalInterface +interface CheckedIterable { + + /** + * {@return an {@linkplain CheckedIterator iterator} over elements of type {@code E}} + */ + CheckedIterator iterator() throws Exception; + + static CheckedIterable fromIterable(Iterable iterable) { + return () -> { + Iterator iterator = iterable.iterator(); + return CheckedIterator.fromIterator(iterator); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java new file mode 100644 index 00000000000..42e8e2b6c87 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An {@link Iterator} clone supporting checked exceptions. + * + * @param the type of elements returned by this iterator + */ +interface CheckedIterator { + + /** + * {@return {@code true} if the iteration has more elements} + * @throws Exception if operation fails + */ + boolean hasNext() throws Exception; + + /** + * {@return the next element in the iteration} + * + * @throws NoSuchElementException if the iteration has no more elements + * @throws Exception if operation fails + */ + E next() throws Exception; + + static CheckedIterator fromIterator(Iterator iterator) { + return new CheckedIterator<>() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java index 1ee54ed2bef..c50a4922e80 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java @@ -27,6 +27,7 @@ package jdk.internal.net.http; import java.io.IOException; import java.net.ProtocolException; +import java.net.http.HttpClient.Version; import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -62,6 +63,7 @@ final class Exchange { volatile ExchangeImpl exchImpl; volatile CompletableFuture> exchangeCF; volatile CompletableFuture bodyIgnored; + volatile boolean streamLimitReached; // used to record possible cancellation raised before the exchImpl // has been established. @@ -74,11 +76,18 @@ final class Exchange { final String dbgTag; // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. + // or HTTP/3 exchange so that it can be aborted/timed out mid-setup. final ConnectionAborter connectionAborter = new ConnectionAborter(); final AtomicInteger nonFinalResponses = new AtomicInteger(); + // This will be set to true only when it is guaranteed that the server hasn't processed + // the request. Typically, this happens when the server explicitly states (through a GOAWAY frame + // or a relevant error code in reset frame) that the corresponding stream (id) wasn't processed. + // However, there can be cases where the client is certain that the request wasn't sent + // to the server (and thus not processed). In such cases, the client can set this to true. + private volatile boolean unprocessedByPeer; + Exchange(HttpRequestImpl request, MultiExchange multi) { this.request = request; this.upgrading = false; @@ -110,9 +119,13 @@ final class Exchange { } // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. - static final class ConnectionAborter { + // or HTTP/3 exchange so that it can be aborted/timed out mid setup. + final class ConnectionAborter { + // In case of HTTP/3 requests we may have + // two connections in parallel: a regular TCP connection + // and a QUIC connection. private volatile HttpConnection connection; + private volatile HttpQuicConnection quicConnection; private volatile boolean closeRequested; private volatile Throwable cause; @@ -123,10 +136,11 @@ final class Exchange { // closed closeRequested = this.closeRequested; if (!closeRequested) { - this.connection = connection; - } else { - // assert this.connection == null - this.closeRequested = false; + if (connection instanceof HttpQuicConnection quicConnection) { + this.quicConnection = quicConnection; + } else { + this.connection = connection; + } } } if (closeRequested) closeConnection(connection, cause); @@ -134,6 +148,7 @@ final class Exchange { void closeConnection(Throwable error) { HttpConnection connection; + HttpQuicConnection quicConnection; Throwable cause; synchronized (this) { cause = this.cause; @@ -141,39 +156,64 @@ final class Exchange { cause = error; } connection = this.connection; - if (connection == null) { + quicConnection = this.quicConnection; + if (connection == null || quicConnection == null) { closeRequested = true; this.cause = cause; } else { + this.quicConnection = null; this.connection = null; this.cause = null; } } closeConnection(connection, cause); + closeConnection(quicConnection, cause); } + // Called by HTTP/2 after an upgrade. + // There is no upgrade for HTTP/3 HttpConnection disable() { HttpConnection connection; synchronized (this) { connection = this.connection; this.connection = null; + this.quicConnection = null; this.closeRequested = false; this.cause = null; } return connection; } - private static void closeConnection(HttpConnection connection, Throwable cause) { - if (connection != null) { - try { - connection.close(cause); - } catch (Throwable t) { - // ignore + void clear(HttpConnection connection) { + synchronized (this) { + var c = this.connection; + if (connection == c) this.connection = null; + var qc = this.quicConnection; + if (connection == qc) this.quicConnection = null; + } + } + + private void closeConnection(HttpConnection connection, Throwable cause) { + if (connection == null) { + return; + } + try { + connection.close(cause); + } catch (Throwable t) { + // ignore + if (debug.on()) { + debug.log("ignoring exception that occurred during closing of connection: " + + connection, t); } } } } + // true if previous attempt resulted in streamLimitReached + public boolean hasReachedStreamLimit() { return streamLimitReached; } + // can be used to set or clear streamLimitReached (for instance clear it after retrying) + void streamLimitReached(boolean streamLimitReached) { this.streamLimitReached = streamLimitReached; } + // Called for 204 response - when no body is permitted // This is actually only needed for HTTP/1.1 in order // to return the connection to the pool (or close it) @@ -253,7 +293,7 @@ final class Exchange { impl.cancel(cause); } else { // abort/close the connection if setting up the exchange. This can - // be important when setting up HTTP/2 + // be important when setting up HTTP/2 or HTTP/3 closeReason = failed.get(); if (closeReason != null) { connectionAborter.closeConnection(closeReason); @@ -283,6 +323,9 @@ final class Exchange { cf = exchangeCF; } } + if (multi.requestCancelled() && impl != null && cause == null) { + cause = new IOException("Request cancelled"); + } if (cause == null) return; if (impl != null) { // The exception is raised by propagating it to the impl. @@ -314,7 +357,7 @@ final class Exchange { // if upgraded, we don't close the connection. // cancelling will be handled by the HTTP/2 exchange // in its own time. - if (!upgraded) { + if (!upgraded && !(connection instanceof HttpQuicConnection)) { t = getCancelCause(); if (t == null) t = new IOException("Request cancelled"); if (debug.on()) debug.log("exchange cancelled during connect: " + t); @@ -350,8 +393,8 @@ final class Exchange { private CompletableFuture> establishExchange(HttpConnection connection) { if (debug.on()) { - debug.log("establishing exchange for %s,%n\t proxy=%s", - request, request.proxy()); + debug.log("establishing exchange for %s #%s,%n\t proxy=%s", + request, multi.id, request.proxy()); } // check if we have been cancelled first. Throwable t = getCancelCause(); @@ -364,7 +407,17 @@ final class Exchange { } CompletableFuture> cf, res; - cf = ExchangeImpl.get(this, connection); + + cf = ExchangeImpl.get(this, connection) + // set exchImpl and call checkCancelled to make sure exchImpl + // gets cancelled even if the exchangeCf was completed exceptionally + // before the CF returned by ExchangeImpl.get completed. This deals + // with issues when the request is cancelled while the exchange impl + // is being created. + .thenApply((eimpl) -> { + synchronized (Exchange.this) {exchImpl = eimpl;} + checkCancelled(); return eimpl; + }).copy(); // We should probably use a VarHandle to get/set exchangeCF // instead - as we need CAS semantics. synchronized (this) { exchangeCF = cf; }; @@ -390,7 +443,7 @@ final class Exchange { } // Completed HttpResponse will be null if response succeeded - // will be a non null responseAsync if expect continue returns an error + // will be a non-null responseAsync if expect continue returns an error public CompletableFuture responseAsync() { return responseAsyncImpl(null); @@ -715,4 +768,13 @@ final class Exchange { String dbgString() { return dbgTag; } + + final boolean isUnprocessedByPeer() { + return this.unprocessedByPeer; + } + + // Marks the exchange as unprocessed by the peer + final void markUnprocessedByPeer() { + this.unprocessedByPeer = true; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java index f393b021cd4..74600e78557 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -26,17 +26,26 @@ package jdk.internal.net.http; import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.ResponseInfo; +import java.net.http.UnsupportedProtocolVersionException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Supplier; +import jdk.internal.net.http.Http2Connection.ALPNException; import jdk.internal.net.http.common.HttpBodySubscriberWrapper; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.MinimalFuture; import jdk.internal.net.http.common.Utils; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; /** * Splits request so that headers and body can be sent separately with optional @@ -60,10 +69,6 @@ abstract class ExchangeImpl { private volatile boolean expectTimeoutRaised; - // this will be set to true only when the peer explicitly states (through a GOAWAY frame or - // a relevant error code in reset frame) that the corresponding stream (id) wasn't processed - private volatile boolean unprocessedByPeer; - ExchangeImpl(Exchange e) { // e == null means a http/2 pushed stream this.exchange = e; @@ -98,23 +103,414 @@ abstract class ExchangeImpl { static CompletableFuture> get(Exchange exchange, HttpConnection connection) { - if (exchange.version() == HTTP_1_1) { + HttpRequestImpl request = exchange.request(); + var version = exchange.version(); + if (version == HTTP_1_1 || request.isWebSocket()) { if (debug.on()) debug.log("get: HTTP/1.1: new Http1Exchange"); return createHttp1Exchange(exchange, connection); - } else { - Http2ClientImpl c2 = exchange.client().client2(); // #### improve - HttpRequestImpl request = exchange.request(); - CompletableFuture c2f = c2.getConnectionFor(request, exchange); + } else if (!request.secure() && request.isHttp3Only(version)) { + assert version == HTTP_3; + assert !request.isWebSocket(); if (debug.on()) - debug.log("get: Trying to get HTTP/2 connection"); - // local variable required here; see JDK-8223553 - CompletableFuture>> fxi = - c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); - return fxi.thenCompose(x->x); + debug.log("get: HTTP/3: HTTP/3 is not supported on plain connections"); + return MinimalFuture.failedFuture( + new UnsupportedProtocolVersionException( + "HTTP/3 is not supported on plain connections")); + } else if (version == HTTP_2 || isTCP(connection) || !request.secure()) { + assert !request.isWebSocket(); + return attemptHttp2Exchange(exchange, connection); + } else { + assert request.secure(); + assert version == HTTP_3; + assert !request.isWebSocket(); + return attemptHttp3Exchange(exchange, connection); } } + private static boolean isTCP(HttpConnection connection) { + if (connection instanceof HttpQuicConnection) return false; + if (connection == null) return false; + // if it's not an HttpQuicConnection and it's not null it's + // a TCP connection + return true; + } + + private static CompletableFuture> + attemptHttp2Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + Http2ClientImpl c2 = exchange.client().client2(); // #### improve + CompletableFuture c2f = c2.getConnectionFor(request, exchange); + if (debug.on()) + debug.log("get: Trying to get HTTP/2 connection"); + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = + c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); + return fxi.thenCompose(x -> x); + } + + private static CompletableFuture> + attemptHttp3Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + var exchvers = exchange.version(); + assert request.secure() : request.uri() + " is not secure"; + assert exchvers == HTTP_3 : "expected HTTP/3, got " + exchvers; + // when we reach here, it's guaranteed that the client supports HTTP3 + assert exchange.client().client3().isPresent() : "HTTP3 isn't supported by the client"; + var client3 = exchange.client().client3().get(); + CompletableFuture c3f; + Supplier> c2fs; + var config = request.http3Discovery(); + + if (debug.on()) { + debug.log("get: Trying to get HTTP/3 connection; config is %s", config); + } + // The algorithm here depends on whether HTTP/3 is specified on + // the request itself, or on the HttpClient. + // In both cases, we may attempt a direct HTTP/3 connection if + // we don't have an H3 endpoint registered in the AltServicesRegistry. + // However, if HTTP/3 is not specified explicitly on the request, + // we will start both an HTTP/2 and an HTTP/3 connection at the + // same time, and use the one that complete first. If HTTP/3 is + // specified on the request, we will give priority to HTTP/3 ond + // only start the HTTP/2 connection if the HTTP/3 connection fails, + // or doesn't succeed in the imparted timeout. The timeout can be + // specified with the property "jdk.httpclient.http3.maxDirectConnectionTimeout". + // If unspecified it defaults to 2750ms. + // + // Because the HTTP/2 connection may start as soon as we create the + // CompletableFuture returned by the Http2Client, + // we are using a Supplier> to + // set up the call chain that would start the HTTP/2 connection. + try { + // first look to see if we already have an HTTP/3 connection in + // the pool. If we find one, we're almost done! We won't need + // to start any HTTP/2 connection. + Http3Connection pooled = client3.findPooledConnectionFor(request, exchange); + if (pooled != null) { + c3f = MinimalFuture.completedFuture(pooled); + c2fs = null; + } else { + if (debug.on()) + debug.log("get: no HTTP/3 pooled connection found"); + // possibly start an HTTP/3 connection + boolean mayAttemptDirectConnection = client3.mayAttemptDirectConnection(request); + c3f = client3.getConnectionFor(request, exchange); + if ((!c3f.isDone() || c3f.isCompletedExceptionally()) && mayAttemptDirectConnection) { + // We don't know if the server supports HTTP/3. + // happy eyeball: prepare to try both HTTP/3 and HTTP/2 and + // to use the first that succeeds + if (config != Http3DiscoveryMode.HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("get: trying with both HTTP/3 and HTTP/2"); + } + Http2ClientImpl client2 = exchange.client().client2(); + c2fs = () -> client2.getConnectionFor(request, exchange); + } else { + if (debug.on()) { + debug.log("get: trying with HTTP/3 only"); + } + c2fs = null; + } + } else { + // We have a completed Http3Connection future. + // No need to attempt direct HTTP/3 connection. + c2fs = null; + } + } + } catch (IOException io) { + return MinimalFuture.failedFuture(io); + } + if (c2fs == null) { + // Do not attempt a happy eyeball: go the normal route to + // attempt an HTTP/3 connection + // local variable required here; see JDK-8223553 + if (debug.on()) debug.log("No HTTP/3 eyeball needed"); + CompletableFuture>> fxi = + c3f.handle((h3c, t) -> createExchangeImpl(h3c, t, exchange, connection)); + return fxi.thenCompose(x->x); + } else if (request.version().orElse(null) == HTTP_3) { + // explicit request to use HTTP/3, only use HTTP/2 if HTTP/3 fails, but + // still start both connections in parallel. HttpQuicConnection will + // attempt a direct connection. Because we register + // firstToComplete as a dependent action of c3f we will actually + // only use HTTP/2 (or HTTP/1.1) if HTTP/3 failed + CompletableFuture>> fxi = + c3f.handle((h3c, e) -> firstToComplete(exchange, connection, c2fs, c3f)); + if (debug.on()) { + debug.log("Explicit HTTP/3 request: " + + "attempt HTTP/3 first, then default to HTTP/2"); + } + return fxi.thenCompose(x->x); + } + if (debug.on()) { + debug.log("Attempt HTTP/3 and HTTP/2 in parallel, use the first that connects"); + } + // default client version is HTTP/3 - request version is not set. + // so try HTTP/3 + HTTP/2 in parallel and take the first that completes. + return firstToComplete(exchange, connection, c2fs, c3f); + } + + // Use the first connection that successfully completes. + // This is a bit hairy because HTTP/2 may be downgraded to HTTP/1 if the server + // doesn't support HTTP/2. In which case the connection attempt will succeed but + // c2f will be completed with a ALPNException. + private static CompletableFuture> firstToComplete( + Exchange exchange, + HttpConnection connection, + Supplier> c2fs, + CompletableFuture c3f) { + if (debug.on()) { + debug.log("firstToComplete(connection=%s)", connection); + debug.log("Will use the first connection that succeeds from HTTP/2 or HTTP/3"); + } + assert connection == null : "should not come here if connection is not null: " + connection; + + // Set up a completable future (cf) that will complete + // when the first HTTP/3 or HTTP/2 connection result is + // available. Error cases (when the result is exceptional) + // is handled in a dependent action of cf later below + final CompletableFuture cf; + // c3f is used for HTTP/3, c2f for HTTP/2 + final CompletableFuture c2f; + if (c3f.isDone()) { + // We already have a result for HTTP/3, consider that first; + // There's no need to start HTTP/2 yet if the result is successful. + c2f = null; + cf = c3f; + } else { + // No result for HTTP/3 yet, start HTTP/2 now and wait for the + // first that completes. + c2f = c2fs.get(); + cf = CompletableFuture.anyOf(c2f, c3f); + } + + CompletableFuture>> cfxi = cf.handle((r, t) -> { + if (debug.on()) { + debug.log("Checking which from HTTP/2 or HTTP/3 succeeded first"); + } + CompletableFuture> res; + // first check if c3f is completed successfully + if (c3f.isDone()) { + Http3Connection h3c = c3f.exceptionally((e) -> null).resultNow(); + if (h3c != null) { + // HTTP/3 success! Use HTTP/3 + if (debug.on()) { + debug.log("HTTP/3 connect completed first, using HTTP/3"); + } + res = createExchangeImpl(h3c, null, exchange, connection); + if (c2f != null) c2f.thenApply(c -> { + if (c != null) { + c.abandonStream(); + } + return c; + }); + } else { + // HTTP/3 failed! Use HTTP/2 + if (debug.on()) { + debug.log("HTTP/3 connect completed unsuccessfully," + + " either with null or with exception - waiting for HTTP/2"); + c3f.handle((r3, t3) -> { + debug.log("\tcf3: result=%s, throwable=%s", + r3, Utils.getCompletionCause(t3)); + return r3; + }).exceptionally((e) -> null).join(); + } + // c2f may be null here in the case where c3f was already completed + // when firstToComplete was called. + var h2cf = c2f == null ? c2fs.get() : c2f; + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = h2cf + .handle((h2c, e) -> createExchangeImpl(h2c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } else if (c2f != null && c2f.isDone()) { + Http2Connection h2c = c2f.exceptionally((e) -> null).resultNow(); + if (h2c != null) { + // HTTP/2 succeeded first! Use it. + if (debug.on()) { + debug.log("HTTP/2 connect completed first, using HTTP/2"); + } + res = createExchangeImpl(h2c, null, exchange, connection); + } else if (exchange.multi.requestCancelled()) { + // special case for when the exchange is cancelled + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully, but request cancelled"); + } + CompletableFuture>> fxi = c2f + .handle((c, e) -> createExchangeImpl(c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully," + + " either with null or with exception"); + c2f.handle((r2, t2) -> { + debug.log("\tcf2: result=%s, throwable=%s", + r2, Utils.getCompletionCause(t2)); + return r2; + }).exceptionally((e) -> null).join(); + } + + // Now is the more complex stuff. + // HTTP/2 could have failed in the ALPN, but we still + // created a valid TLS connection to the server => default + // to HTTP/1.1 over TLS + HttpConnection http1Connection = null; + if (c2f.isCompletedExceptionally() && !c2f.isCancelled()) { + Throwable cause = Utils.getCompletionCause(c2f.exceptionNow()); + if (cause instanceof ALPNException alpn) { + debug.log("HTTP/2 downgraded to HTTP/1.1 - use HTTP/1.1"); + http1Connection = alpn.getConnection(); + } + } + if (http1Connection != null) { + if (debug.on()) { + debug.log("HTTP/1.1 connect completed first, using HTTP/1.1"); + } + // ALPN failed - but we have a valid HTTP/1.1 connection + // to the server: use that. + res = createHttp1Exchange(exchange, http1Connection); + } else { + if (c2f.isCompletedExceptionally()) { + // Wait for HTTP/3 to complete, potentially fallback to + // HTTP/1.1 + // local variable required here; see JDK-8223553 + debug.log("HTTP/2 completed with exception, wait for HTTP/3, " + + "possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + // + // r2 == null && t2 == null - which means we know the + // server doesn't support h2, and we probably already + // have an HTTP/1.1 connection to it + // + // If an HTTP/1.1 connection is available use it. + // Otherwise, wait for the HTTP/3 to complete, potentially + // fallback to HTTP/1.1 + HttpRequestImpl request = exchange.request(); + InetSocketAddress proxy = Utils.resolveAddress(request.proxy()); + InetSocketAddress addr = request.getAddress(); + ConnectionPool pool = exchange.client().connectionPool(); + // if we have an HTTP/1.1 connection in the pool, use that. + http1Connection = pool.getConnection(true, addr, proxy); + if (http1Connection != null && http1Connection.isOpen()) { + debug.log("Server doesn't support HTTP/2, " + + "but we have an HTTP/1.1 connection in the pool"); + debug.log("Using HTTP/1.1"); + res = createHttp1Exchange(exchange, http1Connection); + } else { + // we don't have anything ready to use in the pool: + // wait for http/3 to complete, possibly falling back + // to HTTP/1.1 + debug.log("Server doesn't support HTTP/2, " + + "and we do not have an HTTP/1.1 connection"); + debug.log("Waiting for HTTP/3, possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } + } + } + } else { + assert c2f != null; + Throwable failed = t != null ? t : new InternalError("cf1 or cf2 should have completed"); + res = MinimalFuture.failedFuture(failed); + } + return res; + }); + return cfxi.thenCompose(x -> x); + } + + private static CompletableFuture> + fallbackToHttp1OnTimeout(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (t != null) { + Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof HttpConnectTimeoutException) { + // when we reach here we already tried with HTTP/2, + // and we most likely have an HTTP/1.1 connection in + // the idle pool. So fallback to that. + if (debug.on()) { + debug.log("HTTP/3 connection timed out: fall back to HTTP/1.1"); + } + return createHttp1Exchange(exchange, null); + } + } + return createExchangeImpl(c, t, exchange, connection); + } + + + + // Creates an HTTP/3 exchange, possibly downgrading to HTTP/2 + private static CompletableFuture> + createExchangeImpl(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (debug.on()) + debug.log("handling HTTP/3 connection creation result"); + if (t == null && exchange.multi.requestCancelled()) { + return MinimalFuture.failedFuture(new IOException("Request cancelled")); + } + if (c == null && t == null) { + if (debug.on()) + debug.log("downgrading to HTTP/2"); + return attemptHttp2Exchange(exchange, connection); + } else if (t != null) { + t = Utils.getCompletionCause(t); + if (debug.on()) { + if (t instanceof HttpConnectTimeoutException || t instanceof ConnectException) { + debug.log("HTTP/3 connection creation failed: " + t); + } else { + debug.log("HTTP/3 connection creation failed " + + "with unexpected exception:", t); + } + } + return MinimalFuture.failedFuture(t); + } else { + if (debug.on()) + debug.log("creating HTTP/3 exchange"); + try { + if (exchange.hasReachedStreamLimit()) { + // clear the flag before attempting to create a stream again + exchange.streamLimitReached(false); + } + return c.createStream(exchange) + .thenApply(ExchangeImpl::checkCancelled); + } catch (IOException e) { + return MinimalFuture.failedFuture(e); + } + } + } + + private static > T checkCancelled(T exchangeImpl) { + Exchange e = exchangeImpl.getExchange(); + if (debug.on()) { + debug.log("checking cancellation for: " + exchangeImpl); + } + if (e.multi.requestCancelled()) { + if (debug.on()) { + debug.log("request was cancelled"); + } + if (!exchangeImpl.isCanceled()) { + if (debug.on()) { + debug.log("cancelling exchange: " + exchangeImpl); + } + var cause = e.getCancelCause(); + if (cause == null) cause = new IOException("Request cancelled"); + exchangeImpl.cancel(cause); + } + } + return exchangeImpl; + } + + + // Creates an HTTP/2 exchange, possibly downgrading to HTTP/1 private static CompletableFuture> createExchangeImpl(Http2Connection c, Throwable t, @@ -280,12 +676,4 @@ abstract class ExchangeImpl { // an Expect-Continue void expectContinueFailed(int rcode) { } - final boolean isUnprocessedByPeer() { - return this.unprocessedByPeer; - } - - // Marks the exchange as unprocessed by the peer - final void markUnprocessedByPeer() { - this.unprocessedByPeer = true; - } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java new file mode 100644 index 00000000000..3eb4d631c75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; + +/** + * Verifies that when a HTTP3 frame arrives on a stream, then that particular frame type + * is in the expected order as compared to the previous frame type that was received. + * In effect, does what the RFC-9114, section 4.1 and section 6.2.1 specifies. + * Note that the H3FrameOrderVerifier is only responsible for checking the order in which a + * frame type is received on a stream. It isn't responsible for checking if that particular frame + * type is expected to be received on a particular stream type. + */ +abstract class H3FrameOrderVerifier { + long currentProcessingFrameType = -1; // -1 implies no frame being processed currently + long lastCompletedFrameType = -1; // -1 implies no frame processing has completed yet + + /** + * {@return a frame order verifier for HTTP3 request/response stream} + */ + static H3FrameOrderVerifier newForRequestResponseStream() { + return new ResponseStreamVerifier(false); + } + + /** + * {@return a frame order verifier for HTTP3 push promise stream} + */ + static H3FrameOrderVerifier newForPushPromiseStream() { + return new ResponseStreamVerifier(true); + } + + /** + * {@return a frame order verifier for HTTP3 control stream} + */ + static H3FrameOrderVerifier newForControlStream() { + return new ControlStreamVerifier(); + } + + /** + * @param frame The frame that has been received + * {@return true if the {@code frameType} processing can start. false otherwise} + */ + abstract boolean allowsProcessing(final Http3Frame frame); + + /** + * Marks the receipt of complete content of a frame that was currently being processed + * + * @param frame The frame whose content was fully received + * @throws IllegalStateException If the passed frame type wasn't being currently processed + */ + void completed(final Http3Frame frame) { + if (frame instanceof UnknownFrame) { + return; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != frameType) { + throw new IllegalStateException("Unexpected completion of processing " + + "of frame type (" + frameType + "): " + + Http3FrameType.asString(frameType) + ", expected " + + Http3FrameType.asString(currentProcessingFrameType)); + } + currentProcessingFrameType = -1; + lastCompletedFrameType = frameType; + } + + private static final class ControlStreamVerifier extends H3FrameOrderVerifier { + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't "track" it either + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion. However, if an unknown frame + // is the first frame on a control stream then that's an error and we return "false" + // to prevent processing that frame. + // RFC-9114, section 9, which states - "where a known frame type is required to be + // in a specific location, such as the SETTINGS frame as the first frame of the + // control stream, an unknown frame type does not satisfy that requirement and + // SHOULD be treated as an error" + return lastCompletedFrameType != -1; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + // we are not currently processing any frame + if (lastCompletedFrameType == -1) { + // there was no previous frame either, so this is the first frame to have been + // received + if (frameType != SettingsFrame.TYPE) { + // unexpected first frame type + return false; + } + currentProcessingFrameType = frameType; + // expected first frame type + return true; + } + // there's no specific ordering specified on control stream other than expecting + // the SETTINGS frame to be the first received (which we have already verified before + // reaching here) + currentProcessingFrameType = frameType; + return true; + } + } + + private static final class ResponseStreamVerifier extends H3FrameOrderVerifier { + private boolean headerSeen; + private boolean dataSeen; + private boolean trailerCompleted; + private final boolean pushStream; + + private ResponseStreamVerifier(boolean pushStream) { + this.pushStream = pushStream; + } + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't track their processing/completion + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion + return true; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + if (frameType == DataFrame.TYPE) { + if (!headerSeen || trailerCompleted) { + // DATA is not permitted before HEADERS or after trailer + return false; + } + dataSeen = true; + } else if (frameType == HeadersFrame.TYPE) { + if (trailerCompleted) { + // HEADERS is not permitted after trailer + return false; + } + headerSeen = true; + if (dataSeen) { + trailerCompleted = true; + } + } else if (frameType == PushPromiseFrame.TYPE) { + // a push promise is only permitted on a response, + // and not on a push stream + if (pushStream) { + return false; + } + } else { + // no other frames permitted + return false; + } + + currentProcessingFrameType = frameType; + return true; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java index ecc4a63c9d0..02ce63b6314 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java @@ -244,7 +244,7 @@ class Http1Exchange extends ExchangeImpl { this.connection = connection; } else { InetSocketAddress addr = request.getAddress(); - this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1); + this.connection = HttpConnection.getConnection(addr, client, exchange, request, HTTP_1_1); } this.requestAction = new Http1Request(request, this); this.asyncReceiver = new Http1AsyncReceiver(executor, this); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java index 815b6bad20c..8d28b664036 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java @@ -290,7 +290,8 @@ class Http1Request { } String uriString = requestURI(); StringBuilder sb = new StringBuilder(64); - sb.append(request.method()) + String method = request.method(); + sb.append(method) .append(' ') .append(uriString) .append(" HTTP/1.1\r\n"); @@ -300,11 +301,15 @@ class Http1Request { systemHeadersBuilder.setHeader("Host", hostString()); } - // GET, HEAD and DELETE with no request body should not set the Content-Length header if (requestPublisher != null) { contentLength = requestPublisher.contentLength(); if (contentLength == 0) { - systemHeadersBuilder.setHeader("Content-Length", "0"); + // PUT and POST with no request body should set the Content-Length header + // even when the content is empty. + // Other methods defined in RFC 9110 should not send the header in that case. + if ("POST".equals(method) || "PUT".equals(method)) { + systemHeadersBuilder.setHeader("Content-Length", "0"); + } } else if (contentLength > 0) { systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength)); streaming = false; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java index 92a48d901ff..cc8a2a7142b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java @@ -76,7 +76,7 @@ class Http2ClientImpl { /** * When HTTP/2 requested only. The following describes the aggregate behavior including the - * calling code. In all cases, the HTTP2 connection cache + * calling code. In all cases, the HTTP/2 connection cache * is checked first for a suitable connection and that is returned if available. * If not, a new connection is opened, except in https case when a previous negotiate failed. * In that case, we want to continue using http/1.1. When a connection is to be opened and @@ -144,6 +144,7 @@ class Http2ClientImpl { if (conn != null) { try { conn.reserveStream(true, exchange.pushEnabled()); + exchange.connectionAborter.clear(conn.connection); } catch (IOException e) { throw new UncheckedIOException(e); // shouldn't happen } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java index c33cc93e7dd..63889fa6af2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java @@ -33,6 +33,7 @@ import java.lang.invoke.VarHandle; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -70,6 +71,7 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.ValidatingHeadersConsumer; import jdk.internal.net.http.common.ValidatingHeadersConsumer.Context; +import jdk.internal.net.http.frame.AltSvcFrame; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.ErrorFrame; @@ -90,6 +92,7 @@ import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.DecodingCallback; import jdk.internal.net.http.hpack.Encoder; import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH; import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE; @@ -527,6 +530,7 @@ class Http2Connection { AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection) HttpConnection.getConnection(request.getAddress(), h2client.client(), + exchange, request, HttpClient.Version.HTTP_2); @@ -635,6 +639,32 @@ class Http2Connection { return true; } + void abandonStream() { + boolean shouldClose = false; + stateLock.lock(); + try { + long reserved = --numReservedClientStreams; + assert reserved >= 0; + if (finalStream && reserved == 0 && streams.isEmpty()) { + shouldClose = true; + } + } catch (Throwable t) { + shutdown(t); // in case the assert fires... + } finally { + stateLock.unlock(); + } + + // We should close the connection here if + // it's not pooled. If it's not pooled it will + // be marked final stream, reserved will be 0 + // after decrementing it by one, and there should + // be no active request-response streams. + if (shouldClose) { + shutdown(new IOException("HTTP/2 connection abandoned")); + } + + } + boolean shouldClose() { stateLock.lock(); try { @@ -1218,6 +1248,8 @@ class Http2Connection { case PingFrame.TYPE -> handlePing((PingFrame) frame); case GoAwayFrame.TYPE -> handleGoAway((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> handleWindowUpdate((WindowUpdateFrame) frame); + case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame, + connection, connection.client()); default -> protocolError(ErrorFrame.PROTOCOL_ERROR); } @@ -1323,7 +1355,8 @@ class Http2Connection { try { // idleConnectionTimeoutEvent is always accessed within a lock protected block if (streams.isEmpty() && idleConnectionTimeoutEvent == null) { - idleConnectionTimeoutEvent = client().idleConnectionTimeout() + final HttpClient.Version version = Version.HTTP_2; + idleConnectionTimeoutEvent = client().idleConnectionTimeout(version) .map(IdleConnectionTimeoutEvent::new) .orElse(null); if (idleConnectionTimeoutEvent != null) { @@ -1367,6 +1400,7 @@ class Http2Connection { String protocolError = "protocol error" + (msg == null?"":(": " + msg)); ProtocolException protocolException = new ProtocolException(protocolError); + this.cause.compareAndSet(null, protocolException); if (markHalfClosedLocal()) { framesDecoder.close(protocolError); subscriber.stop(protocolException); @@ -1844,8 +1878,16 @@ class Http2Connection { } finally { Throwable x = errorRef.get(); if (x != null) { - if (debug.on()) debug.log("Stopping scheduler", x); scheduler.stop(); + if (client2.stopping()) { + if (debug.on()) { + debug.log("Stopping scheduler"); + } + } else { + if (debug.on()) { + debug.log("Stopping scheduler", x); + } + } Http2Connection.this.shutdown(x); } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java new file mode 100644 index 00000000000..05b27c3d529 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java @@ -0,0 +1,844 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.ConnectionExpiredException; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.quic.QuicTLSContext; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.WAIT_FOR_PENDING_CONNECT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; + +/** + * Http3 specific aspects of HttpClientImpl + */ +final class Http3ClientImpl implements AutoCloseable { + // Setting this property disables HTTPS hostname verification. Use with care. + private static final boolean disableHostnameVerification = Utils.isHostnameVerificationDisabled(); + // QUIC versions in their descending order of preference + private static final List availableQuicVersions; + static { + // we default to QUIC v1 followed by QUIC v2, if no specific preference cannot be + // determined + final List defaultPref = List.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + // check user specified preference + final String sysPropVal = Utils.getProperty("jdk.httpclient.quic.available.versions"); + if (sysPropVal == null || sysPropVal.isBlank()) { + // default to supporting both v1 and v2, with v1 given preference + availableQuicVersions = defaultPref; + } else { + final List descendingPref = new ArrayList<>(); + for (final String val : sysPropVal.split(",")) { + final QuicVersion qv; + try { + // parse QUIC version number represented as a hex string + final var vernum = Integer.parseInt(val.trim(), 16); + qv = QuicVersion.of(vernum).orElse(null); + } catch (NumberFormatException nfe) { + // ignore and continue with next + continue; + } + if (qv == null) { + continue; + } + descendingPref.add(qv); + } + availableQuicVersions = descendingPref.isEmpty() ? defaultPref : descendingPref; + } + } + + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + final HttpClientImpl client; + private final Http3ConnectionPool connections = new Http3ConnectionPool(debug); + private final Http3PendingConnections reconnections = new Http3PendingConnections(); + private final Set pendingClose = ConcurrentHashMap.newKeySet(); + private final Set noH3 = ConcurrentHashMap.newKeySet(); + + private final QuicClient quicClient; + private volatile boolean closed; + private final AtomicReference errorRef = new AtomicReference<>(); + private final ReentrantLock lock = new ReentrantLock(); + + Http3ClientImpl(HttpClientImpl client) { + this.client = client; + var executor = client.theExecutor().safeDelegate(); + var context = client.theSSLContext(); + var parameters = client.sslParameters(); + if (!disableHostnameVerification) { + // setting the endpoint identification algo to HTTPS ensures that + // during the TLS handshake, the cert presented by the server is verified + // for hostname checks against the SNI hostname(s) set by the client + // or in its absence the peer's hostname. + // see sun.security.ssl.X509TrustManagerImpl#checkIdentity(...) + parameters.setEndpointIdentificationAlgorithm("HTTPS"); + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicClient.Builder builder = new QuicClient.Builder(); + builder.availableVersions(availableQuicVersions) + .tlsContext(quicTLSContext) + .sslParameters(parameters) + .executor(executor) + .applicationErrors(Http3Error::stringForCode) + .clientId(client.dbgString()); + if (client.localAddress() != null) { + builder.bindAddress(new InetSocketAddress(client.localAddress(), 0)); + } + final QuicTransportParameters transportParameters = new QuicTransportParameters(); + // HTTP/3 doesn't allow remote bidirectional stream + transportParameters.setIntParameter(initial_max_streams_bidi, 0); + // HTTP/3 doesn't allow remote bidirectional stream: no need to allow data + transportParameters.setIntParameter(initial_max_stream_data_bidi_remote, 0); + builder.transportParameters(transportParameters); + this.quicClient = builder.build(); + } + + // Records an exchange waiting for a connection recovery to complete. + // A connection recovery happens when a connection has maxed out its number + // of streams, and no MAX_STREAM frame has arrived. In that case, the connection + // is abandoned (marked with setFinalStream() and taken out of the pool) and a + // new connection is initiated. Waiters are waiting for the new connection + // handshake to finish and for the connection to be put in the pool. + record Waiter(MinimalFuture cf, HttpRequestImpl request, Exchange exchange) { + void complete(Http3Connection conn, Throwable error) { + if (error != null) cf.completeExceptionally(error); + else cf.complete(conn); + } + static Waiter of(HttpRequestImpl request, Exchange exchange) { + return new Waiter(new MinimalFuture<>(), request, exchange); + } + } + + // Indicates that recovery is needed, or in progress, for a given + // connection + sealed interface ConnectionRecovery permits PendingConnection, StreamLimitReached { + } + + // Indicates that recovery of a connection has been initiated. + // Waiters will be put in wait until the handshake is completed + // and the connection is inserted in the pool + record PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) + implements ConnectionRecovery { + PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) { + this.altSvc = altSvc; + this.waiters = Objects.requireNonNull(waiters); + this.exchange = exchange; + } + PendingConnection(AltService altSvc, Exchange exchange) { + this(altSvc, exchange, new ConcurrentLinkedQueue<>()); + } + } + + // Indicates that a connection that was in the pool has maxed out + // its stream limit and will be taken out of the pool. A new connection + // will be created for the first request/response exchange that needs + // it. + record StreamLimitReached(Http3Connection connection) implements ConnectionRecovery {} + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + public void streamLimitReached(Http3Connection connection, HttpRequestImpl request) { + lock.lock(); + try { + reconnections.streamLimitReached(connectionKey(request), connection); + } finally { + lock.unlock(); + } + } + + HttpClientImpl client() { + return client; + } + + String dbgString() { + return "Http3ClientImpl(" + client.dbgString() + ")"; + } + + QuicClient quicClient() { + return this.quicClient; + } + + String connectionKey(HttpRequestImpl request) { + return connections.connectionKey(request); + } + + Http3Connection findPooledConnectionFor(HttpRequestImpl request, + Exchange exchange) + throws IOException { + if (request.secure() && request.proxy() == null) { + final var pooled = connections.lookupFor(request); + if (pooled == null) { + return null; + } + if (pooled.tryReserveForPoolCheckout() && !pooled.isFinalStream()) { + final var altService = pooled.connection() + .getSourceAltService().orElse(null); + if (altService != null) { + // if this connection was created because it was advertised by some alt-service + // then verify that the alt-service is still valid/active + if (altService.wasAdvertised() && !client.registry().isActive(altService)) { + if (debug.on()) { + debug.log("Alt-Service %s for pooled connection has expired," + + " marking the connection as unusable for new streams", altService); + } + // alt-service that was the reason for this H3 connection to be created (and pooled) + // is no longer valid. We set a state on the connection to disallow any new streams + // and be auto-closed when all current streams are done + pooled.setFinalStreamAndCloseIfIdle(); + return null; + } + } + if (debug.on()) { + debug.log("Found Http3Connection in connection pool"); + } + // found a valid connection in pool, return it + return pooled; + } else { + if (debug.on()) { + debug.log("Pooled connection expired. Removing it."); + } + removeFromPool(pooled); + } + } + return null; + } + + private static String label(Http3Connection conn) { + return Optional.ofNullable(conn) + .map(Http3Connection::connection) + .map(HttpQuicConnection::label) + .orElse("null"); + } + + private static String describe(HttpRequestImpl request, long id) { + return String.format("%s #%s", request, id); + } + + private static String describe(Exchange exchange) { + if (exchange == null) return "null"; + return describe(exchange.request, exchange.multi.id); + } + + private static String describePendingExchange(String prefix, PendingConnection pending) { + return String.format("%s %s", prefix, describe(pending.exchange)); + } + + private static String describeAltSvc(PendingConnection pendingConnection) { + return Optional.ofNullable(pendingConnection) + .map(PendingConnection::altSvc) + .map(AltService::toString) + .map(s -> "altsvc: " + s) + .orElse("no altSvc"); + } + + // Called after a recovered connection has been put back in the pool + // (or when recovery has failed), or when a new connection handshake + // has completed. + // Waiters, if any, will be notified. + private void connectionCompleted(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + try { + if (Log.http3()) { + Log.logHttp3("Checking waiters on completed connection {0} to {1} created for {2}", + label(conn), connectionKey, describe(origExchange)); + } + connectionCompleted0(connectionKey, origExchange, conn, error); + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError(t); + } + throw t; + } + } + + private void connectionCompleted0(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + lock.lock(); + // There should be a connection in the pool at this point, + // so we can remove the PendingConnection from the reconnections list; + PendingConnection pendingConnection = null; + try { + var recovery = reconnections.removeCompleted(connectionKey, origExchange, conn); + if (recovery instanceof PendingConnection pending) { + pendingConnection = pending; + } + } finally { + lock.unlock(); + } + if (pendingConnection == null) { + if (Log.http3()) { + Log.logHttp3("No waiters to complete for " + label(conn)); + } + return; + } + + int waitersCount = pendingConnection.waiters.size(); + if (waitersCount != 0 && Log.http3()) { + Log.logHttp3("Completing " + waitersCount + + " waiters on recreated connection " + label(conn) + + describePendingExchange(" - originally created for", pendingConnection)); + } + + // now for each waiter we're going to try to complete it. + // however, there may be more waiters than available streams! + // so it's rinse and repeat at this point + boolean origExchangeCancelled = origExchange == null ? false : origExchange.multi.requestCancelled(); + int completedWaiters = 0; + int errorWaiters = 0; + int retriedWaiters = 0; + try { + while (!pendingConnection.waiters.isEmpty()) { + var waiter = pendingConnection.waiters.poll(); + if (error != null && (!origExchangeCancelled || waiter.exchange == origExchange)) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + error); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + conn + " error=" + error); + } + errorWaiters++; + waiter.complete(conn, error); + } else { + var request = waiter.request; + var exchange = waiter.exchange; + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null && !pooled.isFinalStream() && !waiter.cf.isDone()) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(pooled)); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with pooled conn " + label(pooled)); + } + completedWaiters++; + waiter.cf.complete(pooled); + } else if (!waiter.cf.isDone()) { + // we call getConnectionFor: it should put waiter in the + // new waiting list, or attempt to open a connection again + if (conn != null) { + if (Log.http3()) { + Log.logHttp3("Not enough streams on recreated connection for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(conn)); + } else if (debug.on()) { + debug.log("Not enough streams on recreated connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + label(conn) + + ": retrying on new connection"); + } + retriedWaiters++; + getConnectionFor(request, exchange, waiter); + } else { + if (Log.http3()) { + Log.logHttp3("No HTTP/3 connection for:: " + waiter.request + " #" + + waiter.exchange.multi.id + ": will downgrade or fail"); + } else if (debug.on()) { + debug.log("No HTTP/3 connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + ": will downgrade or fail"); + } + completedWaiters++; + waiter.complete(null, error); + } + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with error: " + + Utils.getCompletionCause(t)); + } + var cause = Utils.getCompletionCause(t); + if (cause instanceof ClosedChannelException) { + cause = new ConnectionExpiredException(cause); + } + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + cause); + } + errorWaiters++; + waiter.cf.completeExceptionally(cause); + } + } + } + } finally { + if (Log.http3()) { + String pendingInfo = describePendingExchange(" - originally created for", pendingConnection); + + if (conn != null) { + Log.logHttp3(("Connection creation completed for requests to %s: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } else { + Log.logHttp3(("No HTTP/3 connection created for requests to %s, will fail or downgrade: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } + } + } + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, Exchange exchange) { + assert request != null; + return getConnectionFor(request, exchange, null); + } + + private void completeWaiter(Logger debug, Waiter pendingWaiter, Http3Connection r, Throwable t) { + // the recovery was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + if (Log.http3()) { + Log.logHttp3("Completing waiter for: " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } else if (debug.on()) { + debug.log("Completing pending waiter for " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } + pendingWaiter.complete(r, t); + } + + private CompletableFuture wrapForDebug(CompletableFuture h3Cf, + Exchange exchange, + HttpRequestImpl request) { + if (debug.on() || Log.http3()) { + if (Log.http3()) { + Log.logHttp3("Recreating connection for: " + request + " #" + + exchange.multi.id); + } else if (debug.on()) { + debug.log("Recreating connection for: " + request + " #" + + exchange.multi.id); + } + return h3Cf.whenComplete((r, t) -> { + if (Log.http3()) { + if (r != null && t == null) { + Log.logHttp3("Connection recreated for " + request + " #" + + exchange.multi.id + " on " + label(r)); + } else if (t != null) { + Log.logHttp3("Connection creation failed for " + request + " #" + + exchange.multi.id + ": " + t); + } else if (r == null) { + Log.logHttp3("No connection found for " + request + " #" + + exchange.multi.id); + } + } else if (debug.on()) { + debug.log("Connection recreated for " + request + " #" + + exchange.multi.id); + } + }); + } else { + return h3Cf; + } + } + + Optional lookupAltSvc(HttpRequestImpl request) { + return client.registry() + .lookup(request.uri(), H3::equals) + .findFirst(); + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, + Exchange exchange, + Waiter pendingWaiter) { + assert request != null; + if (Log.http3()) { + if (pendingWaiter != null) { + Log.logHttp3("getConnectionFor pendingWaiter {0}", + describe(pendingWaiter.request, pendingWaiter.exchange.multi.id)); + } else { + Log.logHttp3("getConnectionFor exchange {0}", + describe(request, exchange.multi.id)); + } + } + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null) { + if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + if (request.secure() && request.proxy() == null) { + boolean reconnecting, waitForPendingConnect; + PendingConnection pendingConnection = null; + String key; + Waiter waiter = null; + if (reconnecting = exchange.hasReachedStreamLimit()) { + if (debug.on()) { + debug.log("Exchange has reached limit for: " + request + " #" + + exchange.multi.id); + } + } + if (pendingWaiter != null) reconnecting = true; + lock.lock(); + try { + key = connectionKey(request); + + var recovery = reconnections.lookupFor(key, request, client); + if (debug.on()) debug.log("lookup found %s for %s", recovery, request); + if (recovery instanceof PendingConnection pending) { + // Recovery already initiated. Add waiter to the list! + if (debug.on()) { + debug.log("PendingConnection (%s) found for %s", + describePendingExchange("originally created for", pending), + describe(request, exchange.multi.id)); + } + pendingConnection = pending; + waiter = pendingWaiter == null + ? Waiter.of(request, exchange) + : pendingWaiter; + exchange.streamLimitReached(false); + pendingConnection.waiters.add(waiter); + return waiter.cf; + } else if (recovery instanceof StreamLimitReached) { + // A connection to this server has maxed out its allocated + // streams and will be taken out of the pool, but recovery + // has not been initiated yet. Do that now. + reconnecting = waitForPendingConnect = true; + } else waitForPendingConnect = WAIT_FOR_PENDING_CONNECT; + // By default, we allow concurrent attempts to + // create HTTP/3 connections to the same host, except when + // one connection has reached the maximum number of streams + // it is allowed to use. However, + // if waitForPendingConnect is set to `true` above we will + // only allow one connection to attempt handshake at a given + // time, other requests will be added to a pending list so + // that they can go through that connection. + if (waitForPendingConnect) { + // check again + if ((pooled = findPooledConnectionFor(request, exchange)) == null) { + // initiate recovery + var altSvc = lookupAltSvc(request).orElse(null); + // maybe null if ALT_SVC && altSvc == null + pendingConnection = reconnections.addPending(key, request, altSvc, exchange); + } else if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + } finally { + lock.unlock(); + if (waiter != null && waiter != pendingWaiter && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + var orig = Optional.of(pendingConnection) + .map(PendingConnection::exchange) + .map(e -> " created for #" + e.multi.id) + .orElse(""); + Log.logHttp3("Waiting for connection for: " + describe(request, exchange.multi.id) + + " " + altSvc + orig); + } else if (pendingWaiter != null && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + Log.logHttp3("Creating connection for: " + describe(request, exchange.multi.id) + + " " + altSvc); + } else if (debug.on() && waiter != null) { + debug.log("Waiting for connection for: " + describe(request, exchange.multi.id) + + (waiter == pendingWaiter ? " (still pending)" : "")); + } + } + + if (Log.http3()) { + Log.logHttp3("Creating connection for Exchange {0}", describe(exchange)); + } else if (debug.on()) { + debug.log("Creating connection for Exchange %s", describe(exchange)); + } + + CompletableFuture h3Cf = Http3Connection + .createAsync(request, this, exchange); + if (reconnecting) { + // System.err.println("Recreating connection for: " + request + " #" + // + exchange.multi.id); + h3Cf = wrapForDebug(h3Cf, exchange, request); + } + if (pendingWaiter != null) { + // the connection was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + h3Cf = h3Cf.whenComplete((r,t) -> completeWaiter(debug, pendingWaiter, r, t)); + } + h3Cf = h3Cf.thenApply(conn -> { + if (conn != null) { + if (debug.on()) { + debug.log("Offering connection %s created for %s", + label(conn), exchange.multi.id); + } + var offered = offerConnection(conn); + if (debug.on()) { + debug.log("Connection offered %s created for %s", + label(conn), exchange.multi.id); + } + // if we return null here, we will downgrade + // but if we return `conn` we will open a new connection. + return offered == null ? conn : offered; + } else { + if (debug.on()) { + debug.log("No connection for exchange #" + exchange.multi.id); + } + return null; + } + }); + if (pendingConnection != null) { + // need to wake up waiters after successful handshake and recovery + h3Cf = h3Cf.whenComplete((r, t) -> connectionCompleted(key, exchange, r, t)); + } + return h3Cf; + } else { + if (debug.on()) + debug.log("Request is unsecure, or proxy isn't null: can't use HTTP/3"); + if (request.isHttp3Only(exchange.version())) { + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "can't use HTTP/3 with proxied or unsecured connection")); + } + return MinimalFuture.completedFuture(null); + } + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError("Failed to get connection for {0}: {1}", + describe(exchange), t); + } + return MinimalFuture.failedFuture(t); + } + } + + /* + * Cache the given connection, if no connection to the same + * destination exists. If one exists, then we let the initial stream + * complete but allow it to close itself upon completion. + * This situation should not arise with https because the request + * has not been sent as part of the initial alpn negotiation + */ + Http3Connection offerConnection(Http3Connection c) { + if (debug.on()) debug.log("offering to the connection pool: %s", c); + if (!c.isOpen() || c.isFinalStream()) { + if (debug.on()) + debug.log("skipping offered closed or closing connection: %s", c); + return null; + } + + String key = c.key(); + lock.lock(); + try { + if (closed) { + var error = errorRef.get(); + if (error == null) error = new IOException("client closed"); + c.connectionError(error, Http3Error.H3_INTERNAL_ERROR); + return null; + } + Http3Connection c1 = connections.putIfAbsent(key, c); + if (c1 != null) { + // there was a connection in the pool + if (!c1.isFinalStream() || c.isFinalStream()) { + if (!c.isFinalStream()) { + c.allowOnlyOneStream(); + return c; + } else if (c1.isFinalStream()) { + return c; + } + if (debug.on()) + debug.log("existing entry %s in connection pool for %s", c1, key); + // c1 will remain in the pool and we will use c for the given + // request. + if (Log.http3()) { + Log.logHttp3("Existing connection {0} for {1} found in the pool", label(c1), c1.key()); + Log.logHttp3("New connection {0} marked final and not offered to the pool", label(c)); + } + return c1; + } + connections.put(key, c); + } + if (debug.on()) + debug.log("put in the connection pool: %s", c); + return c; + } finally { + lock.unlock(); + } + } + + void removeFromPool(Http3Connection c) { + lock.lock(); + try { + if (connections.remove(c.key(), c)) { + if (debug.on()) + debug.log("removed from the connection pool: %s", c); + } + if (c.isOpen()) { + if (debug.on()) + debug.log("adding to pending close: %s", c); + pendingClose.add(c); + } + } finally { + lock.unlock(); + } + } + + void connectionClosed(Http3Connection c) { + removeFromPool(c); + if (pendingClose.remove(c)) { + if (debug.on()) + debug.log("removed from pending close: %s", c); + } + } + + public Logger debug() { return debug;} + + @Override + public void close() { + try { + lock.lock(); + try { + closed = true; + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + // The client itself is being closed, so we don't individually close the connections + // here and instead just close the QuicClient which then initiates the close of + // the QUIC endpoint. That will silently terminate the underlying QUIC connections + // without exchanging any datagram packets with the peer, since there's no point + // sending/receiving those (including GOAWAY frame) when the endpoint (socket channel) + // itself won't be around after this point. + } finally { + quicClient.close(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + if (debug.on()) { + debug.log("HTTP/3 client aborting due to " + t); + } + try { + errorRef.compareAndSet(null, t); + List connectionList; + lock.lock(); + try { + closed = true; + connectionList = new ArrayList<>(connections.values().toList()); + connectionList.addAll(pendingClose); + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + for (var conn : connectionList) { + conn.close(t); + } + } finally { + quicClient.abort(t); + } + } + + public void stop() { + close(); + } + + /** + * After an unsuccessful H3 direct connection attempt, + * mark the authority as not supporting h3. + * @param rawAuthority the raw authority (host:port) + */ + public void noH3(String rawAuthority) { + noH3.add(rawAuthority); + } + + /** + * Tells whether the given authority has been marked as + * not supporting h3 + * @param rawAuthority the raw authority (host:port) + * @return true if the given authority is believed to not support h3 + */ + public boolean hasNoH3(String rawAuthority) { + return noH3.contains(rawAuthority); + } + + /** + * A direct HTTP/3 attempt may be attempted if we don't have an + * AltService h3 endpoint recorded for it, and if the given request + * URI's raw authority hasn't been marked as not supporting HTTP/3, + * and if the request discovery config is not ALT_SVC. + * Note that a URI may be marked has not supporting H3 if it doesn't + * acknowledge the first initial quic packet in the time defined + * by {@systemProperty jdk.httpclient.http3.maxDirectConnectionTimeout}. + * @param request the request that may go through h3 + * @return true if there's no h3 endpoint already registered for the given uri. + */ + public boolean mayAttemptDirectConnection(HttpRequestImpl request) { + var config = request.http3Discovery(); + return switch (config) { + // never attempt direct connection with ALT_SVC + case Http3DiscoveryMode.ALT_SVC -> false; + // always attempt direct connection with HTTP_3_ONLY, unless + // it was attempted before and failed + case Http3DiscoveryMode.HTTP_3_URI_ONLY -> + !hasNoH3(request.uri().getRawAuthority()); + // otherwise, attempt direct connection only if we have no + // alt service and it wasn't attempted and failed before + default -> lookupAltSvc(request).isEmpty() + && !hasNoH3(request.uri().getRawAuthority()); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java new file mode 100644 index 00000000000..81f8c8109d5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.common.Utils; + +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY; + +/** + * A class that groups initial values for HTTP/3 client properties. + *

      + * Properties starting with {@code jdk.internal.} are not exposed and + * typically reserved for testing. They could be removed, and their name, + * semantics, or values, could be changed at any time. + *

      + * Properties that are exposed are JDK specifics and typically documented + * in the {@link java.net.http} module API documentation. + *

        + *
      1. + *
      2. + *
      + * + * @apiNote + * Not all properties are exposed. Properties that are not included in + * the {@link java.net.http} module API documentation are subject to + * change, and should be considered internal, though we might also consider + * exposing them in the future if needed. + * + */ +public final class Http3ClientProperties { + + private Http3ClientProperties() { + throw new InternalError("should not come here"); + } + + // The maximum timeout to wait for a reply to the first INITIAL + // packet when attempting a direct connection + public static final long MAX_DIRECT_CONNECTION_TIMEOUT; + + // The maximum timeout to wait for a MAX_STREAM frame + // before throwing StreamLimitException + public static final long MAX_STREAM_LIMIT_WAIT_TIMEOUT; + + // The maximum number of concurrent push streams + // by connection + public static final long MAX_HTTP3_PUSH_STREAMS; + + // Limit for dynamic table capacity that the encoder is allowed + // to set. Its capacity is also limited by the QPACK_MAX_TABLE_CAPACITY + // HTTP/3 setting value received from the peer decoder. + public static final long QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long QPACK_DECODER_BLOCKED_STREAMS; + + // of available space in the dynamic table + + // Percentage of occupied space in the dynamic table that controls when + // the draining index starts increasing. This index determines which entries + // are too close to eviction, and can be referenced by the encoder. + public static final int QPACK_ENCODER_DRAINING_THRESHOLD; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean QPACK_ALLOW_BLOCKING_ENCODING = Utils.getBooleanProperty( + "jdk.internal.httpclient.qpack.allowBlockingEncoding", false); + + // whether localhost is acceptable as an alternative service origin + public static final boolean ALTSVC_ALLOW_LOCAL_HOST_ORIGIN = Utils.getBooleanProperty( + "jdk.httpclient.altsvc.allowLocalHostOrigin", true); + + // whether concurrent HTTP/3 requests to the same host should wait for + // first connection to succeed (or fail) instead of attempting concurrent + // connections. Where concurrent connections are attempted, only one of + // them will be offered to the connection pool. The others will serve a + // single request. + public static final boolean WAIT_FOR_PENDING_CONNECT = Utils.getBooleanProperty( + "jdk.httpclient.http3.waitForPendingConnect", true); + + + static { + // 375 is ~ to the initial loss timer + // 1000 is ~ the initial PTO + // We will set a timeout of 2*1375 ms to wait for the reply to our + // first initial packet for a direct connection + long defaultMaxDirectConnectionTimeout = 1375 << 1; // ms + long maxDirectConnectionTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxDirectConnectionTimeout", + defaultMaxDirectConnectionTimeout); + long maxStreamLimitTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxStreamLimitTimeout", + defaultMaxDirectConnectionTimeout); + int defaultMaxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.maxstreams", + 100); + int maxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.http3.maxConcurrentPushStreams", + defaultMaxHttp3PushStreams); + long defaultDecoderMaxCapacity = 0; + long decoderMaxTableCapacity = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderMaxTableCapacity", + defaultDecoderMaxCapacity); + long decoderBlockedStreams = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderBlockedStreams", + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + long defaultEncoderTableCapacityLimit = 4096; + long encoderTableCapacityLimit = Utils.getLongProperty( + "jdk.httpclient.qpack.encoderTableCapacityLimit", + defaultEncoderTableCapacityLimit); + int defaultDecoderMaxFieldSectionSize = 393216; // 384kB + long decoderMaxFieldSectionSize = Utils.getIntegerNetProperty( + "jdk.http.maxHeaderSize", Integer.MIN_VALUE, Integer.MAX_VALUE, + defaultDecoderMaxFieldSectionSize, true); + // Percentage of occupied space in the dynamic table that when + // exceeded the dynamic table draining index starts increasing + int drainingThreshold = Utils.getIntegerProperty( + "jdk.internal.httpclient.qpack.encoderDrainingThreshold", + 75); + + MAX_DIRECT_CONNECTION_TIMEOUT = maxDirectConnectionTimeout <= 0 + ? defaultMaxDirectConnectionTimeout : maxDirectConnectionTimeout; + MAX_STREAM_LIMIT_WAIT_TIMEOUT = maxStreamLimitTimeout < 0 + ? defaultMaxDirectConnectionTimeout + : maxStreamLimitTimeout; + MAX_HTTP3_PUSH_STREAMS = Math.max(maxHttp3PushStreams, 0); + QPACK_ENCODER_TABLE_CAPACITY_LIMIT = encoderTableCapacityLimit < 0 + ? defaultEncoderTableCapacityLimit : encoderTableCapacityLimit; + QPACK_DECODER_MAX_TABLE_CAPACITY = decoderMaxTableCapacity < 0 ? + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY : decoderMaxTableCapacity; + QPACK_DECODER_MAX_FIELD_SECTION_SIZE = decoderMaxFieldSectionSize < 0 ? + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE : decoderMaxFieldSectionSize; + QPACK_DECODER_BLOCKED_STREAMS = decoderBlockedStreams < 0 ? + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS : decoderBlockedStreams; + QPACK_ENCODER_DRAINING_THRESHOLD = Math.clamp(drainingThreshold, 10, 90); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java new file mode 100644 index 00000000000..b97a441881d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java @@ -0,0 +1,1657 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.net.http.StreamLimitException; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.GoAwayFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.MaxPushIdFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.streams.QuicStreams; +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; + +/** + * An HTTP/3 connection wraps an HttpQuicConnection and implements + * HTTP/3 on top it. + */ +public final class Http3Connection implements AutoCloseable { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3ClientImpl client; + private final HttpQuicConnection connection; + private final QuicConnection quicConnection; + // key by which this connection will be referred to within the connection pool + private final String connectionKey; + private final String dbgTag; + private final UniStreamPair controlStreamPair; + private final UniStreamPair qpackEncoderStreams; + private final UniStreamPair qpackDecoderStreams; + private final Encoder qpackEncoder; + private final Decoder qpackDecoder; + private final FramesDecoder controlFramesDecoder; + private final Predicate remoteStreamListener; + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForControlStream(); + // streams for HTTP3 exchanges + private final ConcurrentMap exchangeStreams = new ConcurrentHashMap<>(); + private final ConcurrentMap> exchanges = new ConcurrentHashMap<>(); + // true when the settings frame has been received on the control stream of this connection + private volatile boolean settingsFrameReceived; + // the settings we received from the peer + private volatile ConnectionSettings peerSettings; + // the settings we send to our peer + private volatile ConnectionSettings ourSettings; + // for tests + private final MinimalFuture peerSettingsCF = new MinimalFuture<>(); + // the (lowest) request stream id received in GOAWAY frames on this connection. + // subsequent request stream id(s) (if any) must always be equal to lesser than this value + // as per spec + // -1 is used to imply no GOAWAY received so far + private final AtomicLong lowestGoAwayReceipt = new AtomicLong(-1); + private volatile IdleConnectionTimeoutEvent idleConnectionTimeoutEvent; + // value of true implies no more streams will be initiated on this connection, + // and the connection will be closed once the in-progress streams complete. + private volatile boolean finalStream; + private volatile boolean allowOnlyOneStream; + // set to true if we decide to open a new connection + // due to stream limit reached + private volatile boolean streamLimitReached; + + private static final int GOAWAY_SENT = 1; // local endpoint sent GOAWAY + private static final int GOAWAY_RECEIVED = 2; // received GOAWAY from remote peer + private static final int CLOSED = 4; // close called on QUIC connection + volatile int closedState; + + private final ReentrantLock lock = new ReentrantLock(); + private final Http3PushManager pushManager; + private final AtomicLong reservedStreamCount = new AtomicLong(); + + // The largest pushId for a remote created stream. + // After GOAWAY has been sent, we will not accept + // any larger pushId. + private final AtomicLong largestPushId = new AtomicLong(); + + // The max pushId for which a frame was scheduled to be sent. + // This should always be less or equal to pushManager.maxPushId + private final AtomicLong maxPushIdSent = new AtomicLong(); + + + /** + * Creates a new HTTP/3 connection over a given {@link HttpQuicConnection}. + * + * @apiNote + * This constructor is invoked upon a successful quic connection establishment, + * typically after a successful Quic handshake. Creating the Http3Connection + * earlier, for instance, after receiving the Server Hello, could also be considered. + * + * @implNote + * Creating an HTTP/3 connection will trigger the creation of the HTTP/3 control + * stream, sending of the HTTP/3 Settings frame, and creation of the QPack + * encoder/decoder streams. + * + * @param request the request which triggered the creation of the connection + * @param client the Http3Client instance this connection belongs to + * @param connection the {@code HttpQuicConnection} that was established + */ + Http3Connection(HttpRequestImpl request, Http3ClientImpl client, HttpQuicConnection connection) { + this.connectionKey = client.connectionKey(request); + this.client = client; + this.connection = connection; + this.quicConnection = connection.quicConnection(); + var qdb = quicConnection.dbgTag(); + this.dbgTag = "H3(" + qdb +")"; + this.pushManager = new Http3PushManager(this); // OK to leak this + controlFramesDecoder = new FramesDecoder("H3-control("+qdb+")", + FramesDecoder::isAllowedOnControlStream); + controlStreamPair = new UniStreamPair( + StreamType.CONTROL, + quicConnection, + this::processPeerControlBytes, + this::lcsWriterLoop, + this::controlStreamFailed, + debug); + + qpackEncoder = new Encoder(Http3Connection::shouldUpdateDynamicTable, + this::createEncoderStreams, this::connectionError); + qpackEncoderStreams = qpackEncoder.encoderStreams(); + qpackDecoder = new Decoder(this::createDecoderStreams, this::connectionError); + qpackDecoderStreams = qpackDecoder.decoderStreams(); + // Register listener to be called when the peer opens a new stream + remoteStreamListener = this::onOpenRemoteStream; + quicConnection.addRemoteStreamListener(remoteStreamListener); + + // Registers dependent actions with the controlStreamPair + // .futureSenderStreamWriter() CF, in order to send + // the SETTINGS and MAX_PUSHID frames. + // These actions will be executed when the stream writer is + // available. + // + // This will schedule the SETTINGS and MAX_PUSHID frames + // for writing, buffering them if necessary until control + // flow credits are available. + // + // If an exception happens the connection will be + // closed abruptly (by closing the underlying quic connection) + // with an error of type Http3Error.H3_INTERNAL_ERROR + controlStreamPair.futureSenderStreamWriter() + // Send SETTINGS first + .thenApply(this::sendSettings) + // Chains to sending MAX_PUSHID after SETTINGS + .thenApply(this::sendMaxPushId) + // arranges for the connection to be closed + // in case of exception. Throws in the dependent + // action after wrapping the exception if needed. + .exceptionally(this::exceptionallyAndClose); + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection created for " + quicConnectionTag() + " - local address: " + + quicConnection.localAddress()); + } + } + + public String quicConnectionTag() { + return quicConnection.logTag(); + } + + private static boolean shouldUpdateDynamicTable(TableEntry tableEntry) { + if (tableEntry.type() == TableEntry.EntryType.NAME_VALUE) { + return false; + } + return switch (tableEntry.name().toString()) { + case ":authority", "user-agent" -> !tableEntry.value().isEmpty(); + default -> false; + }; + } + + private void lock() { + lock.lock(); + } + + private void unlock() { + lock.unlock(); + } + + /** + * Debug tag used to create the debug logger for this + * HTTP/3 connection instance. + * + * @return a debug tag + */ + String dbgTag() { + return dbgTag; + } + + /** + * Asynchronously create an instance of an HTTP/3 connection, if the + * server has a known HTTP/3 endpoint. + * @param request the first request that will go over this connection + * @param h3client the HTTP/3 client + * @param exchange the exchange for which this connection is created + * @return a completable future that will be completed with a new + * HTTP/3 connection, or {@code null} if no usable HTTP/3 endpoint + * was found, or completed exceptionally if an error occurred + */ + static CompletableFuture createAsync(HttpRequestImpl request, + Http3ClientImpl h3client, + Exchange exchange) { + assert request.secure(); + final HttpConnection connection = HttpConnection.getConnection(request.getAddress(), + h3client.client(), + exchange, + request, + HTTP_3); + var debug = h3client.debug(); + var where = "Http3Connection.createAsync"; + if (!(connection instanceof HttpQuicConnection httpQuicConnection)) { + if (Log.http3()) { + Log.logHttp3("{0}: Connection for {1} #{2} is not an HttpQuicConnection: {3}", + where, request, exchange.multi.id, connection); + } + if (debug.on()) + debug.log("%s: Connection is not an HttpQuicConnection: %s", where, connection); + if (request.isHttp3Only(exchange.version())) { + assert connection == null; + // may happen if the client doesn't support HTTP3 + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "cannot establish exchange to requested origin with HTTP/3")); + } + return MinimalFuture.completedFuture(null); + } + if (debug.on()) { + debug.log("%s: Got HttpQuicConnection: %s", where, connection); + } + if (Log.http3()) { + Log.logHttp3("{0}: Got HttpQuicConnection for {1} #{2} is: {3}", + where, request, exchange.multi.id, connection.label()); + } + + // Expose the underlying connection to the exchange's aborter so it can + // be closed if a timeout occurs. + exchange.connectionAborter.connection(httpQuicConnection); + + return httpQuicConnection.connectAsync(exchange) + .thenCompose(unused -> httpQuicConnection.finishConnect()) + .thenCompose(unused -> checkSSLConfig(httpQuicConnection)) + .thenCompose(notused-> { + CompletableFuture cf = new MinimalFuture<>(); + try { + if (debug.on()) + debug.log("creating Http3Connection for %s", httpQuicConnection); + Http3Connection hc = new Http3Connection(request, h3client, httpQuicConnection); + if (!hc.isFinalStream()) { + exchange.connectionAborter.clear(httpQuicConnection); + cf.complete(hc); + } else { + var io = new IOException("can't reserve first stream"); + if (Log.http3()) { + Log.logHttp3(" Unable to use HTTP/3 connection over {0}: {1}", + hc.quicConnectionTag(), + io); + } + hc.protocolError(io); + cf.complete(null); + } + } catch (Exception e) { + cf.completeExceptionally(e); + } + return cf; } ) + .whenComplete(httpQuicConnection::connectionEstablished); + } + + private static CompletableFuture checkSSLConfig(HttpQuicConnection quic) { + // HTTP/2 checks ALPN here; with HTTP/3, we only offer one ALPN, + // and TLS verifies that it's negotiated. + + // We can examine the negotiated parameters here and possibly fail + // if they are not satisfactory. + return MinimalFuture.completedFuture(null); + } + + HttpQuicConnection connection() { + return connection; + } + + String key() { + return connectionKey; + } + + /** + * Whether the final stream (last stream allowed on a connection), has + * been set. + * + * @return true if the final stream has been set. + */ + boolean isFinalStream() { + return this.finalStream; + } + + /** + * Sets the final stream to be the next stream opened on + * the connection. No other stream will be opened after this. + */ + void setFinalStream() { + this.finalStream = true; + } + + void setFinalStreamAndCloseIfIdle() { + boolean closeNow; + lock(); + try { + setFinalStream(); + closeNow = finalStreamClosed(); + } finally { + unlock(); + } + if (closeNow) close(); + } + + void allowOnlyOneStream() { + lock(); + try { + if (isFinalStream()) return; + this.allowOnlyOneStream = true; + this.finalStream = true; + } finally { + unlock(); + } + } + + boolean isOpen() { + return closedState == 0 && quicConnection.isOpen(); + } + + private IOException checkConnectionError() { + final TerminationCause tc = quicConnection.terminationCause(); + return tc == null ? null : tc.getCloseCause(); + } + + // Used only by tests + CompletableFuture peerSettingsCF() { + return peerSettingsCF; + } + + private boolean reserveStream() { + lock(); + try { + boolean allowStream0 = this.allowOnlyOneStream; + this.allowOnlyOneStream = false; + if (finalStream && !allowStream0) { + return false; + } + reservedStreamCount.incrementAndGet(); + return true; + } finally { + unlock(); + } + } + + CompletableFuture> + createStream(final Exchange exchange) throws IOException { + // check if this connection is closing before initiating this new stream + if (!reserveStream()) { + if (Log.http3()) { + Log.logHttp3("Cannot initiate new stream on connection {0} for exchange {1}", + quicConnectionTag(), exchange); + } + // we didn't create the stream and thus the server hasn't yet processed this request. + // mark the request as unprocessed to allow it to be retried on a different connection. + exchange.markUnprocessedByPeer(); + String message = "cannot initiate additional new streams on chosen connection"; + IOException cause = streamLimitReached + ? new StreamLimitException(HTTP_3, message) + : new IOException(message); + return MinimalFuture.failedFuture(cause); + } + // TODO: this duration is currently "computed" from the request timeout duration. + // this computation needs a bit more thought + final Duration streamLimitIncreaseDuration = exchange.request.timeout() + .map((reqTimeout) -> reqTimeout.dividedBy(2)) + .orElse(Duration.ofMillis(MAX_STREAM_LIMIT_WAIT_TIMEOUT)); + final CompletableFuture bidiStream = + quicConnection.openNewLocalBidiStream(streamLimitIncreaseDuration); + // once the bidi stream creation completes: + // - if completed exceptionally, we transform any QuicStreamLimitException into a + // StreamLimitException + // - if completed successfully, we create a Http3 exchange and return that as the result + final CompletableFuture>> h3ExchangeCf = + bidiStream.handle((stream, t) -> { + if (t == null) { + // no exception occurred and a bidi stream was created on the quic + // connection, but check if the connection has been terminated + // in the meantime + final var terminationCause = checkConnectionError(); + if (terminationCause != null) { + // connection already closed and we haven't yet issued the request. + // mark the exchange as unprocessed to allow it to be retried on + // a different connection. + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(terminationCause); + } + // creation of bidi stream succeeded, now create the H3 exchange impl + // and return it + final Http3ExchangeImpl h3Exchange = createHttp3ExchangeImpl(exchange, stream); + return MinimalFuture.completedFuture(h3Exchange); + } + // failed to open a bidi stream + reservedStreamCount.decrementAndGet(); + final Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof QuicStreamLimitException) { + if (Log.http3()) { + Log.logHttp3("Maximum stream limit reached on {0} for exchange {1}", + quicConnectionTag(), exchange.multi.streamLimitState()); + } + if (debug.on()) { + debug.log("bidi stream creation failed due to stream limit: " + + cause + ", connection will be marked as unusable for subsequent" + + " requests"); + } + // Since we have reached the stream creation limit (which translates to not + // being able to initiate new requests on this connection), we mark the + // connection as "final stream" (i.e. don't consider this (pooled) + // connection for subsequent requests) + this.streamLimitReachedWith(exchange); + return MinimalFuture.failedFuture(new StreamLimitException(HTTP_3, + "No more streams allowed on connection")); + } else if (cause instanceof ClosedChannelException) { + // stream creation failed due to the connection (that was chosen) + // got closed. Thus the request wasn't processed by the server. + // mark the request as unprocessed to allow it to be + // initiated on a different connection + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(cause); + } + return MinimalFuture.failedFuture(cause); + }); + return h3ExchangeCf.thenCompose(Function.identity()); + } + + private void streamLimitReachedWith(Exchange exchange) { + streamLimitReached = true; + client.streamLimitReached(this, exchange.request); + setFinalStream(); + } + + private Http3ExchangeImpl createHttp3ExchangeImpl(Exchange exchange, QuicBidiStream stream) { + if (debug.on()) { + debug.log("Temporary reference h3 stream: " + stream.streamId()); + } + if (Log.http3()) { + Log.logHttp3("Creating HTTP/3 exchange for {0}/streamId={1}", + quicConnectionTag(), Long.toString(stream.streamId())); + } + client.client.h3StreamReference(); + try { + lock(); + try { + this.exchangeStreams.put(stream.streamId(), stream); + reservedStreamCount.decrementAndGet(); + var te = idleConnectionTimeoutEvent; + if (te != null) { + client.client().cancelTimer(te); + idleConnectionTimeoutEvent = null; + } + } finally { + unlock(); + } + var http3Exchange = new Http3ExchangeImpl<>(this, exchange, stream); + return registerAndStartExchange(http3Exchange); + } finally { + if (debug.on()) { + debug.log("Temporary unreference h3 stream: " + stream.streamId()); + } + client.client.h3StreamUnreference(); + } + } + + private Http3ExchangeImpl registerAndStartExchange(Http3ExchangeImpl exchange) { + var streamId = exchange.streamId(); + if (debug.on()) debug.log("Reference h3 stream: " + streamId); + client.client.h3StreamReference(); + exchanges.put(streamId, exchange); + exchange.start(); + return exchange; + } + + // marks this connection as no longer available for creating additional streams. current + // streams will run to completion. marking the connection as gracefully shutdown + // can involve sending the necessary protocol message(s) to the peer. + private void sendGoAway() throws IOException { + if (markSentGoAway()) { + // already sent (either successfully or an attempt was made) GOAWAY, nothing more to do + return; + } + // RFC-9114, section 5.2: Endpoints initiate the graceful shutdown of an HTTP/3 connection + // by sending a GOAWAY frame. + final QuicStreamWriter writer = controlStreamPair.localWriter(); + if (writer != null && quicConnection.isOpen()) { + try { + // We send here the largest pushId for which the peer has + // opened a stream. We won't process pushIds larger than that, and + // we will later cancel any pending push promises anyway. + final long lastProcessedPushId = largestPushId.get(); + final GoAwayFrame goAwayFrame = new GoAwayFrame(lastProcessedPushId); + final long size = goAwayFrame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + final var buf = ByteBuffer.allocate((int) size); + goAwayFrame.writeFrame(buf); + buf.flip(); + if (debug.on()) { + debug.log("Sending GOAWAY frame %s from client connection %s", goAwayFrame, this); + } + writer.scheduleForWriting(buf, false); + } catch (Exception e) { + // ignore - we couldn't send a GOAWAY + if (debug.on()) { + debug.log("Failed to send GOAWAY from client " + this, e); + } + Log.logError("Could not send a GOAWAY from client {0}", this); + Log.logError(e); + } + } + } + + @Override + public void close() { + try { + sendGoAway(); + } catch (IOException ioe) { + // log and ignore the failure + // failure to send a GOAWAY shouldn't prevent closing a connection + if (debug.on()) { + debug.log("failed to send a GOAWAY frame before initiating a close: " + ioe); + } + } + // TODO: ideally we should hava flushForClose() which goes all the way to terminator to flush + // streams and increasing the chances of GOAWAY being sent. + // check RFC-9114, section 5.3 which seems to allow including GOAWAY and CONNECTION_CLOSE + // frames in same packet (optionally) + close(Http3Error.H3_NO_ERROR, "H3 connection closed - no error"); + } + + void close(final Throwable throwable) { + close(H3_INTERNAL_ERROR, null, throwable); + } + + void close(final Http3Error error, final String message) { + if (error != H3_NO_ERROR) { + // construct a ProtocolException representing the connection termination cause + final ProtocolException cause = new ProtocolException(message); + close(error, message, cause); + } else { + close(error, message, null); + } + } + + void close(final Http3Error error, final String logMsg, + final Throwable closeCause) { + if (!markClosed()) { + // already closed, nothing to do + return; + } + if (debug.on()) { + debug.log("Closing HTTP/3 connection: %s %s %s", error, logMsg == null ? "" : logMsg, + closeCause == null ? "" : closeCause.toString()); + debug.log("State is: " + describeClosedState(closedState)); + } + exchanges.values().forEach(e -> e.recordError(closeCause)); + // close the underlying QUIC connection + connection.close(error.code(), logMsg, closeCause); + final TerminationCause tc = connection.quicConnection.terminationCause(); + assert tc != null : "termination cause is null"; + // close all HTTP streams + exchanges.values().forEach(exchange -> exchange.cancelImpl(tc.getCloseCause(), error)); + pushManager.cancelAllPromises(tc.getCloseCause(), error); + discardConnectionState(); + // No longer wait for reading HTTP/3 stream types: + // stop waiting on any stream for which we haven't received the stream + // type yet. + try { + var listener = remoteStreamListener; + if (listener != null) { + quicConnection.removeRemoteStreamListener(listener); + } + } finally { + client.connectionClosed(this); + } + if (!peerSettingsCF.isDone()) { + peerSettingsCF.completeExceptionally(tc.getCloseCause()); + } + } + + private void discardConnectionState() { + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + qpackDecoderStreams.stopSchedulers(); + qpackEncoderStreams.stopSchedulers(); + } + + private boolean markClosed() { + return markClosedState(CLOSED); + } + + void protocolError(IOException error) { + connectionError(error, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + void connectionError(Throwable throwable, Http3Error error) { + connectionError(null, throwable, error.code(), null); + } + + void connectionError(Http3Stream exchange, Throwable throwable, long errorCode, + String logMsg) { + final Optional error = Http3Error.fromCode(errorCode); + assert error.isPresent() : "not a HTTP3 error code: " + errorCode; + close(error.get(), logMsg, throwable); + } + + public String toString() { + return String.format("Http3Connection(%s)", connection()); + } + + private boolean finalStreamClosed() { + lock(); + try { + return this.finalStream && this.exchangeStreams.isEmpty() && this.reservedStreamCount.get() == 0; + } finally { + unlock(); + } + } + + /** + * Called by the {@link Http3ExchangeImpl} when the exchange is closed. + * + * @param streamId The request stream id + */ + void onExchangeClose(Http3ExchangeImpl exch, final long streamId) { + // we expect it to be a request/response stream + if (!(QuicStreams.isClientInitiated(streamId) && QuicStreams.isBidirectional(streamId))) { + throw new IllegalArgumentException("Not a client initiated bidirectional stream"); + } + if (this.exchangeStreams.remove(streamId) != null) { + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + decrementStreamsCount(exch, streamId); + exchanges.remove(streamId); + } + + if (finalStreamClosed()) { + // no more streams open on this connection. close the connection + if (Log.http3()) { + Log.logHttp3("Closing HTTP/3 connection {0} on final stream (streamId={1})", + quicConnectionTag(), Long.toString(streamId)); + } + // close will take care of canceling all pending push promises + // if any push promises are left pending + close(); + } else { + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection {0} left open: exchanged streamId={1} closed; " + + "finalStream={2}, exchangeStreams={3}, reservedStreamCount={4}", + quicConnectionTag(), Long.toString(streamId), finalStream, + exchangeStreams.size(), reservedStreamCount.get()); + } + lock(); + try { + var te = idleConnectionTimeoutEvent; + if (te == null && exchangeStreams.isEmpty()) { + te = idleConnectionTimeoutEvent = client.client().idleConnectionTimeout(HTTP_3) + .map(IdleConnectionTimeoutEvent::new).orElse(null); + if (te != null) { + client.client().registerTimer(te); + } + } + } finally { + unlock(); + } + } + } + + void decrementStreamsCount(Http3ExchangeImpl exch, long streamid) { + if (exch.deRegister()) { + debug.log("Unreference h3 stream: " + streamid); + client.client.h3StreamUnreference(); + } else { + debug.log("Already unreferenced h3 stream: " + streamid); + } + } + + // Called from Http3PushPromiseStream::start (via Http3ExchangeImpl) + void onPushPromiseStreamStarted(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to increment ref-counting if we wanted to + } + + // Called by Http3PushPromiseStream::close + void onPushPromiseStreamClosed(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to decrement ref-counting if we wanted to + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + } + + /** + * A class used to dispatch peer initiated unidirectional streams + * according to their HTTP/3 stream type. + * The type of an HTTP/3 unidirectional stream is determined by + * reading a variable length integer code off the stream, which + * indicates the type of stream. + * @see Http3Streams + */ + private final class Http3StreamDispatcher extends PeerUniStreamDispatcher { + Http3StreamDispatcher(QuicReceiverStream stream) { + super(stream); + } + + @Override + protected Logger debug() { return debug; } + + @Override + protected void onStreamAbandoned(QuicReceiverStream stream) { + if (debug.on()) debug.log("Stream " + stream.streamId() + " abandoned!"); + qpackDecoder.cancelStream(stream.streamId()); + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, controlStreamPair.futureReceiverStream()); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackDecoderStreams.futureReceiverStream()); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackEncoderStreams.futureReceiverStream()); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + Http3Connection.this.onPushStreamCreated(stream, pushId); + } + + // completes the given completable future with the given stream + private void complete(String description, QuicReceiverStream stream, CompletableFuture cf) { + debug.log("completing CF for %s with stream %s", description, stream.streamId()); + boolean completed = cf.complete(stream); + if (!completed) { + if (!cf.isCompletedExceptionally()) { + debug.log("CF for %s already completed with stream %s!", description, cf.resultNow().streamId()); + close(Http3Error.H3_STREAM_CREATION_ERROR, + "%s already created".formatted(description)); + } else { + debug.log("CF for %s already completed exceptionally!", description); + } + } + } + + /** + * Dispatches the given remote initiated unidirectional stream to the + * given Http3Connection after reading the stream type off the stream. + * + * @param conn the Http3Connection with which the stream is associated + * @param stream a newly opened remote unidirectional stream. + */ + static CompletableFuture dispatch(Http3Connection conn, QuicReceiverStream stream) { + assert stream.isRemoteInitiated(); + assert !stream.isBidirectional(); + var dispatcher = conn.new Http3StreamDispatcher(stream); + dispatcher.start(); + return dispatcher.dispatchCF(); + } + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForPoolCheckout() { + // must be done with "stateLock" held to co-ordinate idle connection management + lock(); + try { + cancelIdleShutdownEvent(); + // co-ordinate with the QUIC connection to prevent it from silently terminating + // a potentially idle transport + if (!quicConnection.connectionTerminator().tryReserveForUse()) { + // QUIC says the connection can't be used + return false; + } + // consider the reservation successful only if the connection's state hasn't moved + // to "being closed" + return isOpen() && finalStream == false; + } finally { + unlock(); + } + } + + /** + * Cancels any event that might have been scheduled to shutdown this connection. Must be called + * with the stateLock held. + */ + private void cancelIdleShutdownEvent() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + if (idleConnectionTimeoutEvent == null) return; + idleConnectionTimeoutEvent.cancel(); + idleConnectionTimeoutEvent = null; + } + + // An Idle connection is one that has no active streams + // and has not sent the final stream flag + final class IdleConnectionTimeoutEvent extends TimeoutEvent { + + // both cancelled and idleShutDownInitiated are to be accessed + // when holding the connection's lock + private boolean cancelled; + private boolean idleShutDownInitiated; + + IdleConnectionTimeoutEvent(Duration duration) { + super(duration); + } + + @Override + public void handle() { + boolean okToIdleTimeout; + lock(); + try { + if (cancelled || idleShutDownInitiated) { + return; + } + idleShutDownInitiated = true; + if (debug.on()) { + debug.log("H3 idle shutdown initiated"); + } + setFinalStream(); + okToIdleTimeout = finalStreamClosed(); + } finally { + unlock(); + } + if (okToIdleTimeout) { + if (debug.on()) { + debug.log("closing idle H3 connection"); + } + close(); + } + } + + /** + * Cancels this event. Should be called with stateLock held + */ + void cancel() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + // mark as cancelled to prevent potentially already triggered event from actually + // doing the shutdown + this.cancelled = true; + // cancel the timer to prevent the event from being triggered (if it hasn't already) + client.client().cancelTimer(this); + } + + @Override + public String toString() { + return "IdleConnectionTimeoutEvent, " + super.toString(); + } + + } + + /** + * This method is called when the peer opens a new stream. + * The stream can be unidirectional or bidirectional. + * + * @param stream the new stream + * @return always returns true (see {@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + private boolean onOpenRemoteStream(QuicReceiverStream stream) { + debug.log("on open remote stream: " + stream.streamId()); + if (stream instanceof QuicBidiStream bidi) { + // A server will never open a bidirectional stream + // with the client. A client opens a new bidirectional + // stream for each request/response exchange. + return onRemoteBidirectionalStream(bidi); + } else { + // Four types of unidirectional stream are defined: + // control stream, qpack encoder, qpack decoder, push + // promise stream + return onRemoteUnidirectionalStream(stream); + } + } + + /** + * This method is called when the peer opens a unidirectional stream. + * + * @param uni the unidirectional stream opened by the peer + * @return always returns true ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteUnidirectionalStream(QuicReceiverStream uni) { + assert !uni.isBidirectional(); + assert uni.isRemoteInitiated(); + if (!isOpen()) return false; + debug.log("dispatching unidirectional remote stream: " + uni.streamId()); + Http3StreamDispatcher.dispatch(this, uni).whenComplete((r, t)-> { + if (t!=null) this.dispatchingFailed(uni, t); + }); + return true; + } + + /** + * Called when the peer opens a bidirectional stream. + * On the client side, this method should never be called. + * + * @param bidi the new bidirectional stream opened by the + * peer. + * @return always returns false ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteBidirectionalStream(QuicBidiStream bidi) { + assert bidi.isRemoteInitiated(); + assert bidi.isBidirectional(); + + // From RFC 9114, Section 6.1: + // Clients MUST treat receipt of a server-initiated bidirectional + // stream as a connection error of type H3_STREAM_CREATION_ERROR + // [ unless such an extension has been negotiated]. + // We don't support any extension, so this is a connection error. + close(Http3Error.H3_STREAM_CREATION_ERROR, + "Bidirectional stream %s opened by server peer" + .formatted(bidi.streamId())); + return false; + } + + /** + * Called if the dispatch failed. + * + * @param reason the reason of the failure + */ + protected void dispatchingFailed(QuicReceiverStream uni, Throwable reason) { + debug.log("dispatching failed for streamId=%s: %s", uni.streamId(), reason); + close(H3_STREAM_CREATION_ERROR, "failed to dispatch remote stream " + uni.streamId(), reason); + } + + + /** + * Schedules sending of client settings. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + QuicStreamWriter sendSettings(QuicStreamWriter writer) { + try { + final SettingsFrame settings = QPACK.updateDecoderSettings(SettingsFrame.defaultRFCSettings()); + this.ourSettings = ConnectionSettings.createFrom(settings); + this.qpackDecoder.configure(ourSettings); + if (debug.on()) { + debug.log("Sending client settings %s for connection %s", this.ourSettings, this); + } + long size = settings.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + var buf = ByteBuffer.allocate((int) size); + settings.writeFrame(buf); + buf.flip(); + writer.scheduleForWriting(buf, false); + return writer; + } catch (IOException io) { + throw new CompletionException(io); + } + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @param writer the control stream writer + * @return the {@link QuicStreamWriter} passed as parameter + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer) { + try { + long maxPushId = pushManager.getMaxPushId(); + if (maxPushId > 0 && maxPushId > maxPushIdSent.get()) { + return sendMaxPushId(writer, maxPushId); + } else { + return writer; + } + } catch (IOException io) { + // will wrap the io exception in CompletionException, + // close the connection, and throw. + throw new CompletionException(io); + } + } + + // local control stream write loop + void lcsWriterLoop() { + // since we do not write much data on the control stream + // we don't check for credit and always directly buffer + // the data to send in the writer. Therefore, there is + // nothing to do in the control stream writer loop. + // + // When more credit is available, check if we need + // to send maxpushid; + if (maxPushIdSent.get() < pushManager.getMaxPushId()) { + var writer = controlStreamPair.localWriter(); + if (writer != null && writer.connected()) { + sendMaxPushId(writer); + } + } + } + + void controlStreamFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Control stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9114, section 6.2.1: + // If either control stream is closed at any point, + // this MUST be treated as a connection error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "control stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since control stream " + stream.mode() + + " failed", throwable); + } + } + close(throwable); + } + + /** + * This method is called to process bytes received on the peer + * control stream. + * + * @param buffer the bytes received + */ + private void processPeerControlBytes(final ByteBuffer buffer) { + debug.log("received server control: %s bytes", buffer.remaining()); + controlFramesDecoder.submit(buffer); + Http3Frame frame; + while ((frame = controlFramesDecoder.poll()) != null) { + final long frameType = frame.type(); + debug.log("server control frame: %s", Http3FrameType.asString(frameType)); + if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + final Http3Error error = Http3Error.fromCode(malformed.getErrorCode()) + .orElse(H3_INTERNAL_ERROR); + close(error, malformed.getMessage()); + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + final boolean settingsRcvd = this.settingsFrameReceived; + if ((frameType == SettingsFrame.TYPE && settingsRcvd) + || !this.frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now, we either use H3_FRAME_UNEXPECTED + // or H3_MISSING_SETTINGS for the connection error, depending on the context. + // + // RFC-9114, section 4.1: Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + // + // RFC-9114, section 6.2.1: If the first frame of the control stream + // is any other frame type, this MUST be treated as a connection error of + // type H3_MISSING_SETTINGS. + final String logMsg = "unexpected (order of) frame type: " + unexpectedFrameType + + " on control stream"; + if (!settingsRcvd) { + close(Http3Error.H3_MISSING_SETTINGS, logMsg); + } else { + close(Http3Error.H3_FRAME_UNEXPECTED, logMsg); + } + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + if (frame instanceof SettingsFrame settingsFrame) { + this.settingsFrameReceived = true; + this.peerSettings = ConnectionSettings.createFrom(settingsFrame); + if (debug.on()) { + debug.log("Received peer settings %s for connection %s", this.peerSettings, this); + } + peerSettingsCF.completeAsync(() -> peerSettings, + client.client().theExecutor().safeDelegate()); + // We can only initialize encoder's DT only when we get Settings frame with all parameters + qpackEncoder().configure(peerSettings); + } + if (frame instanceof CancelPushFrame cancelPush) { + pushManager.cancelPushPromise(cancelPush.getPushId(), null, CancelPushReason.CANCEL_RECEIVED); + } + if (frame instanceof GoAwayFrame goaway) { + handleIncomingGoAway(goaway); + } + if (frame instanceof PartialFrame partial) { + var payloadBytes = controlFramesDecoder.readPayloadBytes(); + debug.log("added %s bytes to %s", + payloadBytes == null ? 0 : Utils.remaining(payloadBytes), + frame); + if (partial.remaining() == 0) { + this.frameOrderVerifier.completed(frame); + } else if (payloadBytes == null || payloadBytes.isEmpty()) { + break; + } + // only reserved frames reach here; just drop the payload + } else { + this.frameOrderVerifier.completed(frame); + } + if (controlFramesDecoder.eof()) { + break; + } + } + if (controlFramesDecoder.eof()) { + close(H3_CLOSED_CRITICAL_STREAM, "EOF reached while reading server control stream"); + } + } + + /** + * Called when a new push promise stream is created by the peer. + * + * @apiNote this method gives an opportunity to cancel the stream + * before reading the pushId, if it is known that no push + * will be accepted anyway. + * + * @param pushStream the new push promise stream + * @param pushId or -1 if the pushId is not available yet + */ + private void onPushStreamCreated(QuicReceiverStream pushStream, long pushId) { + assert pushStream.isRemoteInitiated(); + assert !pushStream.isBidirectional(); + + onPushPromiseStream(pushStream, pushId); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + pushManager.onPushPromiseStream(pushStream, pushId); + } + + /** + * This method is called by the {@link Http3PushManager} to figure out whether + * a push stream or a push promise should be processed, with respect to the + * GOAWAY state. Any pushId larger than what was sent in the GOAWAY frame + * should be cancelled /rejected. + * + * @param pushStream a push stream (may be null if not yet materialized) + * @param pushId a pushId, must be > 0 + * @return true if the pushId can be processed + */ + boolean acceptLargerPushPromise(QuicReceiverStream pushStream, long pushId) { + // if GOAWAY has been sent, just cancel the push promise + // otherwise - track this as the maxPushId that will be + // sent in GOAWAY + if (checkMaxPushId(pushId) != null) return false; // connection will be closed + while (true) { + long largestPushId = this.largestPushId.get(); + if ((closedState & GOAWAY_SENT) == GOAWAY_SENT) { + if (pushId >= largestPushId) { + if (pushStream != null) { + pushStream.requestStopSending(H3_NO_ERROR.code()); + } + pushManager.cancelPushPromise(pushId, null, CancelPushReason.PUSH_CANCELLED); + return false; + } + } + if (pushId <= largestPushId) break; + if (!this.largestPushId.compareAndSet(largestPushId, pushId)) continue; + if ((closedState & GOAWAY_SENT) == 0) break; + } + // If we reach here, then either GOAWAY has been sent with a largestPushId >= pushId, + // or GOAWAY has not been sent yet. + return true; + } + + QueuingStreamPair createEncoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, quicConnection, + encoderReceiver, this::onEncoderStreamsFailed, debug); + } + + private void onEncoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Encoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK encoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack encoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + QueuingStreamPair createDecoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_DECODER, quicConnection, + encoderReceiver, this::onDecoderStreamsFailed, debug); + } + + private void onDecoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Decoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK decoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack decoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + // This method never returns anything: it always throws + private T exceptionallyAndClose(Throwable t) { + try { + return exceptionally(t); + } finally { + close(t); + } + } + + // This method never returns anything: it always throws + private T exceptionally(Throwable t) { + try { + debug.log(t.getMessage(), t); + throw t; + } catch (RuntimeException | Error r) { + throw r; + } catch (ExecutionException x) { + throw new CompletionException(x.getMessage(), x.getCause()); + } catch (Throwable e) { + throw new CompletionException(e.getMessage(), e); + } + } + + Decoder qpackDecoder() { + return qpackDecoder; + } + + Encoder qpackEncoder() { + return qpackEncoder; + } + + /** + * {@return the settings, sent by the peer, for this connection. If none is present, due to the SETTINGS + * frame not yet arriving from the peer, this method returns {@link Optional#empty()}} + */ + Optional getPeerSettings() { + return Optional.ofNullable(this.peerSettings); + } + + private void handleIncomingGoAway(final GoAwayFrame incomingGoAway) { + final long quicStreamId = incomingGoAway.getTargetId(); + if (debug.on()) { + debug.log("Received GOAWAY %s", incomingGoAway); + } + // ensure request stream id is a bidirectional stream originating from the client. + // RFC-9114, section 7.2.6: A client MUST treat receipt of a GOAWAY frame containing + // a stream ID of any other type as a connection error of type H3_ID_ERROR. + if (!(QuicStreams.isClientInitiated(quicStreamId) + && QuicStreams.isBidirectional(quicStreamId))) { + close(Http3Error.H3_ID_ERROR, "Invalid stream id in GOAWAY frame"); + return; + } + boolean validStreamId = false; + long current = lowestGoAwayReceipt.get(); + while (current == -1 || quicStreamId <= current) { + if (lowestGoAwayReceipt.compareAndSet(current, quicStreamId)) { + validStreamId = true; + break; + } + current = lowestGoAwayReceipt.get(); + } + if (!validStreamId) { + // the request stream id received in the GOAWAY frame is greater than the one received + // in some previous GOAWAY frame. This isn't allowed by spec. + // RFC-9114, section 5.2: An endpoint MAY send multiple GOAWAY frames indicating + // different identifiers, but the identifier in each frame MUST NOT be greater than + // the identifier in any previous frame, ... Receiving a GOAWAY containing a larger + // identifier than previously received MUST be treated as a connection error of + // type H3_ID_ERROR. + close(Http3Error.H3_ID_ERROR, "Invalid stream id in newer GOAWAY frame"); + return; + } + markReceivedGoAway(); + // mark a state on this connection to let it know that no new streams are allowed on this + // connection. + // RFC-9114, section 5.2: Endpoints MUST NOT initiate new requests or promise new pushes on + // the connection after receipt of a GOAWAY frame from the peer. + setFinalStream(); + if (debug.on()) { + debug.log("Connection will no longer allow new streams due to receipt of GOAWAY" + + " from peer"); + } + handlePeerUnprocessedStreams(quicStreamId); + if (finalStreamClosed()) { + close(Http3Error.H3_NO_ERROR, "GOAWAY received"); + } + } + + private void handlePeerUnprocessedStreams(final long leastUnprocessedStreamId) { + this.exchanges.forEach((id, exchange) -> { + if (id >= leastUnprocessedStreamId) { + // close the exchange as unprocessed + client.client().theExecutor().execute(exchange::closeAsUnprocessed); + } + }); + } + + private boolean isMarked(int state, int mask) { + return (state & mask) == mask; + } + + private boolean markSentGoAway() { + return markClosedState(GOAWAY_SENT); + } + + private boolean markReceivedGoAway() { + return markClosedState(GOAWAY_RECEIVED); + } + + private boolean markClosedState(int flag) { + int state, desired; + do { + state = closedState; + if ((state & flag) == flag) return false; + desired = state | flag; + } while (!CLOSED_STATE.compareAndSet(this, state, desired)); + return true; + } + + String describeClosedState(int state) { + if (state == 0) return "active"; + String desc = null; + if (isMarked(state, GOAWAY_SENT)) { + if (desc == null) desc = "goaway-sent"; + else desc += "+goaway-sent"; + } + if (isMarked(state, GOAWAY_RECEIVED)) { + if (desc == null) desc = "goaway-rcvd"; + else desc += "+goaway-rcvd"; + } + if (isMarked(state, CLOSED)) { + if (desc == null) desc = "quic-closed"; + else desc += "+quic-closed"; + } + return desc != null ? desc : "0x" + Integer.toHexString(state); + } + + // PushPromise handling + // ==================== + + /** + * {@return a new PushId for the given pushId} + * @param pushId the pushId + */ + PushId newPushId(long pushId) { + return new Http3PushId(pushId, connection.label()); + } + + /** + * Called when a pushId needs to be cancelled. + * @param pushId the pushId to cancel + * @param cause the cause (may be {@code null}). + */ + void pushCancelled(long pushId, Throwable cause) { + pushManager.cancelPushPromise(pushId, cause, CancelPushReason.PUSH_CANCELLED); + } + + /** + * Called if a PushPromiseFrame is received by an exchange that doesn't have any + * {@link java.net.http.HttpResponse.PushPromiseHandler}. The pushId will be + * cancelled, unless it's already been accepted by another exchange. + * + * @param pushId the pushId + */ + void noPushHandlerFor(long pushId) { + pushManager.cancelPushPromise(pushId, null, CancelPushReason.NO_HANDLER); + } + + boolean acceptPromises() { + return exchanges.values().stream().anyMatch(Http3ExchangeImpl::acceptPushPromise); + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + *

      + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * @apiNote + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + *

      + * Callers should not forward the pushId to a {@link + * java.net.http.HttpResponse.PushPromiseHandler} unless the future is completed + * with {@code true} + * + * @param pushId the pushId + */ + CompletableFuture whenPushAccepted(long pushId) { + return pushManager.whenAccepted(pushId); + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + return pushManager.onPushPromiseFrame(exchange, pushId, promiseHeaders); + } + + /** + * Checks whether a MAX_PUSH_ID frame should be sent. + */ + void checkSendMaxPushId() { + pushManager.checkSendMaxPushId(); + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer, long maxPushId) throws IOException { + debug.log("Sending max push id frame with max push id set to " + maxPushId); + final MaxPushIdFrame maxPushIdFrame = new MaxPushIdFrame(maxPushId); + final long frameSize = maxPushIdFrame.size(); + assert frameSize >= 0 && frameSize < Integer.MAX_VALUE; + final ByteBuffer buf = ByteBuffer.allocate((int) frameSize); + maxPushIdFrame.writeFrame(buf); + buf.flip(); + if (writer.credit() > buf.remaining()) { + long previous; + do { + previous = maxPushIdSent.get(); + if (previous >= maxPushId) return writer; + } while (!maxPushIdSent.compareAndSet(previous, maxPushId)); + writer.scheduleForWriting(buf, false); + } + return writer; + } + + /** + * Send a MAX_PUSH_ID frame on the control stream with the given {@code maxPushId} + * + * @param maxPushId the new maxPushId + * + * @throws IOException if the pushId could not be sent + */ + void sendMaxPushId(long maxPushId) throws IOException { + sendMaxPushId(controlStreamPair.localWriter(), maxPushId); + } + + /** + * Sends a CANCEL_PUSH frame for the given {@code pushId}. + * If not null, the cause may indicate why the push is cancelled. + * + * @apiNote the cause is only used for logging + * + * @param pushId the pushId to cancel + * @param cause the reason for cancelling, may be {@code null} + */ + void sendCancelPush(long pushId, Throwable cause) { + // send CANCEL_PUSH frame here + if (debug.on()) { + if (cause != null) { + debug.log("Push Promise %s cancelled: %s", pushId, cause.getMessage()); + } else { + debug.log("Push Promise %s cancelled", pushId); + } + } + try { + CancelPushFrame cancelPush = new CancelPushFrame(pushId); + long size = cancelPush.size(); + // frame should contain type, length, pushId + assert size <= 3 * VariableLengthEncoder.MAX_INTEGER_LENGTH; + ByteBuffer buffer = ByteBuffer.allocate((int) size); + cancelPush.writeFrame(buffer); + controlStreamPair.localWriter().scheduleForWriting(buffer, false); + } catch (IOException io) { + debug.log("Failed to cancel pushId: " + pushId); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return checkMaxPushId(pushId, maxPushIdSent.get()); + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + private IOException checkMaxPushId(long pushId, long max) { + if (pushId >= max) { + var io = new ProtocolException("Max pushId exceeded (%s >= %s)".formatted(pushId, max)); + connectionError(io, Http3Error.H3_ID_ERROR); + return io; + } + return null; + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + public long getMinPushId() { + return pushManager.getMinPushId(); + } + + private static final VarHandle CLOSED_STATE; + static { + try { + CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http3Connection.class, "closedState", int.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java new file mode 100644 index 00000000000..eaacd8213cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.common.Logger; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class encapsulate the HTTP/3 connection pool managed + * by an instance of {@link Http3ClientImpl}. + */ +class Http3ConnectionPool { + /* Map key is "scheme:host:port" */ + private final Map advertised = new ConcurrentHashMap<>(); + /* Map key is "scheme:host:port" */ + private final Map unadvertised = new ConcurrentHashMap<>(); + + private final Logger debug; + Http3ConnectionPool(Logger logger) { + this.debug = Objects.requireNonNull(logger); + } + + // https:: + String connectionKey(HttpRequestImpl request) { + var uri = request.uri(); + var scheme = uri.getScheme().toLowerCase(Locale.ROOT); + var host = uri.getHost(); + var port = uri.getPort(); + assert scheme.equals("https"); + if (port < 0) port = 443; // https + return String.format("%s:%s:%d", scheme, host, port); + } + + private Http3Connection lookupUnadvertised(String key, Http3DiscoveryMode discoveryMode) { + var unadvertisedConn = unadvertised.get(key); + if (unadvertisedConn == null) return null; + if (discoveryMode == ANY) return unadvertisedConn; + if (discoveryMode == ALT_SVC) return null; + + assert discoveryMode == HTTP_3_URI_ONLY : String.valueOf(discoveryMode); + + // Double check that if there is an alt service, it has same origin. + final var altService = Optional.ofNullable(unadvertisedConn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + + if (altService == null || altService.originHasSameAuthority()) { + return unadvertisedConn; + } + + // We should never come here. + assert false : "unadvertised connection with different origin: %s -> %s" + .formatted(key, altService); + return null; + } + + Http3Connection lookupFor(HttpRequestImpl request) { + var discoveryMode = request.http3Discovery(); + var key = connectionKey(request); + + Http3Connection unadvertisedConn = null; + // If not ALT_SVC, we can use unadvertised connections + if (discoveryMode != ALT_SVC) { + unadvertisedConn = lookupUnadvertised(key, discoveryMode); + if (unadvertisedConn != null && discoveryMode == HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + } + + // Then see if we have a connection which was advertised. + var advertisedConn = advertised.get(key); + // We can use it for HTTP3_URI_ONLY too if it has same origin + if (advertisedConn != null) { + final var altService = advertisedConn.connection() + .getSourceAltService().orElse(null); + assert altService != null && altService.wasAdvertised(); + switch (discoveryMode) { + case ANY -> { + return advertisedConn; + } + case ALT_SVC -> { + if (debug.on()) { + debug.log("HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + case HTTP_3_URI_ONLY -> { + if (altService != null && altService.originHasSameAuthority()) { + if (debug.on()) { + debug.log("Same authority HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + } + } + } + + if (unadvertisedConn != null) { + assert discoveryMode != ALT_SVC; + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + + // do not log here: this produces confusing logs as this method + // can be called several times when trying to establish a + // connection, when no connection is found in the pool + return null; + } + + Http3Connection putIfAbsent(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.putIfAbsent(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.putIfAbsent(key, c); + } + + Http3Connection put(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.put(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.put(key, c); + } + + boolean remove(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + boolean remUndavertised = unadvertised.remove(key, c); + assert !remUndavertised + : "advertised connection found in unadvertised pool for " + key; + return advertised.remove(key, c); + } + + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.remove(key, c); + } + + void clear() { + advertised.clear(); + unadvertised.clear(); + } + + java.util.stream.Stream values() { + return java.util.stream.Stream.concat( + advertised.values().stream(), + unadvertised.values().stream()); + } + +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java new file mode 100644 index 00000000000..41a4a84958a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java @@ -0,0 +1,1806 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiPredicate; + +import jdk.internal.net.http.PushGroup.Acceptor; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import static jdk.internal.net.http.http3.ConnectionSettings.UNLIMITED_MAX_FIELD_SECTION_SIZE; + +/** + * This class represents an HTTP/3 Request/Response stream. + */ +final class Http3ExchangeImpl extends Http3Stream { + + private static final String COOKIE_HEADER = "Cookie"; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpRequestImpl request; + private final BodyPublisher requestPublisher; + private final HttpHeadersBuilder responseHeadersBuilder; + private final HeadersConsumer rspHeadersConsumer; + private final HttpHeaders requestPseudoHeaders; + private final HeaderFrameReader headerFrameReader; + private final HeaderFrameWriter headerFrameWriter; + private final Decoder qpackDecoder; + private final Encoder qpackEncoder; + private final AtomicReference errorRef; + private final CompletableFuture requestBodyCF; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnRequestStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final SequentialScheduler writeScheduler = + SequentialScheduler.lockingScheduler(this::sendQuicData); + private final List> response_cfs = new ArrayList<>(5); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ReentrantLock response_cfs_lock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForRequestResponseStream(); + + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + private final QuicBidiStream stream; + private final QuicStreamReader reader; + private final QuicStreamWriter writer; + volatile boolean closed; + volatile RequestSubscriber requestSubscriber; + volatile HttpResponse.BodySubscriber pendingResponseSubscriber; + volatile HttpResponse.BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean requestSent; + volatile boolean responseReceived; + volatile long requestContentLen; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + volatile boolean deRegistered; + private String dbgTag = null; + private final AtomicLong sentQuicBytes = new AtomicLong(); + + Http3ExchangeImpl(final Http3Connection connection, final Exchange exchange, + final QuicBidiStream stream) { + super(exchange); + this.errorRef = new AtomicReference<>(); + this.requestBodyCF = new MinimalFuture<>(); + this.connection = connection; + this.request = exchange.request(); + this.requestPublisher = request.requestPublisher; // may be null + this.responseHeadersBuilder = new HttpHeadersBuilder(); + this.rspHeadersConsumer = new HeadersConsumer(ValidatingHeadersConsumer.Context.RESPONSE); + this.qpackDecoder = connection.qpackDecoder(); + this.qpackEncoder = connection.qpackEncoder(); + this.headerFrameReader = qpackDecoder.newHeaderFrameReader(rspHeadersConsumer); + this.headerFrameWriter = qpackEncoder.newHeaderFrameWriter(); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); + this.stream = stream; + this.reader = stream.connectReader(readScheduler); + this.writer = stream.connectWriter(writeScheduler); + if (debug.on()) debug.log("Http3ExchangeImpl created"); + } + + public void start() { + if (exchange.pushGroup != null) { + connection.checkSendMaxPushId(); + } + if (Log.http3()) { + Log.logHttp3("Starting HTTP/3 exchange for {0}/streamId={1} ({2} #{3})", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + request, Long.toString(exchange.multi.id)); + } + this.reader.start(); + } + + boolean acceptPushPromise() { + return exchange.pushGroup != null; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3ExchangeImpl(" + ctag + ", streamId=" + sid + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + Http3Connection http3Connection() { + return connection; + } + + void recordError(Throwable closeCause) { + errorRef.compareAndSet(null, closeCause); + } + + private sealed class HeadersConsumer extends StreamHeadersConsumer permits PushHeadersConsumer { + + private HeadersConsumer(Context context) { + super(context); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return headerFrameReader; + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return responseHeadersBuilder; + } + + @Override + protected final Decoder qpackDecoder() { + return qpackDecoder; + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + + @Override + String headerFieldType() { + return "RESPONSE HEADER FIELD"; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed response: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + handleResponse(); + } + + @Override + public final long streamId() { + return stream.streamId(); + } + + } + + private final class PushHeadersConsumer extends HeadersConsumer { + volatile PushPromiseState state; + + private PushHeadersConsumer() { + super(Context.REQUEST); + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return state.headersBuilder(); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return state.reader(); + } + + @Override + String headerFieldType() { + return "PUSH REQUEST HEADER FIELD"; + } + + void resetDone() { + if (debug.on()) { + debug.log("Push request builder cleared."); + } + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push request: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + try { + if (exchange.pushGroup == null) { + long pushId = state.frame().getPushId(); + connection.noPushHandlerFor(pushId); + reset(); + } else { + handlePromise(this); + } + } catch (IOException io) { + cancelPushPromise(state, io); + } + } + + public void setState(PushPromiseState state) { + this.state = state; + } + } + + // TODO: this is also defined on Stream + // + private static boolean hasProxyAuthorization(HttpHeaders headers) { + return headers.firstValue("proxy-authorization") + .isPresent(); + } + + // TODO: this is also defined on Stream + // + // Determines whether we need to build a new HttpHeader object. + // + // Ideally we should pass the filter to OutgoingHeaders refactor the + // code that creates the HeaderFrame to honor the filter. + // We're not there yet - so depending on the filter we need to + // apply and the content of the header we will try to determine + // whether anything might need to be filtered. + // If nothing needs filtering then we can just use the + // original headers. + private static boolean needsFiltering(HttpHeaders headers, + BiPredicate filter) { + if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) { + // we're either connecting or proxying + // slight optimization: we only need to filter out + // disabled schemes, so if there are none just + // pass through. + return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER) + && hasProxyAuthorization(headers); + } else { + // we're talking to a server, either directly or through + // a tunnel. + // Slight optimization: we only need to filter out + // proxy authorization headers, so if there are none just + // pass through. + return hasProxyAuthorization(headers); + } + } + + // TODO: this is also defined on Stream + // + private HttpHeaders filterHeaders(HttpHeaders headers) { + HttpConnection conn = connection(); + BiPredicate filter = conn.headerFilter(request); + if (needsFiltering(headers, filter)) { + return HttpHeaders.of(headers.map(), filter); + } + return headers; + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + final MinimalFuture completable = MinimalFuture.completedFuture(null); + return completable.thenApply(_ -> this.sendHeaders()); + } + + private Http3ExchangeImpl sendHeaders() { + assert stream != null; + assert writer != null; + + if (debug.on()) debug.log("H3 sendHeaders"); + if (Log.requests()) { + Log.logRequest(request.toString()); + } + if (requestPublisher != null) { + requestContentLen = requestPublisher.contentLength(); + } else { + requestContentLen = 0; + } + + Throwable t = errorRef.get(); + if (t != null) { + if (debug.on()) debug.log("H3 stream already cancelled, headers not sent: %s", (Object) t); + if (t instanceof CompletionException ce) throw ce; + throw new CompletionException(t); + } + + HttpHeadersBuilder h = request.getSystemHeadersBuilder(); + if (requestContentLen > 0) { + h.setHeader("content-length", Long.toString(requestContentLen)); + } + HttpHeaders sysh = filterHeaders(h.build()); + HttpHeaders userh = filterHeaders(request.getUserHeaders()); + // Filter context restricted from userHeaders + userh = HttpHeaders.of(userh.map(), Utils.ACCEPT_ALL); + Utils.setUserAuthFlags(request, userh); + + // Don't override Cookie values that have been set by the CookieHandler. + final HttpHeaders uh = userh; + BiPredicate overrides = + (k, v) -> COOKIE_HEADER.equalsIgnoreCase(k) + || uh.firstValue(k).isEmpty(); + + // Filter any headers from systemHeaders that are set in userHeaders + // except for "Cookie:" - user cookies will be appended to system + // cookies + sysh = HttpHeaders.of(sysh.map(), overrides); + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 HEADERS FRAME (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", requestPseudoHeaders); + Log.dumpHeaders(sb, " ", sysh); + Log.dumpHeaders(sb, " ", userh); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + final Optional peerSettings = connection.getPeerSettings(); + // It's possible that the peer settings hasn't yet arrived, in which case we use the + // default of "unlimited" header size limit and proceed with sending the request. As per + // RFC-9114, section 7.2.4.2, this is allowed: All settings begin at an initial value. Each + // endpoint SHOULD use these initial values to send messages before the peer's SETTINGS frame + // has arrived, as packets carrying the settings can be lost or delayed. + // When the SETTINGS frame arrives, any settings are changed to their new values. This + // removes the need to wait for the SETTINGS frame before sending messages. + final long headerSizeLimit = peerSettings.isEmpty() ? UNLIMITED_MAX_FIELD_SECTION_SIZE + : peerSettings.get().maxFieldSectionSize(); + if (headerSizeLimit != UNLIMITED_MAX_FIELD_SECTION_SIZE) { + // specific limit has been set on the header size for this connection. + // we compute the header size and ensure that it doesn't exceed that limit + final long computedHeaderSize = computeHeaderSize(requestPseudoHeaders, sysh, userh); + if (computedHeaderSize > headerSizeLimit) { + // RFC-9114, section 4.2.2: An implementation that has received this parameter + // SHOULD NOT send an HTTP message header that exceeds the indicated size. + // we fail the request. + throw new CompletionException(new ProtocolException("Request headers size" + + " exceeds limit set by peer")); + } + } + List buffers = qpackEncoder.encodeHeaders(headerFrameWriter, streamId(), + 1024, requestPseudoHeaders, sysh, userh); + HeadersFrame headersFrame = new HeadersFrame(Utils.remaining(buffers)); + ByteBuffer buffer = ByteBuffer.allocate(headersFrame.headersSize()); + headersFrame.writeHeaders(buffer); + buffer.flip(); + long sentBytes = 0; + try { + boolean hasNoBody = requestContentLen == 0; + int last = buffers.size() - 1; + int toSend = buffer.remaining(); + if (last < 0) { + writer.scheduleForWriting(buffer, hasNoBody); + } else { + writer.queueForWriting(buffer); + } + sentBytes += toSend; + for (int i = 0; i <= last; i++) { + var nextBuffer = buffers.get(i); + toSend = nextBuffer.remaining(); + if (i == last) { + writer.scheduleForWriting(nextBuffer, hasNoBody); + } else { + writer.queueForWriting(nextBuffer); + } + sentBytes += toSend; + } + } catch (QPackException qe) { + if (qe.isConnectionError()) { + // close the connection + connection.close(qe.http3Error(), "QPack error", qe.getCause()); + } + // fail the request + throw new CompletionException(qe.getCause()); + } catch (IOException io) { + throw new CompletionException(io); + } finally { + if (sentBytes != 0) sentQuicBytes.addAndGet(sentBytes); + } + return this; + } + + private static long computeHeaderSize(final HttpHeaders... headers) { + // RFC-9114, section 4.2.2 states: The size of a field list is calculated based on + // the uncompressed size of fields, including the length of the name and value in bytes + // plus an overhead of 32 bytes for each field. + final int OVERHEAD_BYTES_PER_FIELD = 32; + long computedHeaderSize = 0; + for (final HttpHeaders h : headers) { + for (final Map.Entry> entry : h.map().entrySet()) { + try { + computedHeaderSize = Math.addExact(computedHeaderSize, + entry.getKey().getBytes(StandardCharsets.US_ASCII).length); + for (final String v : entry.getValue()) { + computedHeaderSize = Math.addExact(computedHeaderSize, + v.getBytes(StandardCharsets.US_ASCII).length); + } + computedHeaderSize = Math.addExact(computedHeaderSize, OVERHEAD_BYTES_PER_FIELD); + } catch (ArithmeticException ae) { + // overflow, no point trying to compute further, return MAX_VALUE + return Long.MAX_VALUE; + } + } + } + return computedHeaderSize; + } + + + @Override + CompletableFuture> sendBodyAsync() { + return sendBodyImpl().thenApply((e) -> this); + } + + CompletableFuture sendBodyImpl() { + requestBodyCF.whenComplete((v, t) -> requestSent()); + try { + if (debug.on()) debug.log("H3 sendBodyImpl"); + if (requestPublisher != null && requestContentLen != 0) { + final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen); + requestPublisher.subscribe(requestSubscriber = subscriber); + } else { + // there is no request body, therefore the request is complete, + // END_STREAM has already sent with outgoing headers + requestBodyCF.complete(null); + } + } catch (Throwable t) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + requestBodyCF.completeExceptionally(t); + } + return requestBodyCF; + } + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Reference response body for h3 stream: " + streamId()); + } + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Unreference response body for h3 stream: " + streamId()); + } + client().h3StreamUnreference(); + } + } + + final class Http3StreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3StreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + + @Override + protected void logComplete(Throwable error) { + if (error == null) { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body successfully completed for: " + request + + " #" + exchange.multi.id); + } + } else { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body exceptionally completed for: " + + request + " (" + error + ")" + + " #" + exchange.multi.id); + } + } + } + } + + + @Override + Http3StreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + if (debug.on()) debug.log("Creating body subscriber"); + Http3StreamResponseSubscriber subscriber = + new Http3StreamResponseSubscriber<>(handler.apply(response)); + return subscriber; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + if (Log.trace()) { + Log.logTrace("Reading body on stream {0}", streamId()); + } + if (debug.on()) debug.log("Getting BodySubscriber for: " + response); + Http3StreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + return MinimalFuture.failedFuture(t); + } + } + + @Override + CompletableFuture ignoreBody() { + try { + if (debug.on()) debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + if (Log.trace()) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + } + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + if (debug.on()) debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + long streamid = streamId(); + if (debug.on()) debug.log("Released stream %d", streamid); + // remove this stream from the Http2Connection map. + connection.onExchangeClose(this, streamid); + } + + @Override + void completed() { + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (!requestBodyCF.isDone()) { + // complete requestBodyCF before cancelling subscription + requestBodyCF.completeExceptionally(firstError); // we may be sending the body... + var requestSubscriber = this.requestSubscriber; + if (requestSubscriber != null) { + cancel(requestSubscriber.subscription.get()); + } + } + var responseBodyCF = this.responseBodyCF; + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null && stream.sendingState().isSending()) { + // no use reset if already closed. + var cause = Utils.getCompletionCause(firstError); + if (!(cause instanceof EOFException)) { + if (debug.on()) + debug.log("sending reset %s", error); + stream.reset(error.code()); + } + } + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + errorRef.compareAndSet(null, ex); + if (debug.on()) + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + // cancel subscription and ignore errors in order to continue with + // the cancel/close sequence. + private void cancel(Subscription subscription) { + if (subscription == null) return; + try { subscription.cancel(); } + catch (Throwable t) { + debug.log("Unexpected exception thrown by Subscription::cancel", t); + if (Log.errors()) { + Log.logError("Unexpected exception thrown by Subscription::cancel: " + t); + Log.logError(t); + } + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + CompletableFuture cf; + // The code below deals with race condition that can be caused when + // completeResponse() is being called before getResponseAsync() + response_cfs_lock.lock(); + try { + if (!response_cfs.isEmpty()) { + // This CompletableFuture was created by completeResponse(). + // it will be already completed, unless the expect continue + // timeout fired + cf = response_cfs.get(0); + if (cf.isDone()) { + cf = response_cfs.remove(0); + } + + // if we find a cf here it should be already completed. + // finding a non completed cf should not happen. just assert it. + assert cf.isDone() || request.expectContinue && expectTimeoutRaised() + : "Removing uncompleted response: could cause code to hang!"; + } else { + // getResponseAsync() is called first. Create a CompletableFuture + // that will be completed by completeResponse() when + // completeResponse() is called. + cf = new MinimalFuture<>(); + response_cfs.add(cf); + } + } finally { + response_cfs_lock.unlock(); + } + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + if (Log.trace()) { + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + } + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(Utils.getCompletionCause(e))); + } + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + /** + * Completes the first uncompleted CF on list, and removes it. If there is no + * uncompleted CF then creates one (completes it) and adds to list + */ + void completeResponse(Response resp) { + if (debug.on()) debug.log("completeResponse: %s", resp); + response_cfs_lock.lock(); + try { + CompletableFuture cf; + int cfs_len = response_cfs.size(); + for (int i = 0; i < cfs_len; i++) { + cf = response_cfs.get(i); + if (!cf.isDone() && !expectTimeoutRaised()) { + if (Log.trace()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + response_cfs.remove(cf); + cf.complete(resp); + return; + } else if (expectTimeoutRaised()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + // The Request will be removed in getResponseAsync() + cf.complete(resp); + return; + } // else we found the previous response: just leave it alone. + } + cf = MinimalFuture.completedFuture(resp); + if (Log.trace()) { + Log.logTrace("Created completed future (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Adding completed responseCF(0) with response headers"); + response_cfs.add(cf); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void expectContinueFailed(int rcode) { + // Have to mark request as sent, due to no request body being sent in the + // event of a 417 Expectation Failed or some other non 100 response code + requestSent(); + } + + // methods to update state and remove stream when finished + + void requestSent() { + stateLock.lock(); + try { + requestSent0(); + } finally { + stateLock.unlock(); + } + } + + private void requestSent0() { + assert stateLock.isHeldByCurrentThread(); + requestSent = true; + if (responseReceived) { + if (debug.on()) debug.log("requestSent: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("requestSent: streamid=%d but response not received", streamId()); + } + } + } + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (requestSent) { + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("responseReceived: streamid=%d but request not sent", streamId()); + } + } + } + + /** + * Same as {@link #completeResponse(Response)} above but for errors + */ + void completeResponseExceptionally(Throwable t) { + response_cfs_lock.lock(); + try { + // use index to avoid ConcurrentModificationException + // caused by removing the CF from within the loop. + for (int i = 0; i < response_cfs.size(); i++) { + CompletableFuture cf = response_cfs.get(i); + if (!cf.isDone()) { + response_cfs.remove(i); + cf.completeExceptionally(t); + return; + } + } + response_cfs.add(MinimalFuture.failedFuture(t)); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + /** + * An unprocessed exchange is one that hasn't been processed by a peer. The local end of the + * connection would be notified about such exchanges in either of the following 2 ways: + *

        + *
      • when it receives a GOAWAY frame with a stream id that tells which + * exchanges have been unprocessed. + *
      • or when a particular request's stream is reset with the H3_REQUEST_REJECTED error code. + *
      + *

      + * This method is called on such unprocessed exchanges and the implementation of this method + * will arrange for the request, corresponding to this exchange, to be retried afresh. + */ + void closeAsUnprocessed() { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; + // We arrange for the request to be retried on a new connection as allowed + // by RFC-9114, section 5.2 + this.exchange.markUnprocessedByPeer(); + this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); + // close the exchange and complete the response CF exceptionally + close(); + completeResponseExceptionally(this.errorRef.get()); + if (debug.on()) { + debug.log("request unprocessed by peer " + this.request); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + Throwable error; + stateLock.lock(); + try { + if (closed) return; + closed = true; + error = errorRef.get(); + } finally { + stateLock.unlock(); + } + if (Log.http3()) { + if (error == null) { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1}", + connection.quicConnectionTag(), Long.toString(stream.streamId())); + } else { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1} with error {2}", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + error); + } + } + if (debug.on()) { + debug.log("stream %d is now closed with %s", + streamId(), + error == null ? "no error" : String.valueOf(error)); + } + if (Log.trace()) { + Log.logTrace("Stream {0} is now closed", streamId()); + } + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3StreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(error); + } + connection.onExchangeClose(this, streamId()); + } + + class RequestSubscriber implements Flow.Subscriber { + // can be < 0 if the actual length is not known. + private final long contentLength; + private volatile long remainingContentLength; + private volatile boolean dataHeaderWritten; + private volatile boolean completed; + private final AtomicReference subscription = new AtomicReference<>(); + + RequestSubscriber(long contentLen) { + this.contentLength = contentLen; + this.remainingContentLength = contentLen; + } + + @Override + public void onSubscribe(Subscription subscription) { + if (!this.subscription.compareAndSet(null, subscription)) { + subscription.cancel(); + throw new IllegalStateException("already subscribed"); + } + if (debug.on()) + debug.log("RequestSubscriber: onSubscribe, request 1"); + subscription.request(1); + } + + @Override + public void onNext(ByteBuffer item) { + if (debug.on()) + debug.log("RequestSubscriber: onNext(%d)", item.remaining()); + var subscription = this.subscription.get(); + if (writer.stopSendingReceived()) { + // whether StopSending contains NO_ERROR or not - we should + // not fail the request and simply stop sending the body. + // The sender should either reset the stream or send a full + // response with an error status code if it wants to fail the request. + Http3Error error = Http3Error.fromCode(writer.stream().sndErrorCode()) + .orElse(Http3Error.H3_NO_ERROR); + if (debug.on()) + debug.log("Stop sending requested by peer (%s): canceling subscription", error); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + + if (isCanceled() || errorRef.get() != null) { + if (writer.sendingState().isSending()) { + try { + if (debug.on()) { + debug.log("onNext called after stream cancelled: " + + "resetting stream %s", streamId()); + } + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: ", t); + errorRef.compareAndSet(null, t); + requestBodyCF.completeExceptionally(errorRef.get()); + } + } + return; + } + long len = item.remaining(); + try { + writeHeadersIfNeeded(item); + var remaining = remainingContentLength; + if (contentLength >= 0) { + remaining -= len; + remainingContentLength = remaining; + if (remaining < 0) { + lengthMismatch("Too many bytes in request body"); + subscription.cancel(); + } + } + var completed = remaining == 0; + if (completed) this.completed = true; + writer.scheduleForWriting(item, completed); + sentQuicBytes.addAndGet(len); + if (completed) { + requestBodyCF.complete(null); + } + if (writer.credit() > 0) { + if (debug.on()) + debug.log("RequestSubscriber: request 1"); + subscription.request(1); + } else { + if (debug.on()) + debug.log("RequestSubscriber: no more credit"); + } + } catch (Throwable t) { + if (writer.stopSendingReceived()) { + // We can reach here if we continue sending after stop sending + // was received, which may happen since stop sending is + // received asynchronously. In that case, we should + // not fail the request but simply stop sending the body. + // The sender will either reset the stream or send a full + // response with an error status code if it wants to fail + // or complete the request. + if (debug.on()) + debug.log("Stop sending requested by peer: canceling subscription"); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + // stop sending was not received: cancel the stream + errorRef.compareAndSet(null, t); + if (debug.on()) { + debug.log("Unexpected exception in onNext: " + t); + debug.log("resetting stream %s", streamId()); + } + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable rt) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + cancelImpl(errorRef.get(), Http3Error.H3_REQUEST_CANCELLED); + } + + } + + private void lengthMismatch(String what) { + if (debug.on()) { + debug.log(what + " (%s/%s)", + contentLength - remainingContentLength, contentLength); + } + try { + var failed = new IOException("stream=" + streamId() + " " + + "[" + Thread.currentThread().getName() + "] " + + what + " (" + + (contentLength - remainingContentLength) + "/" + + contentLength + ")"); + errorRef.compareAndSet(null, failed); + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + requestBodyCF.completeExceptionally(errorRef.get()); + } catch (Throwable t) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + close(); + } + + private void writeHeadersIfNeeded(ByteBuffer item) throws IOException { + long len = item.remaining(); + if (contentLength >= 0) { + if (!dataHeaderWritten) { + dataHeaderWritten = true; + len = contentLength; + } else { + // headers already written: nothing to do. + return; + } + } + DataFrame df = new DataFrame(len); + ByteBuffer headers = ByteBuffer.allocate(df.headersSize()); + df.writeHeaders(headers); + headers.flip(); + int sent = headers.remaining(); + writer.queueForWriting(headers); + if (sent != 0) sentQuicBytes.addAndGet(sent); + } + + @Override + public void onError(Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: onError: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + // no need to cancel subscription + close(); + } + } + + @Override + public void onComplete() { + if (debug.on()) debug.log("RequestSubscriber: send request body completed"); + var completed = this.completed; + if (completed || errorRef.get() != null) return; + if (contentLength >= 0 && remainingContentLength != 0) { + if (remainingContentLength < 0) { + lengthMismatch("Too many bytes in request body"); + } else { + lengthMismatch("Too few bytes returned by the publisher"); + } + return; + } + this.completed = true; + try { + writer.scheduleForWriting(QuicStreamReader.EOF, true); + requestBodyCF.complete(null); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to complete stream: " + t, t); + requestBodyCF.completeExceptionally(t); + } + } + + void unblock() { + if (completed || errorRef.get() != null) { + return; + } + var subscription = this.subscription.get(); + try { + if (writer.credit() > 0) { + if (subscription != null) { + subscription.request(1); + } + } + } catch (Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: unblock: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + cancelImpl(throwable, Http3Error.H3_REQUEST_CANCELLED); + subscription.cancel(); + } + } + } + + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + this.responseCode = responseCode; + return this.response = new Response( + request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(responseHeadersBuilder, rspHeadersConsumer, readScheduler, debug); + } + + protected void handlePromise(PushHeadersConsumer consumer) throws IOException { + PushPromiseState state = consumer.state; + PushPromiseFrame ppf = state.frame(); + promiseMap.remove(ppf); + long pushId = ppf.getPushId(); + + HttpHeaders promiseHeaders = state.headersBuilder().build(); + consumer.reset(); + + if (debug.on()) { + debug.log("received promise headers: %s", + promiseHeaders); + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("PUSH_PROMISE HEADERS (pushId: ") + .append(pushId).append("):\n"); + Log.dumpHeaders(sb, " ", promiseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + String method = promiseHeaders.firstValue(":method") + .orElseThrow(() -> new ProtocolException("no method in promise request")); + String path = promiseHeaders.firstValue(":path") + .orElseThrow(() -> new ProtocolException("no path in promise request")); + String authority = promiseHeaders.firstValue(":authority") + .orElseThrow(() -> new ProtocolException("no authority in promise request")); + if (Set.of("PUT", "DELETE", "OPTIONS", "TRACE").contains(method)) { + throw new ProtocolException("push method not allowed pushId=" + pushId); + } + long clen = promiseHeaders.firstValueAsLong("Content-Length").orElse(-1); + if (clen > 0) { + throw new ProtocolException("push headers contain non-zero Content-Length for pushId=" + pushId); + } + if (promiseHeaders.firstValue("Transfer-Encoding").isPresent()) { + throw new ProtocolException("push headers contain Transfer-Encoding for pushId=" + pushId); + } + + + // this will clear the response headers + // At this point the push promise stream may not be opened yet + if (connection.onPushPromiseFrame(this, pushId, promiseHeaders)) { + // the promise response will be handled from a child of this exchange + // once the push stream is open, we have nothing more to do here. + if (debug.on()) { + debug.log("handling push promise response for %s with request-response stream %s", + pushId, streamId()); + } + } else { + // the promise response is being handled by another exchange, just accept the id + if (debug.on()) { + debug.log("push promise response for %s is already handled by another stream", + pushId); + } + PushGroup pushGroup = exchange.getPushGroup(); + connection.whenPushAccepted(pushId).thenAccept((accepted) -> { + if (accepted) { + pushGroup.acceptPushPromiseId(connection.newPushId(pushId)); + } + }); + } + } + + private void cancelPushPromise(PushPromiseState state, IOException cause) { + // send CANCEL_PUSH frame here + long pushId = state.frame().getPushId(); + connection.pushCancelled(pushId, cause); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} {2} #{3} (requestSent={4}, responseReceived={5}, " + + "reader={6}, writer={7}, statusCode={8}, finalStream={9}, " + + "receivedQuicBytes={10}, sentQuicBytes={11}): {12}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), request, String.valueOf(exchange.multi.id), + requestSent, responseReceived, reader.receivingState(), writer.sendingState(), + String.valueOf(responseCode), connection.isFinalStream(), String.valueOf(receivedQuicBytes()), + String.valueOf(sentQuicBytes.get()), io); + } + } + + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: ", streamId(), resetReason); + } + // if the error is H3_REQUEST_REJECTED then it implies + // the request wasn't processed and the client is allowed to reissue + // that request afresh + if (resetError == Http3Error.H3_REQUEST_REJECTED) { + closeAsUnprocessed(); + } else if (!requestSent || !responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("stopping scheduler for stream %s", streamId()); + } + readScheduler.stop(); + } + + + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + if (debug.on()) + debug.log("processQuicData - Unexpected exception", t); + if (!requestSent) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + if (debug.on()) + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace("connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + record PushPromiseState(PushPromiseFrame frame, + HeaderFrameReader reader, + HttpHeadersBuilder headersBuilder, + DecodingCallback consumer) {} + final ConcurrentHashMap promiseMap = new ConcurrentHashMap<>(); + + private void ignorePushPromiseData(PushPromiseFrame ppf, List payload) { + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + buf.limit(buf.position()); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + private boolean ignorePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + long pushId = ppf.getPushId(); + long minPushId = connection.getMinPushId(); + if (exchange.pushGroup == null) { + IOException checkFailed = connection.checkMaxPushId(pushId); + if (checkFailed != null) { + // connection is closed + throw checkFailed; + } + if (!connection.acceptPromises()) { + // if no stream accept promises, we can ignore the data and + // cancel the promise right away. + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (no promise handler): %s%n", ppf); + } + ignorePushPromiseData(ppf, payload); + if (pushId >= minPushId) { + connection.noPushHandlerFor(pushId); + } + return true; + } + } + if (pushId < minPushId) { + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (pushId=%s < %s): %s%n", + pushId, minPushId, ppf); + } + ignorePushPromiseData(ppf, payload); + return true; + } + return false; + } + + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + var state = promiseMap.get(ppf); + if (state == null) { + if (ignorePushPromiseFrame(ppf, payload)) return; + if (debug.on()) + debug.log("received PushPromiseFrame: " + ppf); + var checkFailed = connection.checkMaxPushId(ppf.getPushId()); + if (checkFailed != null) throw checkFailed; + var builder = new HttpHeadersBuilder(); + var consumer = new PushHeadersConsumer(); + var reader = qpackDecoder.newHeaderFrameReader(consumer); + state = new PushPromiseState(ppf, reader, builder, consumer); + consumer.setState(state); + promiseMap.put(ppf, state); + } + if (debug.on()) + debug.log("receive promise headers: buffer list: " + payload); + HeaderFrameReader headerFrameReader = state.reader(); + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + /** + * This method is called by the {@link Http3PushManager} in order to + * invoke the {@link Acceptor} that will accept the push + * promise. This method gets the acceptor, invokes its {@link + * Acceptor#accepted()} method, and if {@code true}, returns the + * {@code Acceptor}. + *

      + * If the push request is not accepted this method returns {@code null}. + * + * @apiNote + * This method is called upon reception of a {@link PushPromiseFrame}. + * The quic stream that will carry the body may not be available yet. + * + * @param pushId the pushId + * @param pushRequest the promised push request + * @return an {@link Acceptor} to get the body handler for the + * push request, or {@code null}. + */ + Acceptor acceptPushPromise(long pushId, HttpRequestImpl pushRequest) { + if (Log.requests()) { + Log.logRequest("PUSH_PROMISE: " + pushRequest.toString()); + } + PushGroup pushGroup = exchange.getPushGroup(); + if (pushGroup == null || exchange.multi.requestCancelled()) { + if (Log.trace()) { + Log.logTrace("Rejecting push promise pushId: " + pushId); + } + connection.pushCancelled(pushId, null); + return null; + } + + Acceptor acceptor = null; + boolean accepted = false; + try { + acceptor = pushGroup.acceptPushRequest(pushRequest, connection.newPushId(pushId)); + accepted = acceptor.accepted(); + } catch (Throwable t) { + if (debug.on()) + debug.log("PushPromiseHandler::applyPushPromise threw exception %s", + (Object)t); + } + if (!accepted) { + // cancel / reject + if (Log.trace()) { + Log.logTrace("No body subscriber for {0}: {1}", pushRequest, + "Push " + pushId + " cancelled by users handler"); + } + connection.pushCancelled(pushId, null); + return null; + } + + assert accepted && acceptor != null; + return acceptor; + } + + /** + * This method is called by the {@link Http3PushManager} once the {@link Acceptor#cf() + * responseCF} has been obtained from the acceptor. + * @param pushId the pushId + * @param responseCF the response completable future + */ + void onPushRequestAccepted(long pushId, CompletableFuture> responseCF) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + // setup housekeeping for when the push is received + // TODO: deal with ignoring of CF anti-pattern + CompletableFuture> cf = responseCF; + cf.whenComplete((HttpResponse resp, Throwable t) -> { + t = Utils.getCompletionCause(t); + if (Log.trace()) { + Log.logTrace("Push {0} completed for {1}{2}", pushId, resp, + ((t==null) ? "": " with exception " + t)); + } + if (t != null) { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + t); + } + pushGroup.pushError(t); + } else { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + resp); + } + } + pushGroup.pushCompleted(); + }); + } + + /** + * This method is called by the {@link Http3PushPromiseStream} when + * starting + * @param pushRequest the pushRequest + * @param pushStream the pushStream + */ + void onHttp3PushStreamStarted(HttpRequestImpl pushRequest, + Http3PushPromiseStream pushStream) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + assert pushStream != null; + connection.onPushPromiseStreamStarted(pushStream, pushStream.streamId()); + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) { + if (debug.on()) + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + // we are at EOF - no need to pause reading + switchReadingPaused(false); + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_FRAME_ERROR); + } + } + + + // Invoked when data can be pushed to the quic stream; + // Headers may block the stream - but they will be buffered in the stream + // so should not cause this method to be called. + // We should reach here only when sending body bytes. + private void sendQuicData() { + // This method is invoked when the sending part of the + // stream is unblocked. + if (!requestBodyCF.isDone()) { + if (!exchange.multi.requestCancelled()) { + var requestSubscriber = this.requestSubscriber; + // the requestSubscriber will request more data from + // upstream if needed + if (requestSubscriber != null) requestSubscriber.unblock(); + } + } + } + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // ensure that the body subscriber will be subscribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + + // We want to allow the subscriber's getBody() method to block, so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. It is called from the processQuicData + // loop. However, we cannot call onNext() if we have no demands. + // So we're using a responseData queue to buffer incoming data. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + if (debug.on()) debug.log("pushResponseData"); + HttpResponse.BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty() && errorRef.get() == null) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onNext {0}", size); + } + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + if (debug.on()) debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("pushResponseData: EOF"); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } + } catch (Throwable throwable) { + if (debug.on()) debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", x); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + + // This method is called by Http2Connection::decrementStreamCount in order + // to make sure that the stream count is decremented only once for + // a given stream. + boolean deRegister() { + return DEREGISTERED.compareAndSet(this, false, true); + } + + private static final VarHandle DEREGISTERED; + static { + try { + DEREGISTERED = MethodHandles.lookup() + .findVarHandle(Http3ExchangeImpl.class, "deRegistered", boolean.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java new file mode 100644 index 00000000000..92d51b101f7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.Http3ClientImpl.ConnectionRecovery; +import jdk.internal.net.http.Http3ClientImpl.PendingConnection; +import jdk.internal.net.http.Http3ClientImpl.StreamLimitReached; +import jdk.internal.net.http.common.Log; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class keeps track of pending HTTP/3 connections + * to avoid making two connections to the same server + * in parallel. Methods in this class are not atomic. + * Therefore, it is expected that they will be called + * while holding a lock in order to ensure atomicity. + */ +class Http3PendingConnections { + + private final Map pendingAdvertised = new ConcurrentHashMap<>(); + private final Map pendingUnadvertised = new ConcurrentHashMap<>(); + + Http3PendingConnections() {} + + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + // Should be called while holding Http3ClientImpl.lock + void streamLimitReached(String key, Http3Connection connection) { + var altSvc = connection.connection().getSourceAltService().orElse(null); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var queue = advertised ? pendingAdvertised : pendingUnadvertised; + queue.computeIfAbsent(key, k -> new StreamLimitReached(connection)); + } + + // Remove a ConnectionRecovery after the connection was established + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery removeCompleted(String connectionKey, Exchange origExchange, Http3Connection conn) { + var altSvc = Optional.ofNullable(conn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + var discovery = Optional.ofNullable(origExchange) + .map(Exchange::request) + .map(HttpRequestImpl::http3Discovery) + .orElse(null); + var advertised = (altSvc != null && altSvc.wasAdvertised()) + || discovery == ALT_SVC; + var sameOrigin = (altSvc != null && altSvc.originHasSameAuthority()); + + ConnectionRecovery recovered = null; + if (advertised) { + recovered = pendingAdvertised.remove(connectionKey); + } + if (discovery == ALT_SVC || recovered != null) return recovered; + if (altSvc == null) { + // for instance, there was an exception, so we don't + // know if there was an altSvc because conn == null + recovered = pendingAdvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingAdvertised.remove(connectionKey, recovered); + return recovered; + } + } + } + recovered = pendingUnadvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingUnadvertised.remove(connectionKey, recovered); + return pending; + } + } + if (!sameOrigin && advertised) return null; + return pendingUnadvertised.remove(connectionKey); + } + + // Lookup a ConnectionRecovery for the given request with the + // given key. + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery lookupFor(String key, HttpRequestImpl request, HttpClientImpl client) { + + var discovery = request.http3Discovery(); + + // if ALT_SVC only look in advertised + if (discovery == ALT_SVC) { + return pendingAdvertised.get(key); + } + + // if HTTP_3_ONLY look first in pendingUnadvertised + var unadvertised = pendingUnadvertised.get(key); + if (discovery == HTTP_3_URI_ONLY && unadvertised != null) { + if (unadvertised instanceof PendingConnection) { + return unadvertised; + } + } + + // then look in advertised + var advertised = pendingAdvertised.get(key); + if (advertised instanceof PendingConnection pending) { + var altSvc = pending.altSvc(); + var sameOrigin = altSvc != null && altSvc.originHasSameAuthority(); + assert altSvc != null; // pending advertised should have altSvc + if (discovery == ANY || sameOrigin) return advertised; + } + + // if HTTP_3_ONLY, nothing found, stop here + assert discovery != HTTP_3_URI_ONLY || !(unadvertised instanceof PendingConnection); + if (discovery == HTTP_3_URI_ONLY) { + if (advertised != null && Log.http3()) { + Log.logHttp3("{0} cannot be used for {1}: return null", advertised, request); + } + assert !(unadvertised instanceof PendingConnection); + return unadvertised; + } + + // if ANY return advertised if found, otherwise unadvertised + if (advertised instanceof PendingConnection) return advertised; + if (unadvertised instanceof PendingConnection) { + if (client.client3().isEmpty()) { + return unadvertised; + } + // if ANY and we have an alt service that's eligible for the request + // and is not same origin as the request's URI authority, then don't + // return unadvertised and instead return advertised (which may be null) + final AltService altSvc = client.client3().get().lookupAltSvc(request).orElse(null); + if (altSvc != null && !altSvc.originHasSameAuthority()) { + return advertised; + } else { + return unadvertised; + } + } + if (advertised != null) return advertised; + return unadvertised; + } + + // Adds a pending connection for the given request with the given + // key and altSvc. + // Should be called while holding Http3ClientImpl.lock + PendingConnection addPending(String key, HttpRequestImpl request, AltService altSvc, Exchange exchange) { + var discovery = request.http3Discovery(); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var sameOrigin = altSvc == null || altSvc.originHasSameAuthority(); + // if advertised and same origin, we don't use pendingUnadvertised + // but pendingAdvertised even if discovery is HTTP_3_URI_ONLY + // if we have an advertised altSvc with not same origin, we still + // want to attempt HTTP_3_URI_ONLY at origin, as an unadvertised + // connection. If advertised & same origin, we can use the advertised + // service instead and use pendingAdvertised, even for HTTP_3_URI_ONLY + if (discovery == HTTP_3_URI_ONLY && (!advertised || !sameOrigin)) { + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous unadvertised pending connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + assert discovery != HTTP_3_URI_ONLY || advertised && sameOrigin; + if (advertised) { + PendingConnection pendingConnection = new PendingConnection(altSvc, exchange); + var previous = pendingAdvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous pending advertised connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + if (discovery == ANY) { + assert !advertised; + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = ("previous unadvertised pending connection found for ANY!" + + " (originally created for %s #%s) while adding pending connection for %s") + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + // last case - if we reach here we're ALT_SVC but couldn't + // find an advertised alt service. + assert discovery == ALT_SVC; + return null; + } +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java new file mode 100644 index 00000000000..b9cf4dbc0f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_HTTP3_PUSH_STREAMS; + +/** + * Manages HTTP/3 push promises for an HTTP/3 connection. + *

      + * This class maintains a bounded collection of recent push promises, + * together with the current state of the promise: pending, processed, or + * cancelled. When a new {@link jdk.internal.net.http.http3.frames.PushPromiseFrame} + * is received, and entry is added in the map, and the state of the promise + * is updated as it goes. + * When the map is full, old entries (lowest pushId) are expunged from + * the map. No promise will be accepted if its pushId is lower than the + * lowest pushId in the map. + * + * @apiNote + * When a PushPromiseFrame is received, {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)} + * is called. This arranges for an entry to be added to the map, unless there's + * already one. Also, the first Http3ExchangeImpl for which this method is called + * for a given pushId gets to handle the PushPromise: its {@link + * java.net.http.HttpResponse.PushPromiseHandler} will be invoked to accept the promise + * and handle the body. + *

      + * When a new PushStream is opened, {@link #onPushPromiseStream(QuicReceiverStream, long)} + * is called. When both {@code onPushPromiseFrame} and {@code onPushPromiseStream} have + * been called for a given {@code pushId}, an {@link Http3PushPromiseStream} is created + * and started to receive the body. + *

      + * {@link Http3ExchangeImpl} that receive a push promise frame, but don't get to handle + * the body (because it's already been delegated to another stream) should call + * {@link #whenAccepted(long)} to figure out when it is safe to invoke {@link + * PushGroup#acceptPushPromiseId(PushId)}. + *

      + * {@link #cancelPushPromise(long, Throwable, CancelPushReason)} can be called to cancel + * a push promise. {@link #pushPromiseProcessed(long)} should be called when the body + * has been fully processed. + */ +final class Http3PushManager { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + + private final ReentrantLock promiseLock = new ReentrantLock(); + private final ConcurrentHashMap promises = new ConcurrentHashMap<>(); + private final CompletableFuture DENIED = MinimalFuture.completedFuture(Boolean.FALSE); + private final CompletableFuture ACCEPTED = MinimalFuture.completedFuture(Boolean.TRUE); + + private final AtomicLong maxPushId = new AtomicLong(); + private final AtomicLong maxPushReceived = new AtomicLong(); + private final AtomicLong minPushId = new AtomicLong(); + // the max history we keep in the promiseMap. We start expunging old + // entries from the map when the size of the map exceeds this value + private static final long MAX_PUSH_HISTORY_SIZE = (3*MAX_HTTP3_PUSH_STREAMS)/2; + // the maxPushId increments, we send on MAX_PUSH_ID frame + // with a maxPushId incremented by that amount. + // Ideally should be <= to MAX_PUSH_HISTORY_SIZE, to avoid + // filling up the history right after the first MAX_PUSH_ID + private static final long MAX_PUSH_ID_INCREMENTS = MAX_HTTP3_PUSH_STREAMS; + private final Http3Connection connection; + + // number of pending promises + private final AtomicInteger pendingPromises = new AtomicInteger(); + // push promises are considered blocked if we have failed to send + // the last MAX_PUSH_ID update due to pendingPromises + // count having reached MAX_HTTP3_PUSH_STREAMS + private volatile boolean pushPromisesBlocked; + + + Http3PushManager(Http3Connection connection) { + this.connection = connection; + } + + String dbgTag() { + return connection.dbgTag(); + } + + public void cancelAllPromises(IOException closeCause, Http3Error error) { + for (var promise : promises.entrySet()) { + var pushId = promise.getKey(); + var pp = promise.getValue(); + switch (pp) { + case ProcessedPushPromise ignored -> {} + case CancelledPushPromise ignored -> {} + case PendingPushPromise ppp -> { + cancelPendingPushPromise(ppp, closeCause); + } + } + } + } + + // Different actions needs to be carried out when cancelling a + // push promise, depending on the state of the promise and the + // cancellation reason. + enum CancelPushReason { + NO_HANDLER, // the exchange has no PushGroup + PUSH_CANCELLED, // the PromiseHandler cancelled the push, + // or an error occurred handling the promise + CANCEL_RECEIVED; // received CANCEL_PUSH from server + } + + /** + * A PushPromise can be a PendingPushPromise, until the push + * response is completely received, or a ProcessedPushPromise, + * which replace the PendingPushPromise after the response body + * has been delivered. If the PushPromise is cancelled before + * accepting it or receiving a body, CancelledPushPromise will + * be recorded and replace the PendingPushPromise. + */ + private sealed interface PushPromise + permits PendingPushPromise, ProcessedPushPromise, CancelledPushPromise { + } + + /** + * Represent a PushPromise whose body as already been delivered + */ + private record ProcessedPushPromise(PushId pushId, HttpHeaders promiseHeaders) + implements PushPromise { } + + /** + * Represent a PushPromise that has been cancelled. No body will be delivered. + */ + private record CancelledPushPromise(PushId pushId) implements PushPromise { } + + // difficult to say what will come first - the push promise, + // or the push stream? + // The first push promise frame received will register the + // exchange with this class - and trigger the parsing of + // the request/response when the stream is available. + // The other will trigger a simple call to register the + // push id. + // Probably we also need some timer to clean + // up the map if the stream doesn't manifest after a while. + // We maintain minPushID, where any frame + // containing a push id < to the min will be discarded, + // and any stream with a pushId < will also be discarded. + + /** + * Represents a PushPromise whose body has not been delivered + * yet. + * @param the type of the body + */ + private static final class PendingPushPromise implements PushPromise { + // called when the first push promise frame is received + PendingPushPromise(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) { + this.accepted = new MinimalFuture<>(); + this.exchange = Objects.requireNonNull(exchange); + this.promiseHeaders = Objects.requireNonNull(promiseHeaders); + this.pushId = pushId; + } + + // called when the push promise stream is opened + PendingPushPromise(QuicReceiverStream stream, long pushId) { + this.accepted = new MinimalFuture<>(); + this.stream = Objects.requireNonNull(stream); + this.pushId = pushId; + } + + // volatiles should not be required since we only modify/read + // those within a lock. Final fields should ensure safe publication + final long pushId; // the push id + QuicReceiverStream stream; // the quic promise stream + Http3ExchangeImpl exchange; // the exchange that will create the body subscriber + Http3PushPromiseStream promiseStream; // the HTTP/3 stream to process the quic stream + HttpHeaders promiseHeaders; // the push promise request headers + CompletableFuture> responseCF; + HttpRequestImpl pushReq; + BodyHandler handler; + final CompletableFuture accepted; // whether the push promise was accepted + + public long pushId() { return pushId; } + + public boolean ready() { + if (stream == null) return false; + if (exchange == null) return false; + if (promiseHeaders == null) return false; + if (!accepted.isDone()) return false; + if (responseCF == null) return false; + if (pushReq == null) return false; + if (handler == null) return false; + return true; + } + + @Override + public String toString() { + return "PendingPushPromise{" + + "pushId=" + pushId + + ", stream=" + stream + + ", exchange=" + dbgTag(exchange) + + ", promiseStream=" + dbgTag(promiseStream) + + ", promiseHeaders=" + promiseHeaders + + ", accepted=" + accepted + + '}'; + } + + String dbgTag(Http3ExchangeImpl exchange) { + return exchange == null ? null : exchange.dbgTag(); + } + + String dbgTag(Http3PushPromiseStream promiseStream) { + return promiseStream == null ? null : promiseStream.dbgTag(); + } + } + + /** + * {@return the maximum pushId that can be accepted from the peer} + * This corresponds to the pushId that has been included in the last + * MAX_PUSH_ID frame sent to the peer. A pushId greater than this + * value must be rejected, and cause the connection to close with + * error. + * + * @apiNote due to internal constraints it is possible that the + * MAX_PUSH_ID frame has not been sent yet, but the {@code Http3PushManager} + * will behave as if the peer had received that frame. + * + * @see Http3Connection#checkMaxPushId(long) + * @see #checkMaxPushId(long) + */ + long getMaxPushId() { + return maxPushId.get(); + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + long getMinPushId() { + return minPushId.get(); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + if (!connection.acceptLargerPushPromise(pushStream, pushId)) return; + PendingPushPromise promise = addPushPromise(pushStream, pushId); + if (promise != null) { + assert promise.stream == pushStream; + // if stream is avoilable start parsing? + tryReceivePromise(promise); + } + } + + /** + * Checks whether a MAX_PUSH_ID frame needs to be sent, + * and send it. + * Called from {@link Http3Connection#checkSendMaxPushId()}. + */ + void checkSendMaxPushId() { + if (MAX_PUSH_ID_INCREMENTS <= 0) return; + long pendingCount = pendingPromises.get(); + long availableSlots = MAX_HTTP3_PUSH_STREAMS - pendingCount; + if (availableSlots <= 0) { + pushPromisesBlocked = true; + if (debug.on()) debug.log("Push promises blocked: availableSlots=%s", pendingCount); + return; + } + long maxPushIdSent = maxPushId.get(); + long maxPushIdReceived = maxPushReceived.get(); + long half = Math.max(1, MAX_PUSH_ID_INCREMENTS /2); + if (maxPushIdSent - maxPushIdReceived < half) { + // do not send a maxPushId that would consume more + // than our available slots + long increment = Math.min(availableSlots, MAX_PUSH_ID_INCREMENTS); + long update = maxPushIdSent + increment; + boolean updated = false; + try { + // let's update the counter before sending the frame, + // otherwise there's a chance we can receive a frame + // before updating the counter. + do { + if (maxPushId.compareAndSet(maxPushIdSent, update)) { + if (debug.on()) { + debug.log("MAX_PUSH_ID updated: %s (%s -> %s), increment %s, pending %s, availableSlots %s", + update, maxPushIdSent, update, increment, + promises.values().stream().filter(PendingPushPromise.class::isInstance) + .map(p -> (PendingPushPromise) p) + .map(PendingPushPromise::pushId).toList(), + availableSlots); + } + updated = true; + break; + } + maxPushIdSent = maxPushId.get(); + } while (maxPushIdSent < update); + if (updated) { + if (pushPromisesBlocked) { + if (debug.on()) debug.log("Push promises unblocked: maxPushIdSent=%s", update); + pushPromisesBlocked = false; + } + connection.sendMaxPushId(update); + } + } catch (IOException io) { + debug.log("Failed to send MAX_PUSH_ID(%s): %s", update, io); + } + } + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @apiNote + * This method calls {@link Http3ExchangeImpl#acceptPushPromise(long, HttpRequestImpl)} + * and {@link Http3ExchangeImpl#onPushRequestAccepted(long, CompletableFuture)} + * for the first exchange that receives the {@link + * jdk.internal.net.http.http3.frames.PushPromiseFrame} + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + * + * @see Http3Connection#onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders) + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + if (!connection.acceptLargerPushPromise(null, pushId)) return false; + PendingPushPromise promise = addPushPromise(exchange, pushId, promiseHeaders); + if (promise == null) { + return false; + } + // A PendingPushPromise is returned only if there was no + // PushPromise present. If a PendingPushPromise is returned + // it should therefore have its exchange already set to the + // current exchange. + assert promise.exchange == exchange; + HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest( + exchange.getExchange().request(), promiseHeaders); + var acceptor = exchange.acceptPushPromise(pushId, pushReq); + if (acceptor == null) { + // nothing to do: the push should already have been cancelled. + return false; + } + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) promise; + var responseCF = pppU.responseCF; + assert responseCF == null; + boolean cancelled = false; + promiseLock.lock(); + try { + promise.pushReq = pushReq; + pppU.responseCF = responseCF = acceptor.cf(); + // recheck to verify the push hasn't been cancelled already + var check = promises.get(pushId); + if (check instanceof CancelledPushPromise || check == null) { + cancelled = true; + } else { + assert promise == check; + pppU.handler = acceptor.bodyHandler(); + } + } finally { + promiseLock.unlock(); + } + if (!cancelled) { + exchange.onPushRequestAccepted(pushId, responseCF); + promise.accepted.complete(true); + // if stream is available start parsing? + tryReceivePromise(promise); + return true; + } else { + cancelPendingPushPromise(promise, null); + // should be a no-op - in theory it should already + // have been completed + promise.accepted.complete(false); + return false; + } + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + * + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + * + * @see Http3Connection#whenPushAccepted(long) + * @param pushId the pushId + */ + CompletableFuture whenAccepted(long pushId) { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise pp) { + return pp.accepted; + } else if (promise instanceof ProcessedPushPromise) { + return ACCEPTED; + } else { // CancelledPushPromise or null + return DENIED; + } + } + + + /** + * Cancel a push promise. In case of concurrent requests receiving the + * same pushId, where one has a PushPromiseHandler and the other doesn't, + * we will cancel the push only if reason != CANCEL_RECEIVED, or no request + * stream has already accepted the push. + * + * @param pushId the promise pushId + * @param cause the cause (can be null) + * @param reason reason for cancelling + */ + void cancelPushPromise(long pushId, Throwable cause, CancelPushReason reason) { + boolean sendCancelPush = false; + PendingPushPromise pending = null; + if (cause != null) { + debug.log("PushPromise cancelled: pushId=" + pushId, cause); + } else { + debug.log("PushPromise cancelled: pushId=%s", pushId); + String msg = "cancelPushPromise(pushId="+pushId+")"; + debug.log(msg); + } + if (reason == CancelPushReason.CANCEL_RECEIVED) { + if (checkMaxPushId(pushId) != null) { + // pushId >= max connection will be closed + return; + } + } + promiseLock.lock(); + try { + var promise = promises.get(pushId); + long min = minPushId.get(); + if (promise == null) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + if (pushId >= min) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } else if (promise instanceof CancelledPushPromise) { + // nothing to do + } else if (promise instanceof ProcessedPushPromise) { + // nothing we can do? + } else if (promise instanceof PendingPushPromise ppp) { + // only cancel if never accepted, or force cancel requested + if (ppp.promiseStream == null || reason != CancelPushReason.NO_HANDLER) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + long pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + ppp.accepted.complete(false); // NO OP if already completed + pending = ppp; + // send cancel push; do not send if we received + // a CancelPushFrame from the peer + // also do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } + } finally { + promiseLock.unlock(); + } + if (sendCancelPush) { + connection.sendCancelPush(pushId, cause); + } + if (pending != null) { + cancelPendingPushPromise(pending, cause); + } + } + + private void cancelPendingPushPromise(PendingPushPromise ppp, Throwable cause) { + var ps = ppp.stream; + var http3 = ppp.promiseStream; + var responseCF = ppp.responseCF; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + if (http3 != null || responseCF != null) { + IOException io; + if (cause == null) { + io = new IOException("Push promise cancelled: " + ppp.pushId); + } else { + io = Utils.toIOException(cause); + } + if (http3 != null) { + http3.cancel(io); + } else if (responseCF != null) { + responseCF.completeExceptionally(io); + } + } + } + + /** + * Called when a push promise response body has been successfully received. + * @param pushId the pushId + */ + void pushPromiseProcessed(long pushId) { + promiseLock.lock(); + try { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise ppp) { + var processed = new ProcessedPushPromise(connection.newPushId(pushId), + ppp.promiseHeaders); + promises.put(pushId, processed); + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + // do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + } + } finally { + promiseLock.unlock(); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return connection.checkMaxPushId(pushId); + } + + // Checks whether an Http3PushPromiseStream can be created now + private void tryReceivePromise(PendingPushPromise promise) { + debug.log("tryReceivePromise: " + promise); + promiseLock.lock(); + Http3PushPromiseStream http3PushPromiseStream = null; + IOException failed = null; + try { + if (promise.ready() && promise.promiseStream == null) { + promise.promiseStream = http3PushPromiseStream = + createPushExchange(promise); + } else { + debug.log("tryReceivePromise: Can't create Http3PushPromiseStream for pushId=%s yet", + promise.pushId); + } + } catch (IOException io) { + failed = io; + } finally { + promiseLock.unlock(); + } + if (failed != null) { + cancelPushPromise(promise.pushId, failed, CancelPushReason.PUSH_CANCELLED); + return; + } + if (http3PushPromiseStream != null) { + // HTTP/3 push promises are not ref-counted + // If we were to change that it could be necessary to + // temporarly increment ref-counting here, until the stream + // read loop effectively starts. + http3PushPromiseStream.start(); + } + } + + // try to create and start an Http3PushPromiseStream when all bits have + // been received + private Http3PushPromiseStream createPushExchange(PendingPushPromise promise) + throws IOException { + assert promise.ready() : "promise is not ready: " + promise; + Http3ExchangeImpl parent = promise.exchange; + HttpRequestImpl pushReq = promise.pushReq; + QuicReceiverStream quicStream = promise.stream; + Exchange pushExch = new Exchange<>(pushReq, parent.exchange.multi); + Http3PushPromiseStream pushStream = new Http3PushPromiseStream<>(pushExch, + parent.http3Connection(), this, + quicStream, promise.responseCF, promise.handler, parent, promise.pushId); + pushExch.exchImpl = pushStream; + return pushStream; + } + + // The first exchange that gets the PushPromise gets a PushPromise object, + // others get null + // TODO: ideally we should start a timer to cancel a push promise if + // the stream doesn't materialize after a while. + // Note that the callers can always start their own timeouts using + // the CompletableFutures we returned to them. + private PendingPushPromise addPushPromise(Http3ExchangeImpl exchange, + long pushId, + HttpHeaders promiseHeaders) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise<>(exchange, pushId, promiseHeaders); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // we don't have the stream; + // the stream will be canceled if it comes later + // do not send push cancel frame (already cancelled, or abandoned) + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var pe = ppp.exchange; + if (pe == null) { + promiseLock.lock(); + try { + if (ppp.exchange == null) { + assert ppp.promiseHeaders == null; + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) ppp; + pppU.exchange = exchange; + pppU.promiseHeaders = promiseHeaders; + return pppU; + } + } finally { + promiseLock.unlock(); + } + } + var previousHeaders = ppp.promiseHeaders; + if (previousHeaders != null && !previousHeaders.equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof ProcessedPushPromise ppp) { + if (!ppp.promiseHeaders().equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof CancelledPushPromise) { + // already cancelled - nothing to do + } + return null; + } + + // TODO: the packet opening the push promise stream might reach us before + // the push promise headers are processed. We could start a timer + // here to cancel the push promise if the PushPromiseFrame doesn't materialize + // after a while. + private PendingPushPromise addPushPromise(QuicReceiverStream stream, long pushId) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise(stream, pushId); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; // maxPushId exceeded, connection closed + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // do not send push cancel frame (already cancelled, or abandoned) + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var ps = ppp.stream; + if (ps == null) { + promiseLock.lock(); + try { + if ((ps = ppp.stream) == null) { + ps = ppp.stream = stream; + } + } finally { + promiseLock.unlock(); + } + } + if (ps == stream) { + @SuppressWarnings("unchecked") + var pp = ((PendingPushPromise) ppp); + return pp; + } else { + // Error! cancel stream... + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } + } else if (promise instanceof ProcessedPushPromise) { + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } else { + // already cancelled? + // Error! cancel stream... + // connection.sendCancelPush(pushId, null); + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + return null; + } + + // We only keep MAX_PUSH_HISTORY_SIZE entries in the map. + // If the map has more than MAX_PUSH_HISTORY_SIZE entries, we start expunging + // pushIds starting at minPushId. This method makes room for at least + // on push promise in the map + private void checkExpungePromiseMap() { + assert promiseLock.isHeldByCurrentThread(); + while (promises.size() >= MAX_PUSH_HISTORY_SIZE) { + long min = minPushId.getAndIncrement(); + var pp = promises.remove(min); + if (pp instanceof PendingPushPromise ppp) { + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + var http3 = ppp.promiseStream; + IOException io = null; + if (http3 != null) { + http3.cancel(io = new IOException("PushPromise cancelled")); + } + if (io == null) { + io = new IOException("PushPromise cancelled"); + } + connection.sendCancelPush(ppp.pushId, io); + var ps = ppp.stream; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java new file mode 100644 index 00000000000..27aaa75891c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java @@ -0,0 +1,746 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.http3.Http3Error.H3_FRAME_UNEXPECTED; + +/** + * This class represents an HTTP/3 PushPromise stream. + */ +final class Http3PushPromiseStream extends Http3Stream { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpHeadersBuilder respHeadersBuilder; + private final PushRespHeadersConsumer respHeadersConsumer; + private final HeaderFrameReader respHeaderFrameReader; + private final Decoder qpackDecoder; + private final AtomicReference errorRef; + private final CompletableFuture pushCF = new MinimalFuture<>(); + private final CompletableFuture> responseCF; + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Http3ExchangeImpl parent; + private final long pushId; + private final Http3PushManager pushManager; + private final BodyHandler pushHandler; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnPromiseStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final ReentrantLock stateLock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForPushPromiseStream(); + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + volatile boolean closed; + volatile BodySubscriber pendingResponseSubscriber; + volatile BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean responseReceived; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + private String dbgTag = null; + + Http3PushPromiseStream(Exchange exchange, + final Http3Connection connection, + final Http3PushManager pushManager, + final QuicReceiverStream stream, + final CompletableFuture> responseCF, + final BodyHandler pushHandler, + Http3ExchangeImpl parent, + long pushId) { + super(exchange); + this.responseCF = responseCF; + this.pushHandler = pushHandler; + this.errorRef = new AtomicReference<>(); + this.pushId = pushId; + this.connection = connection; + this.pushManager = pushManager; + this.stream = stream; + this.parent = parent; + this.respHeadersBuilder = new HttpHeadersBuilder(); + this.respHeadersConsumer = new PushRespHeadersConsumer(); + this.qpackDecoder = connection.qpackDecoder(); + this.respHeaderFrameReader = qpackDecoder.newHeaderFrameReader(respHeadersConsumer); + this.reader = stream.connectReader(readScheduler); + debug.log("Http3PushPromiseStream created"); + } + + void start() { + exchange.exchImpl = this; + parent.onHttp3PushStreamStarted(exchange.request(), this); + this.reader.start(); + } + + long pushId() { + return pushId; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3PushPromiseStream(" + ctag + ", streamId=" + sid + ", pushId="+ pushId + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + private final class PushRespHeadersConsumer extends StreamHeadersConsumer { + + public PushRespHeadersConsumer() { + super(Context.RESPONSE); + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + @Override + String headerFieldType() { + return "PUSH RESPONSE HEADER FIELD"; + } + + @Override + Decoder qpackDecoder() { + return qpackDecoder; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push response: " + super.formatMessage(message, header); + } + + + @Override + HeaderFrameReader headerFrameReader() { + return respHeaderFrameReader; + } + + @Override + HttpHeadersBuilder headersBuilder() { + return respHeadersBuilder; + } + + @Override + void headersCompleted() { + handleResponse(); + } + + @Override + public long streamId() { + return stream.streamId(); + } + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + debug.log("Reference response body for h3 stream: " + streamId()); + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + debug.log("Unreference response body for h3 stream: " + streamId()); + client().h3StreamUnreference(); + } + } + + final class Http3PushStreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3PushStreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + } + + Http3PushStreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + debug.log("Creating body subscriber"); + return new Http3PushStreamResponseSubscriber<>(handler.apply(response)); + } + + @Override + CompletableFuture ignoreBody() { + try { + debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + + } + + @Override + void completed() { + + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + var cf = pushCF; + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + void completeResponse(Response r) { + debug.log("Response: " + r); + Log.logResponse(r::toString); + pushCF.complete(r); // not strictly required for push API + // start reading the body using the obtained BodySubscriber + CompletableFuture start = new MinimalFuture<>(); + start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor())) + .whenComplete((T body, Throwable t) -> { + if (t != null) { + responseCF.completeExceptionally(t); + debug.log("Cancelling push promise %s (stream %s) due to: %s", pushId, streamId(), t); + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else { + HttpResponseImpl resp = + new HttpResponseImpl<>(r.request, r, null, body, getExchange()); + debug.log("Completing responseCF: " + resp); + pushManager.pushPromiseProcessed(pushId); + responseCF.complete(resp); + } + }); + start.completeAsync(() -> null, getExchange().executor()); + } + + // methods to update state and remove stream when finished + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } + + /** + * same as above but for errors + */ + void completeResponseExceptionally(Throwable t) { + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + responseCF.completeExceptionally(t); + } + + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + return MinimalFuture.completedFuture(this); + } + + @Override + CompletableFuture> sendBodyAsync() { + return MinimalFuture.completedFuture(this); + } + + CompletableFuture> responseCF() { + return responseCF; + } + + + BodyHandler getPushHandler() { + // ignored parameters to function can be used as BodyHandler + return this.pushHandler; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + Log.logTrace("Reading body on stream {0}", streamId()); + debug.log("Getting BodySubscriber for: " + response); + Http3PushStreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + pg.pushError(t); + } + return MinimalFuture.failedFuture(t); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + stateLock.lock(); + try { + if (closed) return; + closed = true; + } finally { + stateLock.unlock(); + } + if (debug.on()) debug.log("stream %d is now closed", streamId()); + Log.logTrace("Stream {0} is now closed", streamId()); + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3PushStreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(errorRef.get()); + } + connection.onPushPromiseStreamClosed(this, streamId()); + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + return this.response = new Response( + exchange.request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(respHeadersBuilder, respHeadersConsumer, readScheduler, debug); + } + + @Override + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException { + readScheduler.stop(); + connectionError(new ProtocolException("Unexpected PUSH_PROMISE on push response stream"), H3_FRAME_UNEXPECTED); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} pushId={2} #{3} (responseReceived={4}, " + + "reader={5}, statusCode={6}, finalStream={9}): {10}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), pushId, String.valueOf(exchange.multi.id), + responseReceived, reader.receivingState(), + String.valueOf(responseCode), connection.isFinalStream(), io); + } + } + + @Override + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (!responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: Stopping scheduler", + streamId(), resetReason); + } + readScheduler.stop(); + } + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + debug.log("processQuicData - Unexpected exception", t); + if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) + throws IOException { + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + respHeaderFrameReader); + if (buf == QuicStreamReader.EOF) { + // we are at EOF - no need to pause reading + switchReadingPaused(false); + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_REQUEST_CANCELLED); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace( "connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // We want to allow the subscriber's getBody() method to block so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + // ensure that the body subscriber will be subsribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + debug.log("pushResponseData"); + boolean onCompleteCalled = false; + BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty()) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + Log.logTrace("responseSubscriber.onNext {0}", size); + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + debug.log("pushResponseData: EOF"); + if (!onCompleteCalled) { + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } + } + } catch (Throwable throwable) { + debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (!onCompleteCalled) { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } else { + if (debug.on()) + debug.log("already completed: dropping error %s", (Object) t); + } + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", t); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java new file mode 100644 index 00000000000..cdac68b47f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; +import static jdk.internal.net.http.RedirectFilter.HTTP_NOT_MODIFIED; + +/** + * A common super class for the HTTP/3 request/response stream ({@link Http3ExchangeImpl} + * and the HTTP/3 push promises stream ({@link Http3PushPromiseStream}. + * @param the expected type of the response body + */ +sealed abstract class Http3Stream extends ExchangeImpl permits Http3ExchangeImpl, Http3PushPromiseStream { + enum ResponseState { PERMIT_HEADER, PERMIT_TRAILER, PERMIT_NONE } + + // count of bytes read from the Quic stream. This is weakly consistent and + // used for debug only. Must not be updated outside of processQuicData + private volatile long receivedQuicBytes; + // keep track of which HTTP/3 frames have been parsed and whether more header + // frames are permitted + private ResponseState responseState = ResponseState.PERMIT_HEADER; + // value of content-length header in the response header, or null + private Long contentLength; + // number of data bytes delivered to user subscriber + private long consumedDataBytes; + // switched to true if reading from the quic stream should be temporarily + // paused. After switching back to false, readScheduler.runOrSchedule() should + // called. + private volatile boolean readingPaused; + + // A temporary buffer for response body bytes + final ConcurrentLinkedQueue> responseData = new ConcurrentLinkedQueue<>(); + + private final AtomicInteger nonFinalResponseCount = new AtomicInteger(); + + + Http3Stream(Exchange exchange) { + super(exchange); + } + + /** + * Cancel the stream exchange on error + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + abstract void cancelImpl(Throwable throwable, Http3Error error); + + /** + * {@return the Quic stream id for this exchange (request/response or push response)} + */ + abstract long streamId(); + + /** + * A base class implementing {@link DecodingCallback} used for receiving + * and building HttpHeaders. Can be used for request headers, response headers, + * push response headers, or trailers. + */ + abstract class StreamHeadersConsumer extends ValidatingHeadersConsumer + implements DecodingCallback { + + private volatile boolean hasError; + + StreamHeadersConsumer(Context context) { + super(context); + } + + abstract Decoder qpackDecoder(); + + abstract HeaderFrameReader headerFrameReader(); + + abstract HttpHeadersBuilder headersBuilder(); + + abstract void resetDone(); + + @Override + public void reset() { + super.reset(); + headerFrameReader().reset(); + headersBuilder().clear(); + hasError = false; + resetDone(); + } + + String headerFieldType() {return "HEADER FIELD";} + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + try { + String n = name.toString(); + String v = value.toString(); + super.onDecoded(n, v); + headersBuilder().addHeader(n, v); + if (Log.headers() && Log.trace()) { + Log.logTrace("RECEIVED {0} (streamid={1}): {2}: {3}", + headerFieldType(), streamId(), n, v); + } + } catch (Throwable throwable) { + if (throwable instanceof UncheckedIOException uio) { + // UncheckedIOException is thrown by ValidatingHeadersConsumer.onDecoded + // for cases with invalid headers or unknown/unsupported pseudo-headers. + // It should be treated as a malformed request. + // RFC-9114 4.1.2. Malformed Requests and Responses: + // Malformed requests or responses that are + // detected MUST be treated as a stream error of + // type H3_MESSAGE_ERROR. + onStreamError(uio.getCause(), Http3Error.H3_MESSAGE_ERROR); + } else { + onConnectionError(throwable, Http3Error.H3_INTERNAL_ERROR); + } + } + } + + @Override + public void onComplete() { + // RFC-9204 2.2.2.1: After the decoder finishes decoding a field + // section encoded using representations containing dynamic table + // references, it MUST emit a Section Acknowledgment instruction + qpackDecoder().ackSection(streamId(), headerFrameReader()); + qpackDecoder().resetInsertionsCounter(); + headersCompleted(); + } + + abstract void headersCompleted(); + + @Override + public void onStreamError(Throwable throwable, Http3Error http3Error) { + hasError = true; + qpackDecoder().resetInsertionsCounter(); + // Stream error + cancelImpl(throwable, http3Error); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + hasError = true; + // Connection error + connectionError(throwable, http3Error); + } + + @Override + public boolean hasError() { + return hasError; + } + + } + + /** + * {@return count of bytes read from the QUIC stream so far} + */ + public long receivedQuicBytes() { + return receivedQuicBytes; + } + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @implSpec + * The implementation of this method calls + * {@snippet lang=java : + * connectionError(throwable, error.code(), throwable.getMessage()); + * } + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + void connectionError(Throwable throwable, Http3Error error) { + connectionError(throwable, error.code(), throwable.getMessage()); + } + + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param errorCode an HTTP/3 error code + * @param errMsg an error message to be logged when closing the connection + */ + abstract void connectionError(Throwable throwable, long errorCode, String errMsg); + + + /** + * Push response data to the {@linkplain java.net.http.HttpResponse.BodySubscriber + * response body subscriber} if allowed by the subscription state. + * @param responseData a queue of available data to be pushed to the subscriber + */ + abstract void pushResponseData(ConcurrentLinkedQueue> responseData); + + /** + * Called when an exception is thrown by {@link QuicStreamReader#poll() reader::poll} + * when called from {@link #processQuicData(QuicStreamReader, FramesDecoder, + * H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}. + * This is typically only used for logging purposes. + * @param reader the stream reader + * @param io the exception caught + */ + abstract void onPollException(QuicStreamReader reader, IOException io); + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given header frame. + *

      + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param headers a partially received header frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receiveHeaders(HeadersFrame headers, List payload) throws IOException; + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given push promise frame. + *

      + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param ppf a partially received push promise frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException; + + /** + * {@return whether reading from the quic stream is currently paused} + * Typically reading is paused when waiting for headers to be decoded by QPack. + */ + boolean readingPaused() {return readingPaused;} + + /** + * Switches the value of the {@link #readingPaused() readingPaused} + * flag + *

      + * Subclasses of {@code Http3Stream} can call this method to switch + * the value of this flag if needed, typically in their + * concrete implementation of {@link #receiveHeaders(HeadersFrame, List)}. + * @param value the new value + */ + void switchReadingPaused(boolean value) { + readingPaused = value; + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial data frame are received. + private void receiveData(DataFrame data, List payload, Logger debug) { + if (debug.on()) { + debug.log("receiveData: adding %s payload byte", Utils.remaining(payload)); + } + responseData.add(payload); + pushResponseData(responseData); + } + + private ByteBuffer pollIfNotReset(QuicStreamReader reader) throws IOException { + ByteBuffer buffer; + try { + if (reader.isReset()) return null; + buffer = reader.poll(); + } catch (IOException io) { + if (reader.isReset()) return null; + onPollException(reader, io); + throw io; + } + return buffer; + } + + private Throwable toThrowable(MalformedFrame malformedFrame) { + Throwable cause = malformedFrame.getCause(); + if (cause != null) return cause; + return new ProtocolException(malformedFrame.toString()); + } + + /** + * Called when {@code processQuicData} detects that the {@linkplain + * QuicStreamReader reader} has been reset. + * This method should do the appropriate garbage collection, + * possibly closing the exchange or the connection if needed, and + * closing the read scheduler. + */ + abstract void onReaderReset(); + + /** + * Invoked when some data is received from the underlying quic stream. + * This implements the read loop for a request-response stream or a + * push response stream. + */ + void processQuicData(QuicStreamReader reader, + FramesDecoder framesDecoder, + H3FrameOrderVerifier frameOrderVerifier, + SequentialScheduler readScheduler, + Logger debug) throws IOException { + + + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + + // the reader might be null if the loop is triggered before + // the field is assigned + if (reader == null) return; + + // check whether we need to wait until response headers + // have been decoded: in that case readingPaused will be true + if (readingPaused) return; + + if (debug.on()) debug.log("processQuicData"); + ByteBuffer buffer; + Http3Frame frame; + pushResponseData(responseData); + boolean readmore = responseData.isEmpty(); + // do not read more until data has been pulled + while (readmore && (buffer = pollIfNotReset(reader)) != null) { + if (debug.on()) + debug.log("processQuicData - submitting buffer: %s bytes (ByteBuffer@%s)", + buffer.remaining(), System.identityHashCode(buffer)); + // only updated here + var received = receivedQuicBytes; + receivedQuicBytes = received + buffer.remaining(); + framesDecoder.submit(buffer); + while ((frame = framesDecoder.poll()) != null) { + if (debug.on()) debug.log("processQuicData - frame: " + frame); + final long frameType = frame.type(); + // before we start processing, verify that this frame *type* has arrived in the + // allowed order + if (!frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now + // RFC-9114, section 4.1 - Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + if (debug.on()) { + debug.log("unexpected (order of) frame type: " + + unexpectedFrameType + " on stream"); + } + Log.logError("Connection error due to unexpected (order of) frame type" + + " {0} on stream", unexpectedFrameType); + readScheduler.stop(); + final String errMsg = "Unexpected frame " + unexpectedFrameType; + connectionError(new ProtocolException(errMsg), Http3Error.H3_FRAME_UNEXPECTED); + return; + } + if (frame instanceof PartialFrame partialFrame) { + final List payload = framesDecoder.readPayloadBytes(); + if (debug.on()) { + debug.log("processQuicData - payload: %s", + payload == null ? null : Utils.remaining(payload)); + } + if (framesDecoder.eof() && !framesDecoder.clean()) { + String msg = "Frame truncated: " + partialFrame; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + break; + } + if ((payload == null || payload.isEmpty()) && partialFrame.remaining() != 0) { + break; + } + if (partialFrame instanceof HeadersFrame headers) { + receiveHeaders(headers, payload); + // check if we need to wait for the status code to be decoded + // before reading more + readmore = !readingPaused; + } else if (partialFrame instanceof DataFrame data) { + if (responseState != ResponseState.PERMIT_TRAILER) { + cancelImpl(new IOException("DATA frame not expected here"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (payload != null) { + consumedDataBytes += Utils.remaining(payload); + if (contentLength != null && + consumedDataBytes + data.remaining() > contentLength) { + cancelImpl(new IOException( + String.format("DATA frame (length %d) exceeds content-length (%d) by %d", + data.streamingLength(), contentLength, + consumedDataBytes + data.remaining() - contentLength)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + // don't read more if there is pending data waiting + // to be read from downstream + readmore = responseData.isEmpty(); + receiveData(data, payload, debug); + } + } else if (partialFrame instanceof PushPromiseFrame ppf) { + receivePushPromiseFrame(ppf, payload); + } else if (partialFrame instanceof UnknownFrame) { + if (debug.on()) { + debug.log("ignoring %s bytes for unknown frame type: %s", + Utils.remaining(payload), + Http3FrameType.asString(frameType)); + } + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All have already been taken care above. + // So this here should be dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + // mark as complete, if all expected data has been read for a frame + if (partialFrame.remaining() == 0) { + frameOrderVerifier.completed(frame); + } + } else if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + readScheduler.stop(); + connectionError(toThrowable(malformed), + malformed.getErrorCode(), + malformed.getMessage()); + return; + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All should have already been taken care above, + // including malformed frames. So this here should be + // dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) { + if (!framesDecoder.clean()) { + String msg = "EOF reading frame type and length"; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + } + if (debug.on()) debug.log("processQuicData - EOF"); + if (responseState == ResponseState.PERMIT_HEADER) { + cancelImpl(new EOFException("EOF reached: no header bytes received"), Http3Error.H3_MESSAGE_ERROR); + } else { + if (contentLength != null && + consumedDataBytes != contentLength) { + cancelImpl(new IOException( + String.format("fixed content-length: %d, bytes received: %d", contentLength, consumedDataBytes)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + receiveData(new DataFrame(0), + List.of(QuicStreamReader.EOF), debug); + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("EOF: Stopping scheduler"); + readScheduler.stop(); + } + if (reader.isReset() && responseData.isEmpty()) { + onReaderReset(); + } + } + + final String checkInterimResponseCountExceeded() { + // this is also checked by Exchange - but tracking it here too provides + // a more informative message. + int count = nonFinalResponseCount.incrementAndGet(); + if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) { + return String.format( + "Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s", + streamId(), count, MAX_NON_FINAL_RESPONSES); + } + return null; + } + + /** + * Called to create a new Response object for the newly receive response headers and + * response status code. This method is called from {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, after the status code + * and headers have been validated. + * + * @param responseHeaders response headers + * @param responseCode response code + * @return a new {@code Response} object + */ + abstract Response newResponse(HttpHeaders responseHeaders, int responseCode); + + /** + * Called at the end of {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, to propagate + * the response to the multi exchange. + * @param response the {@code Response} that was received. + */ + abstract void completeResponse(Response response); + + /** + * Validate response headers and status code based on the {@link #responseState}. + * If validated, this method will call {@link #newResponse(HttpHeaders, int)} to + * create a {@code Response} object, which it will then pass to + * {@link #completeResponse(Response)}. + * + * @param responseHeadersBuilder the response headers builder + * @param rspHeadersConsumer the response headers consumer + * @param readScheduler the read scheduler + * @param debug the debug logger + */ + void handleResponse(HttpHeadersBuilder responseHeadersBuilder, + StreamHeadersConsumer rspHeadersConsumer, + SequentialScheduler readScheduler, + Logger debug) { + if (responseState == ResponseState.PERMIT_NONE) { + connectionError(new ProtocolException("HEADERS after trailer"), + Http3Error.H3_FRAME_UNEXPECTED.code(), + "HEADERS after trailer"); + return; + } + HttpHeaders responseHeaders = responseHeadersBuilder.build(); + if (responseState == ResponseState.PERMIT_TRAILER) { + if (responseHeaders.firstValue(":status").isPresent()) { + cancelImpl(new IOException("Unexpected :status header in trailer"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (Log.headers()) { + Log.logHeaders("Ignoring trailers on stream {0}: {1}", streamId(), responseHeaders); + } else if (debug.on()) { + debug.log("Ignoring trailers: %s", responseHeaders); + } + responseState = ResponseState.PERMIT_NONE; + rspHeadersConsumer.reset(); + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + return; + } + + int responseCode; + boolean finalResponse = false; + try { + responseCode = (int) responseHeaders + .firstValueAsLong(":status") + .orElseThrow(() -> new IOException("no statuscode in response")); + } catch (IOException | NumberFormatException exception) { + // RFC-9114: 4.1.2. Malformed Requests and Responses: + // "Malformed requests or responses that are + // detected MUST be treated as a stream error of type H3_MESSAGE_ERROR" + cancelImpl(exception, Http3Error.H3_MESSAGE_ERROR); + return; + } + if (responseCode < 100 || responseCode > 999) { + cancelImpl(new IOException("Unexpected :status header value"), Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (responseCode >= 200) { + responseState = ResponseState.PERMIT_TRAILER; + finalResponse = true; + } else { + assert responseCode >= 100 && responseCode <= 200 : "unexpected responseCode: " + responseCode; + String protocolErrorMsg = checkInterimResponseCountExceeded(); + if (protocolErrorMsg != null) { + if (debug.on()) { + debug.log(protocolErrorMsg); + } + cancelImpl(new ProtocolException(protocolErrorMsg), Http3Error.H3_GENERAL_PROTOCOL_ERROR); + rspHeadersConsumer.reset(); + return; + } + } + + // update readingPaused after having decoded the statusCode and + // switched the responseState. + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + + var response = newResponse(responseHeaders, responseCode); + + if (debug.on()) { + debug.log("received response headers: %s", + responseHeaders); + } + + try { + OptionalLong cl = responseHeaders.firstValueAsLong("content-length"); + if (finalResponse && cl.isPresent()) { + long cll = cl.getAsLong(); + if (cll < 0) { + cancelImpl(new IOException("Invalid content-length value "+cll), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (!(exchange.request().method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED)) { + // HEAD response and 304 response might have a content-length header, + // but it carries no meaning + contentLength = cll; + } + } + } catch (NumberFormatException nfe) { + cancelImpl(nfe, Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 RESPONSE HEADERS (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", responseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + // this will clear the response headers + rspHeadersConsumer.reset(); + + completeResponse(response); + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java index c58f0b0c752..6ea196a4d1c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java @@ -41,6 +41,7 @@ import java.net.ProtocolException; import java.net.ProxySelector; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpTimeoutException; +import java.net.http.UnsupportedProtocolVersionException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; @@ -59,6 +60,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutionException; @@ -74,6 +76,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.stream.Stream; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -93,8 +96,16 @@ import jdk.internal.net.http.common.TimeSource; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.OperationTrackers.Trackable; import jdk.internal.net.http.common.OperationTrackers.Tracker; +import jdk.internal.net.http.common.Utils.SafeExecutor; +import jdk.internal.net.http.common.Utils.SafeExecutorService; import jdk.internal.net.http.websocket.BuilderImpl; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.util.Objects.requireNonNullElse; +import static java.util.Objects.requireNonNullElseGet; +import static jdk.internal.net.quic.QuicTLSContext.isQuicCompatible; + /** * Client implementation. Contains all configuration information and also * the selector manager thread which allows async events to be registered @@ -112,7 +123,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final int DEFAULT_KEEP_ALIVE_TIMEOUT = 30; static final long KEEP_ALIVE_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout", DEFAULT_KEEP_ALIVE_TIMEOUT); // Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property. - static final long IDLE_CONNECTION_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2); // Define the default factory as a static inner class // that embeds all the necessary logic to avoid @@ -145,15 +157,23 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final class DelegatingExecutor implements Executor { private final BooleanSupplier isInSelectorThread; private final Executor delegate; + private final SafeExecutor safeDelegate; private final BiConsumer errorHandler; DelegatingExecutor(BooleanSupplier isInSelectorThread, Executor delegate, BiConsumer errorHandler) { this.isInSelectorThread = isInSelectorThread; this.delegate = delegate; + this.safeDelegate = delegate instanceof ExecutorService svc + ? new SafeExecutorService(svc, ASYNC_POOL, errorHandler) + : new SafeExecutor<>(delegate, ASYNC_POOL, errorHandler); this.errorHandler = errorHandler; } + Executor safeDelegate() { + return safeDelegate; + } + Executor delegate() { return delegate; } @@ -325,6 +345,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final SelectorManager selmgr; private final FilterFactory filters; private final Http2ClientImpl client2; + private final Http3ClientImpl client3; + private final AltServicesRegistry registry; private final long id; private final String dbgTag; private final InetAddress localAddr; @@ -386,6 +408,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final AtomicLong pendingHttpOperationsCount = new AtomicLong(); private final AtomicLong pendingHttpRequestCount = new AtomicLong(); private final AtomicLong pendingHttp2StreamCount = new AtomicLong(); + private final AtomicLong pendingHttp3StreamCount = new AtomicLong(); private final AtomicLong pendingTCPConnectionCount = new AtomicLong(); private final AtomicLong pendingSubscribersCount = new AtomicLong(); private final AtomicBoolean isAlive = new AtomicBoolean(); @@ -429,14 +452,26 @@ final class HttpClientImpl extends HttpClient implements Trackable { id = CLIENT_IDS.incrementAndGet(); dbgTag = "HttpClientImpl(" + id +")"; localAddr = builder.localAddr; - if (builder.sslContext == null) { + version = requireNonNullElse(builder.version, Version.HTTP_2); + sslContext = requireNonNullElseGet(builder.sslContext, () -> { try { - sslContext = SSLContext.getDefault(); + return SSLContext.getDefault(); } catch (NoSuchAlgorithmException ex) { throw new UncheckedIOException(new IOException(ex)); } - } else { - sslContext = builder.sslContext; + }); + final boolean sslCtxSupportedForH3 = isQuicCompatible(sslContext); + if (version == Version.HTTP_3 && !sslCtxSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } + sslParams = requireNonNullElseGet(builder.sslParams, sslContext::getDefaultSSLParameters); + boolean sslParamsSupportedForH3 = sslParams.getProtocols() == null + || sslParams.getProtocols().length == 0 + || isQuicCompatible(sslParams); + if (version == Version.HTTP_3 && !sslParamsSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported - TLSv1.3 isn't configured on SSLParameters")); } Executor ex = builder.executor; if (ex == null) { @@ -450,7 +485,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { this::onSubmitFailure); facadeRef = new WeakReference<>(facadeFactory.createFacade(this)); implRef = new WeakReference<>(this); - client2 = new Http2ClientImpl(this); cookieHandler = builder.cookieHandler; connectTimeout = builder.connectTimeout; followRedirects = builder.followRedirects == null ? @@ -462,17 +496,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { debug.log("proxySelector is %s (user-supplied=%s)", this.proxySelector, userProxySelector != null); authenticator = builder.authenticator; - if (builder.version == null) { - version = HttpClient.Version.HTTP_2; - } else { - version = builder.version; - } - if (builder.sslParams == null) { - sslParams = getDefaultParams(sslContext); - } else { - sslParams = builder.sslParams; - } + boolean h3Supported = sslCtxSupportedForH3 && sslParamsSupportedForH3; + registry = new AltServicesRegistry(id); connections = new ConnectionPool(id); + client2 = new Http2ClientImpl(this); + client3 = h3Supported ? new Http3ClientImpl(this) : null; connections.start(); timeouts = new TreeSet<>(); try { @@ -518,6 +546,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { client2.stop(); // make sure all subscribers are completed closeSubscribers(); + // close client3 + if (client3 != null) { + // close client3 + client3.stop(); + } // close TCP connection if any are still opened openedConnections.forEach(this::closeConnection); // shutdown the executor if needed @@ -610,11 +643,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { return isStarted.get() && !isAlive.get(); } - private static SSLParameters getDefaultParams(SSLContext ctx) { - SSLParameters params = ctx.getDefaultSSLParameters(); - return params; - } - // Returns the facade that was returned to the application code. // May be null if that facade is no longer referenced. final HttpClientFacade facade() { @@ -664,12 +692,14 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long count = pendingOperationCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -681,10 +711,35 @@ final class HttpClientImpl extends HttpClient implements Trackable { return pendingOperationCount.incrementAndGet(); } + // Increments the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamReference() { + pendingHttp3StreamCount.incrementAndGet(); + return pendingOperationCount.incrementAndGet(); + } + // Decrements the pendingHttp2StreamCount and pendingOperationCount. final long streamUnreference() { final long count = pendingOperationCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.decrementAndGet(); + final long http3Count = pendingHttp3StreamCount.get(); + final long httpCount = pendingHttpOperationsCount.get(); + final long webSocketCount = pendingWebSocketCount.get(); + if (count == 0 && facadeRef.refersTo(null)) { + selmgr.wakeupSelector(); + } + assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; + assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; + assert webSocketCount >= 0 : "count of WS operations < 0"; + assert count >= 0 : "count of pending operations < 0"; + return count; + } + + // Decrements the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamUnreference() { + final long count = pendingOperationCount.decrementAndGet(); + final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { @@ -692,6 +747,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -709,11 +765,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long webSocketCount = pendingWebSocketCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -732,6 +790,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { final AtomicLong requestCount; final AtomicLong httpCount; final AtomicLong http2Count; + final AtomicLong http3Count; final AtomicLong websocketCount; final AtomicLong operationsCount; final AtomicLong connnectionsCount; @@ -744,6 +803,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpClientTracker(AtomicLong request, AtomicLong http, AtomicLong http2, + AtomicLong http3, AtomicLong ws, AtomicLong ops, AtomicLong conns, @@ -756,6 +816,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { this.requestCount = request; this.httpCount = http; this.http2Count = http2; + this.http3Count = http3; this.websocketCount = ws; this.operationsCount = ops; this.connnectionsCount = conns; @@ -787,6 +848,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { @Override public long getOutstandingHttp2Streams() { return http2Count.get(); } @Override + public long getOutstandingHttp3Streams() { return http3Count.get(); } + @Override public long getOutstandingWebSocketOperations() { return websocketCount.get(); } @@ -811,6 +874,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { pendingHttpRequestCount, pendingHttpOperationsCount, pendingHttp2StreamCount, + pendingHttp3StreamCount, pendingWebSocketCount, pendingOperationCount, pendingTCPConnectionCount, @@ -866,6 +930,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Thread.currentThread() == selmgr; } + AltServicesRegistry registry() { return registry; } + boolean isSelectorClosed() { return selmgr.isClosed(); } @@ -878,6 +944,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { return client2; } + Optional client3() { + return Optional.ofNullable(client3); + } + private void debugCompleted(String tag, long startNanos, HttpRequest req) { if (debugelapsed.on()) { debugelapsed.log(tag + " elapsed " @@ -906,6 +976,12 @@ final class HttpClientImpl extends HttpClient implements Trackable { } throw ie; } catch (ExecutionException e) { + // Exceptions are often thrown from asynchronous code, and the + // stacktrace may not always contain the application classes. That + // makes it difficult to trace back to the application code which + // invoked the `HttpClient`. Here we instantiate/recreate the + // exceptions to capture the application's calling code in the + // stacktrace of the thrown exception. final Throwable throwable = e.getCause(); final String msg = throwable.getMessage(); @@ -917,6 +993,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg); hcte.initCause(throwable); throw hcte; + } else if (throwable instanceof UnsupportedProtocolVersionException) { + var upve = new UnsupportedProtocolVersionException(msg); + upve.initCause(throwable); + throw upve; } else if (throwable instanceof HttpTimeoutException) { throw new HttpTimeoutException(msg); } else if (throwable instanceof ConnectException) { @@ -972,6 +1052,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { return MinimalFuture.failedFuture(new IOException("closed")); } + final HttpClient.Version vers = userRequest.version().orElse(this.version()); + if (vers == Version.HTTP_3 && client3 == null + && userRequest.getOption(H3_DISCOVERY).orElse(null) == HTTP_3_URI_ONLY) { + // HTTP3 isn't supported by this client + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } // should not happen, unless the selector manager has // exited abnormally if (selmgr.isClosed()) { @@ -1025,6 +1112,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { res = registerPending(pending, res); if (exchangeExecutor != null) { + // We're called by `sendAsync()` - make sure we translate exceptions + res = translateSendAsyncExecFailure(res); // makes sure that any dependent actions happen in the CF default // executor. This is only needed for sendAsync(...), when // exchangeExecutor is non-null. @@ -1042,6 +1131,31 @@ final class HttpClientImpl extends HttpClient implements Trackable { } } + /** + * {@return a new {@code CompletableFuture} wrapping the + * {@link #sendAsync(HttpRequest, BodyHandler, PushPromiseHandler, Executor) sendAsync()} + * execution failures with, as per specification, {@link IOException}, if necessary} + */ + private static CompletableFuture> translateSendAsyncExecFailure( + CompletableFuture> responseFuture) { + return responseFuture + .handle((response, exception) -> { + if (exception == null) { + return MinimalFuture.completedFuture(response); + } + var unwrappedException = Utils.getCompletionCause(exception); + // Except `Error` and `CancellationException`, wrap failures inside an `IOException`. + // This is required to comply with the specification of `HttpClient::sendAsync`. + var translatedException = unwrappedException instanceof Error + || unwrappedException instanceof CancellationException + || unwrappedException instanceof IOException + ? unwrappedException + : new IOException(unwrappedException); + return MinimalFuture.>failedFuture(translatedException); + }) + .thenCompose(Function.identity()); + } + // Main loop for this client's selector private static final class SelectorManager extends Thread { @@ -1095,8 +1209,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { } IOException selectorClosedException() { - var io = new IOException("selector manager closed"); - var cause = errorRef.get(); + final var cause = errorRef.get(); + final String msg = cause == null + ? "selector manager closed" + : "selector manager closed due to: " + cause; + final var io = new IOException(msg); if (cause != null) { io.initCause(cause); } @@ -1181,6 +1298,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { } // double check after closing abortPendingRequests(owner, t); + var client3 = owner.client3; + if (client3 != null) { + client3.abort(t); + } IOException io = toAbort.isEmpty() ? null : selectorClosedException(); @@ -1456,8 +1577,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { String keyInterestOps = key.isValid() ? "key.interestOps=" + Utils.interestOps(key) : "invalid key"; return String.format("channel registered with selector, %s, sa.interestOps=%s", - keyInterestOps, - Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); + keyInterestOps, + Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); } catch (Throwable t) { return String.valueOf(t); } @@ -1627,8 +1748,12 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Optional.ofNullable(connectTimeout); } - Optional idleConnectionTimeout() { - return Optional.ofNullable(getIdleConnectionTimeout()); + Optional idleConnectionTimeout(Version version) { + return switch (version) { + case HTTP_2 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H2); + case HTTP_3 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H3); + case HTTP_1_1 -> timeoutDuration(KEEP_ALIVE_TIMEOUT); + }; } @Override @@ -1755,7 +1880,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { // error from here - but in this case there's not much we // could do anyway. Just let it flow... if (failed == null) failed = e; - else failed.addSuppressed(e); + else Utils.addSuppressed(failed, e); Log.logTrace("Failed to handle event {0}: {1}", event, e); } } @@ -1799,10 +1924,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { return sslBufferSupplier; } - private Duration getIdleConnectionTimeout() { - if (IDLE_CONNECTION_TIMEOUT >= 0) - return Duration.ofSeconds(IDLE_CONNECTION_TIMEOUT); - return null; + private Optional timeoutDuration(long seconds) { + if (seconds >= 0) { + return Optional.of(Duration.ofSeconds(seconds)); + } + return Optional.empty(); } private static long getTimeoutProp(String prop, long def) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java index 07cfc4dbdf6..0219b0960d7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.http.HttpResponse; import java.nio.ByteBuffer; +import java.nio.channels.NetworkChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; import java.util.Comparator; @@ -57,7 +58,10 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter; import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Utils; + +import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static jdk.internal.net.http.common.Utils.ProxyHeaders; /** @@ -69,12 +73,13 @@ import static jdk.internal.net.http.common.Utils.ProxyHeaders; * PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server * AsyncSSLConnection: TLS channel direct to server * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel + * HttpQuicConnection: direct QUIC connection to server */ abstract class HttpConnection implements Closeable { final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG); static final Logger DEBUG_LOGGER = Utils.getDebugLogger( - () -> "HttpConnection(SocketTube(?))", Utils.DEBUG); + () -> "HttpConnection", Utils.DEBUG); public static final Comparator COMPARE_BY_ID = Comparator.comparing(HttpConnection::id); @@ -112,8 +117,8 @@ abstract class HttpConnection implements Closeable { this.label = label; } - private static String nextLabel() { - return "" + LABEL_COUNTER.incrementAndGet(); + private static String nextLabel(String prefix) { + return prefix + LABEL_COUNTER.incrementAndGet(); } /** @@ -198,9 +203,17 @@ abstract class HttpConnection implements Closeable { abstract InetSocketAddress proxy(); /** Tells whether, or not, this connection is open. */ - final boolean isOpen() { + boolean isOpen() { return channel().isOpen() && - (connected() ? !getConnectionFlow().isFinished() : true); + (connected() ? !isFlowFinished() : true); + } + + /** + * {@return {@code true} if the {@linkplain #getConnectionFlow() + * connection flow} is {@linkplain FlowTube#isFinished() finished}. + */ + boolean isFlowFinished() { + return getConnectionFlow().isFinished(); } /** @@ -232,13 +245,17 @@ abstract class HttpConnection implements Closeable { * still open, and the method returns true. * @return true if the channel appears to be still open. */ - final boolean checkOpen() { + boolean checkOpen() { if (isOpen()) { try { // channel is non blocking - int read = channel().read(ByteBuffer.allocate(1)); - if (read == 0) return true; - close(); + if (channel() instanceof SocketChannel channel) { + int read = channel.read(ByteBuffer.allocate(1)); + if (read == 0) return true; + close(); + } else { + return channel().isOpen(); + } } catch (IOException x) { debug.log("Pooled connection is no longer operational: %s", x.toString()); @@ -294,6 +311,7 @@ abstract class HttpConnection implements Closeable { * is one of the following: * {@link PlainHttpConnection} * {@link PlainTunnelingConnection} + * {@link HttpQuicConnection} * * The returned connection, if not from the connection pool, must have its, * connect() or connectAsync() method invoked, which ( when it completes @@ -301,6 +319,7 @@ abstract class HttpConnection implements Closeable { */ public static HttpConnection getConnection(InetSocketAddress addr, HttpClientImpl client, + Exchange exchange, HttpRequestImpl request, Version version) { // The default proxy selector may select a proxy whose address is @@ -322,18 +341,27 @@ abstract class HttpConnection implements Closeable { return getPlainConnection(addr, proxy, request, client); } } else { // secure - if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool + if (version == HTTP_1_1) { // only HTTP/1.1 connections are in the pool c = pool.getConnection(true, addr, proxy); } if (c != null && c.isOpen()) { - final HttpConnection conn = c; - if (DEBUG_LOGGER.on()) - DEBUG_LOGGER.log(conn.getConnectionFlow() - + ": SSL connection retrieved from HTTP/1.1 pool"); + if (DEBUG_LOGGER.on()) { + DEBUG_LOGGER.log(c.getConnectionFlow() + + ": SSL connection retrieved from HTTP/1.1 pool"); + } return c; + } else if (version == HTTP_3 && client.client3().isPresent()) { + // We only come here after we have checked the HTTP/3 connection pool, + // and if the client config supports HTTP/3 + if (DEBUG_LOGGER.on()) + DEBUG_LOGGER.log("Attempting to get an HTTP/3 connection"); + return HttpQuicConnection.getHttpQuicConnection(addr, proxy, request, exchange, client); } else { + assert !request.isHttp3Only(version); // should have failed before String[] alpn = null; if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) { + // We only come here after we have checked the HTTP/2 connection pool. + // We will not negotiate HTTP/2 if we don't have the appropriate TLS version alpn = new String[] { Alpns.H2, Alpns.HTTP_1_1 }; } return getSSLConnection(addr, proxy, alpn, request, client); @@ -346,7 +374,7 @@ abstract class HttpConnection implements Closeable { String[] alpn, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tls:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -433,7 +461,7 @@ abstract class HttpConnection implements Closeable { InetSocketAddress proxy, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tcp:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -483,7 +511,7 @@ abstract class HttpConnection implements Closeable { /* Tells whether or not this connection is a tunnel through a proxy */ boolean isTunnel() { return false; } - abstract SocketChannel channel(); + abstract NetworkChannel channel(); final InetSocketAddress address() { return address; @@ -516,6 +544,19 @@ abstract class HttpConnection implements Closeable { close(); } + /** + * {@return the underlying connection flow, if applicable} + * + * @apiNote + * TCP based protocols like HTTP/1.1 and HTTP/2 are built on + * top of a {@linkplain FlowTube bidirectional connection flow}. + * On the other hand, Quic based protocol like HTTP/3 are + * multiplexed at the Quic level, and therefore do not have + * a connection flow. + * + * @throws IllegalStateException if the underlying transport + * does not expose a single connection flow. + */ abstract FlowTube getConnectionFlow(); /** diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java new file mode 100644 index 00000000000..bbbe1157cdf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java @@ -0,0 +1,690 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.nio.channels.NetworkChannel; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.ConnectionPool.CacheKey; +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_DIRECT_CONNECTION_TIMEOUT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.appLayerException; + +/** + * An {@code HttpQuicConnection} models an HTTP connection over + * QUIC. + * The particulars of the HTTP/3 protocol are handled by the + * Http3Connection class. + */ +abstract class HttpQuicConnection extends HttpConnection { + + final Logger debug = Utils.getDebugLogger(this::quicDbgString); + + final QuicConnection quicConnection; + final ConnectionTerminator quicConnTerminator; + // the alt-service which was advertised, from some origin, for this connection co-ordinates. + // can be null, which indicates this wasn't created because of an alt-service + private final AltService sourceAltService; + // HTTP/2 MUST use TLS version 1.3 or higher for HTTP/3 over TLS + private static final Predicate testRequiredHTTP3TLSVersion = proto -> + proto.equals("TLSv1.3"); + + + HttpQuicConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client, + QuicConnection quicConnection, AltService sourceAltService) { + super(originServer, address, client, "quic:" + quicConnection.uniqueId()); + Objects.requireNonNull(quicConnection); + this.quicConnection = quicConnection; + this.quicConnTerminator = quicConnection.connectionTerminator(); + this.sourceAltService = sourceAltService; + } + + /** + * A HTTP QUIC connection could be created due to an alt-service that was advertised + * from some origin. This method returns that source alt-service if there was one. + * @return The source alt-service if present + */ + Optional getSourceAltService() { + return Optional.ofNullable(this.sourceAltService); + } + + @Override + public List getSNIServerNames() { + final SSLParameters sslParams = this.quicConnection.getTLSEngine().getSSLParameters(); + if (sslParams == null) { + return List.of(); + } + final List sniServerNames = sslParams.getServerNames(); + if (sniServerNames == null) { + return List.of(); + } + return List.copyOf(sniServerNames); + } + + final String quicDbgString() { + String tag = dbgTag; + if (tag == null) tag = dbgTag = "Http" + quicConnection.dbgTag(); + return tag; + } + + /** + * Initiates the connect phase. + * + * Returns a CompletableFuture that completes when the underlying + * TCP connection has been established or an error occurs. + */ + public abstract CompletableFuture connectAsync(Exchange exchange); + + private volatile boolean connected; + /** + * Finishes the connection phase. + * + * Returns a CompletableFuture that completes when any additional, + * type specific, setup has been done. Must be called after connectAsync. + */ + public CompletableFuture finishConnect() { + this.connected = true; + return MinimalFuture.completedFuture(null); + } + + /** Tells whether, or not, this connection is connected to its destination. */ + boolean connected() { + return connected; + } + + /** Tells whether, or not, this connection is secure ( over SSL ) */ + final boolean isSecure() { return true; } // QUIC is secure + + /** + * Tells whether, or not, this connection is proxied. + * Returns true for tunnel connections, or clear connection to + * any host through proxy. + */ + final boolean isProxied() { return false;} // Proxy not supported + + /** + * Returns the address of the proxy used by this connection. + * Returns the proxy address for tunnel connections, or + * clear connection to any host through proxy. + * Returns {@code null} otherwise. + */ + final InetSocketAddress proxy() { return null; } // Proxy not supported + + /** + * This method throws an {@link UnsupportedOperationException} + */ + @Override + final HttpPublisher publisher() { + throw new UnsupportedOperationException("no publisher for a quic connection"); + } + + QuicConnection quicConnection() { + return quicConnection; + } + + /** + * Returns true if the given client's SSL parameter protocols contains at + * least one TLS version that HTTP/3 requires. + */ + private static boolean hasRequiredHTTP3TLSVersion(HttpClient client) { + String[] protos = client.sslParameters().getProtocols(); + if (protos != null) { + return Arrays.stream(protos).anyMatch(testRequiredHTTP3TLSVersion); + } else { + return false; + } + } + + /** + * Called when the HTTP/3 connection is established, either successfully or + * unsuccessfully + * @param connection the HTTP/3 connection, if successful, or null, otherwise + * @param throwable the exception encountered, if unsuccessful + */ + public abstract void connectionEstablished(Http3Connection connection, + Throwable throwable); + + /** + * A functional interface used to update the Alternate Service Registry + * after a direct connection attempt. + */ + @FunctionalInterface + private interface DirectConnectionUpdater { + /** + * This method may update the HttpClient registry, or + * {@linkplain Http3ClientImpl#noH3(String) record the unsuccessful} + * direct connection attempt. + * + * @param conn the connection or null + * @param throwable the exception or null + */ + void onConnectionEstablished( + Http3Connection conn, Throwable throwable); + + /** + * Does nothing + * @param conn the connection + * @param throwable the exception + */ + static void noUpdate( + Http3Connection conn, Throwable throwable) { + } + } + + /** + * This method create and return a new unconnected HttpQuicConnection, + * wrapping a {@link QuicConnection}. May return {@code null} if + * HTTP/3 is not supported with the given parameters. For instance, + * if TLSv1.3 isn't available/enabled in the client's SSLParameters, + * or if ALT_SERVICE is required but no alt service is found. + * + * @param addr the HTTP/3 peer endpoint address, if direct connection + * @param proxy the proxy address, if a proxy is used, in which case this + * method will return {@code null} as proxying is not supported + * with HTTP/3 + * @param request the request for which the connection is being created + * @param exchange the exchange for which the connection is being created + * @param client the HttpClientImpl instance + * @return A new HttpQuicConnection or {@code null} + */ + public static HttpQuicConnection getHttpQuicConnection(final InetSocketAddress addr, + final InetSocketAddress proxy, + final HttpRequestImpl request, + final Exchange exchange, + final HttpClientImpl client) { + if (!client.client3().isPresent()) { + if (Log.http3()) { + Log.logHttp3("HTTP3 isn't supported by the client"); + } + return null; + } + + final Http3ClientImpl h3client = client.client3().get(); + // HTTP_3 with proxy not supported; In this case we will downgrade + // to using HTTP/2 + var debug = h3client.debug(); + var where = "HttpQuicConnection.getHttpQuicConnection"; + if (proxy != null || !hasRequiredHTTP3TLSVersion(client)) { + if (debug.on()) + debug.log("%s: proxy required or SSL version mismatch", where); + return null; + } + + assert request.secure(); + // Question: Do we need this scaffolding? + // I mean - could Http3Connection and HttpQuicConnection be the same + // object? + // Answer: Http3Connection models an established connection which is + // ready to be used. + // HttpQuicConnection serves at establishing a new Http3Connection + // => Http3Connection is pooled, HttpQuicConnection is not. + // => Do we need HttpQuicConnection vs QuicConnection? + // => yes: HttpQuicConnection can access all package protected + // APIs in HttpConnection & al + // QuicConnection is in the quic subpackage. + // HttpQuicConnection makes the necessary adaptation between + // HttpConnection and QuicConnection. + + // find whether we have an alternate service access point for HTTP/3 + // if we do, create a new QuicConnection and a new Http3Connection over it. + var uri = request.uri(); + var config = request.http3Discovery(); + if (debug.on()) { + debug.log("Checking ALT-SVC regardless of H3_DISCOVERY settings"); + } + // we only support H3 right now + var altSvc = client.registry() + .lookup(uri, H3::equals) + .findFirst().orElse(null); + Optional directTimeout = Optional.empty(); + final boolean advertisedAltSvc = altSvc != null && altSvc.wasAdvertised(); + logAltSvcFor(debug, uri, altSvc, where); + switch (config) { + case ALT_SVC: { + if (!advertisedAltSvc) { + // fallback to HTTP/2 + if (altSvc != null) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use unadvertised AltService: {1}", + config, altSvc); + } + } + return null; + } + assert altSvc != null && altSvc.wasAdvertised(); + break; + } + // attempt direct connection if HTTP/3 only + case HTTP_3_URI_ONLY: { + if (advertisedAltSvc && !altSvc.originHasSameAuthority()) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use advertised AltService: {1}", + config, altSvc); + } + altSvc = null; + } + assert altSvc == null || altSvc.originHasSameAuthority(); + break; + } + default: { + // if direct connection already attempted and failed, + // fallback to HTTP/2 + if (altSvc == null && h3client.hasNoH3(uri.getRawAuthority())) { + return null; + } + if (!advertisedAltSvc) { + // directTimeout is only used for happy eyeball + Duration def = Duration.ofMillis(MAX_DIRECT_CONNECTION_TIMEOUT); + Duration timeout = client.connectTimeout() + .filter(d -> d.compareTo(def) <= 0) + .orElse(def); + directTimeout = Optional.of(timeout); + } + break; + } + } + + if (altSvc != null) { + assert H3.equals(altSvc.alpn()); + Log.logAltSvc("{0}: Using AltService for {1}: {2}", + config, uri.getRawAuthority(), altSvc); + } + if (debug.on()) { + debug.log("%s: creating QuicConnection for: %s", where, uri); + } + final QuicConnection quicConnection = (altSvc != null) ? + h3client.quicClient().createConnectionFor(altSvc) : + h3client.quicClient().createConnectionFor(addr, new String[] {H3}); + if (debug.on()) debug.log("%s: QuicConnection: %s", where, quicConnection); + final DirectConnectionUpdater onConnectFinished = advertisedAltSvc + ? DirectConnectionUpdater::noUpdate + : (c,t) -> registerUnadvertised(client, uri, addr, c, t); + // Note: we could get rid of the updater by introducing + // H3DirectQuicConnectionImpl extends H3QuicConnectionImpl + HttpQuicConnection httpQuicConn = new H3QuicConnectionImpl(Origin.from(request.uri()), addr, client, + quicConnection, onConnectFinished, directTimeout, altSvc); + // if we created a connection and if that connection is to an (advertised) alt service then + // we setup the Exchange's request to include the "alt-used" header to refer to the + // alt service that was used (section 5, RFC-7838) + if (httpQuicConn != null && altSvc != null && advertisedAltSvc) { + exchange.request().setSystemHeader("alt-used", altSvc.authority()); + } + return httpQuicConn; + } + + private static void logAltSvcFor(Logger debug, URI uri, AltService altSvc, String where) { + if (altSvc == null) { + if (Log.altsvc()) { + Log.logAltSvc("No AltService found for {0}", uri.getRawAuthority()); + } else if (debug.on()) { + debug.log("%s: No ALT-SVC for %s", where, uri.getRawAuthority()); + } + } else { + if (debug.on()) debug.log("%s: ALT-SVC: %s", where, altSvc); + } + } + + static void registerUnadvertised(final HttpClientImpl client, + final URI requestURI, + final InetSocketAddress destAddr, + final Http3Connection connection, + final Throwable t) { + if (t == null && connection != null) { + // There is an h3 endpoint at the given origin: update the registry + final Origin origin = connection.connection().getOriginServer(); + assert origin != null : "origin server is null on connection: " + + connection.connection(); + assert origin.port() == destAddr.getPort(); + var id = new AltService.Identity(H3, origin.host(), origin.port()); + client.registry().registerUnadvertised(id, origin, connection.connection()); + return; + } + if (t != null) { + assert client.client3().isPresent() : "HTTP3 isn't supported by the client"; + final URI originURI = requestURI.resolve("/"); + // record that there is no h3 at the given origin + client.client3().get().noH3(originURI.getRawAuthority()); + } + } + + // TODO: we could probably merge H3QuicConnectionImpl with HttpQuicConnection now + static class H3QuicConnectionImpl extends HttpQuicConnection { + private final Optional directTimeout; + private final DirectConnectionUpdater connFinishedAction; + H3QuicConnectionImpl(Origin originServer, + InetSocketAddress address, + HttpClientImpl client, + QuicConnection quic, + DirectConnectionUpdater connFinishedAction, + Optional directTimeout, + AltService sourceAltService) { + super(originServer, address, client, quic, sourceAltService); + this.directTimeout = directTimeout; + this.connFinishedAction = connFinishedAction; + } + + @Override + public CompletableFuture connectAsync(Exchange exchange) { + var request = exchange.request(); + var uri = request.uri(); + // Adapt HandshakeCF to CompletableFuture + CompletableFuture> handshakeCfCf = + quicConnection.startHandshake() + .handle((r, t) -> { + if (t == null) { + // successful handshake + return MinimalFuture.completedFuture(r); + } + final TerminationCause terminationCause = quicConnection.terminationCause(); + final boolean appLayerTermination = terminationCause != null + && terminationCause.isAppLayer(); + // QUIC connection handshake failed. we now decide whether we should + // unregister the alt-service (if any) that was the source of this + // connection attempt. + // + // handshake could have failed for one of several reasons, some of them: + // - something at QUIC layer caused the failure (either some internal + // exception or protocol error or QUIC TLS error) + // - or the app layer, through the HttpClient/HttpConnection + // could have triggered a connection close. + // + // we unregister the alt-service (if any) only if the termination cause + // originated in the QUIC layer. An app layer termination cause doesn't + // necessarily mean that the alt-service isn't valid for subsequent use. + if (!appLayerTermination && this.getSourceAltService().isPresent()) { + final AltService altSvc = this.getSourceAltService().get(); + if (debug.on()) { + debug.log("connection attempt to an alternate service at " + + altSvc.authority() + " failed during handshake: " + t); + } + client().registry().markInvalid(this.getSourceAltService().get()); + // fail with ConnectException to allow the request to potentially + // be retried on a different connection + final ConnectException connectException = new ConnectException( + "QUIC connection handshake to an alternate service failed"); + connectException.initCause(t); + return MinimalFuture.failedFuture(connectException); + } else { + // alt service wasn't the cause of this failed connection attempt. + // return a failed future with the original cause + return MinimalFuture.failedFuture(t); + } + }) + .thenApply((handshakeCompletion) -> { + if (handshakeCompletion.isCompletedExceptionally()) { + return MinimalFuture.failedFuture(handshakeCompletion.exceptionNow()); + } + return MinimalFuture.completedFuture(null); + }); + + // In case of direct connection, set up a timeout on the handshakeReachedPeerCf, + // and arrange for it to complete the handshakeCfCf above with a timeout in + // case that timeout expires... + if (directTimeout.isPresent()) { + debug.log("setting up quic direct connect timeout: " + directTimeout.get().toMillis()); + var handshakeReachedPeerCf = quicConnection.handshakeReachedPeer(); + CompletableFuture> fxi2 = handshakeReachedPeerCf + .thenApply((unused) -> MinimalFuture.completedFuture(null)); + fxi2 = fxi2.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic handshake timeout")), + directTimeout.get().toMillis(), TimeUnit.MILLISECONDS); + fxi2.handleAsync((r, t) -> { + if (t != null) { + var cause = Utils.getCompletionCause(t); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + if (r.isCompletedExceptionally()) { + var cause = Utils.getCompletionCause(r.exceptionNow()); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + return r; + }, exchange.parentExecutor.safeDelegate()); + } + + Optional timeout = client().connectTimeout(); + CompletableFuture> fxi = handshakeCfCf; + + // In case of connection timeout, set up a timeout on the handshakeCfCf. + // Note: this is a different timeout than the direct connection timeout. + if (timeout.isPresent()) { + // In case of timeout we need to close the quic connection + debug.log("setting up quic connect timeout: " + timeout.get().toMillis()); + fxi = handshakeCfCf.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic connect timeout")), + timeout.get().toMillis(), TimeUnit.MILLISECONDS); + } + + // If we have set up any timeout, arrange to close the quicConnection + // if one of the timeout expires + if (timeout.isPresent() || directTimeout.isPresent()) { + fxi = fxi.handleAsync(this::handleTimeout, exchange.parentExecutor.safeDelegate()); + } + return fxi.thenCompose(Function.identity()); + } + + @Override + public void connectionEstablished(Http3Connection connection, + Throwable throwable) { + connFinishedAction.onConnectionEstablished(connection, throwable); + } + + private CompletableFuture handleTimeout(CompletableFuture r, Throwable t) { + if (t != null) { + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Timeout expired: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + return MinimalFuture.failedFuture(te); + } + return MinimalFuture.failedFuture(t); + } else if (r.isCompletedExceptionally()) { + t = r.exceptionNow(); + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Completed in timeout: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + } + } + return r; + } + + + @Override + NetworkChannel /* DatagramChannel */ channel() { + // Note: revisit this + // - don't return a new instance each time + // - see if we could avoid exposing + // the channel in the first place + H3QuicConnectionImpl self = this; + return new NetworkChannel() { + @Override + public NetworkChannel bind(SocketAddress local) throws IOException { + throw new UnsupportedOperationException("no bind for a quic connection"); + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return quicConnection.localAddress(); + } + + @Override + public NetworkChannel setOption(SocketOption name, T value) throws IOException { + return this; + } + + @Override + public T getOption(SocketOption name) throws IOException { + return null; + } + + @Override + public Set> supportedOptions() { + return Set.of(); + } + + @Override + public boolean isOpen() { + return quicConnection.isOpen(); + } + + @Override + public void close() throws IOException { + self.close(); + } + }; + } + + @Override + CacheKey cacheKey() { + return null; + } + + // close with H3_NO_ERROR + @Override + public final void close() { + close(H3_NO_ERROR.code(), "connection closed", null); + } + + @Override + void close(final Throwable cause) { + close(H3_INTERNAL_ERROR.code(), null, cause); + } + } + + /* Tells whether this connection is a tunnel through a proxy */ + boolean isTunnel() { return false; } + + abstract NetworkChannel /* DatagramChannel */ channel(); + + abstract ConnectionPool.CacheKey cacheKey(); + + /** + * Closes the underlying transport connection with + * the given {@code connCloseCode} code. This will be considered a application + * layer close and will generate a {@code ConnectionCloseFrame} + * of type {@code 0x1d} as the cause of the termination. + * + * @param connCloseCode the connection close code + * @param logMsg the message to be included in the logs as + * the cause of the connection termination. can be null. + * @param closeCause the underlying cause of the connection termination. can be null, + * in which case just the {@code error} will be recorded as the + * cause of the connection termination. + */ + final void close(final long connCloseCode, final String logMsg, + final Throwable closeCause) { + final TerminationCause terminationCause; + if (closeCause == null) { + terminationCause = appLayerClose(connCloseCode); + } else { + terminationCause = appLayerException(connCloseCode, closeCause); + } + // set the log message only if non-null, else let it default to internal + // implementation sensible default + if (logMsg != null) { + terminationCause.loggedAs(logMsg); + } + quicConnTerminator.terminate(terminationCause); + } + + abstract void close(final Throwable t); + + /** + * {@inheritDoc} + * + * @implSpec + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. Therefore, the higher HTTP + * layer should never call {@code getConnectionFlow()} on an + * {@link HttpQuicConnection}. As a consequence, this method + * always throws {@link IllegalStateException} unconditionally. + * + * @return nothing: this method always throw {@link IllegalStateException} + * + * @throws IllegalStateException always + */ + @Override + final FlowTube getConnectionFlow() { + throw new IllegalStateException( + "An HTTP/3 connection does not expose " + + "a single connection flow"); + } + + /** + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. This method instead will + * return {@code true} if the underlying quic connection + * has been terminated, either exceptionally or normally. + * + * @return {@code true} if the underlying Quic connection + * has been terminated. + */ + @Override + boolean isFlowFinished() { + return !quicConnection().isOpen(); + } + + @Override + public String toString() { + return quicDbgString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java index a495fcce1ee..c68965626b7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java @@ -26,12 +26,18 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; +import java.util.Set; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; @@ -51,6 +57,10 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { private BodyPublisher bodyPublisher; private volatile Optional version; private Duration duration; + private final Map, Object> options = new HashMap<>(); + + private static final Set> supportedOptions = + Set.of(HttpOption.H3_DISCOVERY); public HttpRequestBuilderImpl(URI uri) { requireNonNull(uri, "uri must be non-null"); @@ -100,6 +110,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { b.uri = uri; b.duration = duration; b.version = version; + b.options.putAll(Map.copyOf(options)); return b; } @@ -158,6 +169,19 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { return this; } + @Override + public Builder setOption(HttpOption option, T value) { + Objects.requireNonNull(option, "option"); + if (value == null) options.remove(option); + else if (supportedOptions.contains(option)) { + if (!option.type().isInstance(value)) { + throw newIAE("Illegal value type %s for %s", value, option); + } + options.put(option, value); + } // otherwise just ignore the option + return this; + } + HttpHeadersBuilder headersBuilder() { return headersBuilder; } URI uri() { return uri; } @@ -170,6 +194,8 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Optional version() { return version; } + Map, Object> options() { return options; } + @Override public HttpRequest.Builder GET() { return method0("GET", null); @@ -245,4 +271,30 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Duration timeout() { return duration; } + public static Map, Object> copySupportedOptions(HttpRequest request) { + Objects.requireNonNull(request, "request"); + if (request instanceof ImmutableHttpRequest ihr) { + // already checked and immutable + return ihr.options(); + } + Map, Object> options = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = request.getOption(option); + if (!val.isPresent()) continue; + options.put(option, option.type().cast(val.get())); + } + return Map.copyOf(options); + } + + public static Map, Object> copySupportedOptions(Map, Object> options) { + Objects.requireNonNull(options, "option"); + Map, Object> result = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = options.get(option); + if (val == null) continue; + result.put(option, option.type().cast(val)); + } + return Map.copyOf(result); + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java index 81c693ea192..00730baa0ce 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java @@ -31,9 +31,13 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.time.Duration; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; @@ -46,6 +50,7 @@ import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.websocket.WebSocketRequest; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.Authenticator.RequestorType.SERVER; import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS; import static jdk.internal.net.http.common.Utils.ProxyHeaders; @@ -65,6 +70,8 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { private volatile boolean isWebSocket; private final Duration timeout; // may be null private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; private volatile boolean userSetAuthorization; private volatile boolean userSetProxyAuthorization; @@ -92,6 +99,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.requestPublisher = builder.bodyPublisher(); // may be null this.timeout = builder.timeout(); this.version = builder.version(); + this.options = Map.copyOf(builder.options()); this.authority = null; } @@ -110,12 +118,13 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { "uri must be non null"); Duration timeout = request.timeout().orElse(null); this.method = method == null ? "GET" : method; + this.options = HttpRequestBuilderImpl.copySupportedOptions(request); this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER); - if (request instanceof HttpRequestImpl) { + if (request instanceof HttpRequestImpl impl) { // all cases exception WebSocket should have a new system headers - this.isWebSocket = ((HttpRequestImpl) request).isWebSocket; + this.isWebSocket = impl.isWebSocket; if (isWebSocket) { - this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder; + this.systemHeadersBuilder = impl.systemHeadersBuilder; } else { this.systemHeadersBuilder = new HttpHeadersBuilder(); } @@ -199,6 +208,19 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = other.timeout; this.version = other.version(); this.authority = null; + this.options = other.optionsFor(this.uri); + } + + private Map, Object> optionsFor(URI uri) { + if (this.uri == uri || Objects.equals(this.uri.getRawAuthority(), uri.getRawAuthority())) { + return options; + } + // preserve config if version is HTTP/3 + if (version.orElse(null) == Version.HTTP_3) { + Http3DiscoveryMode h3DiscoveryMode = (Http3DiscoveryMode)options.get(H3_DISCOVERY); + if (h3DiscoveryMode != null) return Map.of(H3_DISCOVERY, h3DiscoveryMode); + } + return Map.of(); } private BodyPublisher publisher(HttpRequestImpl other) { @@ -234,12 +256,26 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { // What we want to possibly upgrade is the tunneled connection to the // target server (so not the CONNECT request itself) this.version = Optional.of(HttpClient.Version.HTTP_1_1); + this.options = Map.of(); } final boolean isConnect() { return "CONNECT".equalsIgnoreCase(method); } + final boolean isHttp3Only(Version version) { + return version == Version.HTTP_3 && http3Discovery() == HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + } + + final Http3DiscoveryMode http3Discovery() { + // see if discovery mode is set on the request + final var h3Discovery = getOption(H3_DISCOVERY); + // if no explicit discovery mode is set, then default to "ANY" + // irrespective of whether the HTTP/3 version may have been + // set on the HttpClient or the HttpRequest + return h3Discovery.orElse(Http3DiscoveryMode.ANY); + } + /** * Creates a HttpRequestImpl from the given set of Headers and the associated * "parent" request. Fields not taken from the headers are taken from the @@ -276,6 +312,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = parent.timeout; this.version = parent.version; this.authority = null; + this.options = parent.options; } @Override @@ -399,6 +436,11 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public void setSystemHeader(String name, String value) { systemHeadersBuilder.setHeader(name, value); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java index 1552cd40ede..ba5bea43078 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java @@ -41,14 +41,14 @@ import jdk.internal.net.http.websocket.RawChannel; /** * The implementation class for HttpResponse */ -class HttpResponseImpl implements HttpResponse, RawChannel.Provider { +final class HttpResponseImpl implements HttpResponse, RawChannel.Provider { final int responseCode; private final String connectionLabel; final HttpRequest initialRequest; - final Optional> previousResponse; + final HttpResponse previousResponse; // may be null; final HttpHeaders headers; - final Optional sslSession; + final SSLSession sslSession; // may be null final URI uri; final HttpClient.Version version; final RawChannelProvider rawChannelProvider; @@ -62,10 +62,10 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { this.responseCode = response.statusCode(); this.connectionLabel = connectionLabel(exch).orElse(null); this.initialRequest = initialRequest; - this.previousResponse = Optional.ofNullable(previousResponse); + this.previousResponse = previousResponse; this.headers = response.headers(); //this.trailers = trailers; - this.sslSession = Optional.ofNullable(response.getSSLSession()); + this.sslSession = response.getSSLSession(); this.uri = response.request().uri(); this.version = response.version(); this.rawChannelProvider = RawChannelProvider.create(response, exch); @@ -96,7 +96,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional> previousResponse() { - return previousResponse; + return Optional.ofNullable(previousResponse); } @Override @@ -111,7 +111,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional sslSession() { - return sslSession; + return Optional.ofNullable(sslSession); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java index 1fb96944afc..b2c15ac3bef 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -28,7 +28,9 @@ package jdk.internal.net.http; import java.net.URI; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient.Version; @@ -43,6 +45,8 @@ final class ImmutableHttpRequest extends HttpRequest { private final boolean expectContinue; private final Optional timeout; private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; /** Creates an ImmutableHttpRequest from the given builder. */ ImmutableHttpRequest(HttpRequestBuilderImpl builder) { @@ -53,6 +57,7 @@ final class ImmutableHttpRequest extends HttpRequest { this.expectContinue = builder.expectContinue(); this.timeout = Optional.ofNullable(builder.timeout()); this.version = Objects.requireNonNull(builder.version()); + this.options = Map.copyOf(builder.options()); } @Override @@ -78,8 +83,17 @@ final class ImmutableHttpRequest extends HttpRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public String toString() { return uri.toString() + " " + method; } + + public Map, Object> options() { + return options; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java index 20120aad7d5..ec621f7f955 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java @@ -29,7 +29,9 @@ import java.io.IOError; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.ConnectException; +import java.net.http.HttpClient.Version; import java.net.http.HttpConnectTimeoutException; +import java.net.http.StreamLimitException; import java.time.Duration; import java.util.List; import java.util.ListIterator; @@ -38,8 +40,6 @@ import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; @@ -62,6 +62,8 @@ import jdk.internal.net.http.common.ConnectionExpiredException; import jdk.internal.net.http.common.Utils; import static jdk.internal.net.http.common.MinimalFuture.completedFuture; import static jdk.internal.net.http.common.MinimalFuture.failedFuture; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcHeader; + /** * Encapsulates multiple Exchanges belonging to one HttpRequestImpl. @@ -76,6 +78,16 @@ class MultiExchange implements Cancelable { static final Logger debug = Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG); + private record RetryContext(Throwable requestFailureCause, + boolean shouldRetry, + AtomicInteger reqAttemptCounter, + boolean shouldResetConnectTimer) { + private static RetryContext doNotRetry(Throwable requestFailureCause) { + return new RetryContext(requestFailureCause, false, null, false); + } + } + + private static final AtomicLong IDS = new AtomicLong(); private final HttpRequest userRequest; // the user request private final HttpRequestImpl request; // a copy of the user request private final ConnectTimeoutTracker connectTimeout; // null if no timeout @@ -83,12 +95,11 @@ class MultiExchange implements Cancelable { final HttpResponse.BodyHandler responseHandler; final HttpClientImpl.DelegatingExecutor executor; final AtomicInteger attempts = new AtomicInteger(); + final long id = IDS.incrementAndGet(); HttpRequestImpl currentreq; // used for retries & redirect HttpRequestImpl previousreq; // used for retries & redirect Exchange exchange; // the current exchange Exchange previous; - volatile Throwable retryCause; - volatile boolean retriedOnce; volatile HttpResponse response; // Maximum number of times a request will be retried/redirected @@ -98,6 +109,12 @@ class MultiExchange implements Cancelable { "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS ); + // Maximum number of times a request should be retried when + // max streams limit is reached + static final int max_stream_limit_attempts = Utils.getIntegerNetProperty( + "jdk.httpclient.retryOnStreamlimit", max_attempts + ); + private final List filters; volatile ResponseTimerEvent responseTimerEvent; volatile boolean cancelled; @@ -113,20 +130,22 @@ class MultiExchange implements Cancelable { volatile AuthenticationFilter.AuthInfo serverauth, proxyauth; // RedirectHandler volatile int numberOfRedirects = 0; + // StreamLimit + private final AtomicInteger streamLimitRetries = new AtomicInteger(); // This class is used to keep track of the connection timeout // across retries, when a ConnectException causes a retry. // In that case - we will retry the connect, but we don't // want to double the timeout by starting a new timer with // the full connectTimeout again. - // Instead we use the ConnectTimeoutTracker to return a new + // Instead, we use the ConnectTimeoutTracker to return a new // duration that takes into account the time spent in the // first connect attempt. // If however, the connection gets connected, but we later // retry the whole operation, then we reset the timer before // retrying (since the connection used for the second request // will not necessarily be the same: it could be a new - // unconnected connection) - see getExceptionalCF(). + // unconnected connection) - see checkRetryEligible(). private static final class ConnectTimeoutTracker { final Duration max; final AtomicLong startTime = new AtomicLong(); @@ -199,8 +218,22 @@ class MultiExchange implements Cancelable { HttpClient.Version version() { HttpClient.Version vers = request.version().orElse(client.version()); - if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null) + if (vers != Version.HTTP_1_1 + && !request.secure() && request.proxy() != null + && !request.isHttp3Only(vers)) { + // downgrade to HTTP_1_1 unless HTTP_3_URI_ONLY. + // if HTTP_3_URI_ONLY and not secure it will fail down the road, so + // we don't downgrade here. vers = HttpClient.Version.HTTP_1_1; + } + if (vers == Version.HTTP_3 && request.secure() && !client.client3().isPresent()) { + if (!request.isHttp3Only(vers)) { + // HTTP/3 not supported with the client config. + // Downgrade to HTTP/2, unless HTTP_3_URI_ONLY is specified + vers = Version.HTTP_2; + if (debug.on()) debug.log("HTTP_3 downgraded to " + vers); + } + } return vers; } @@ -229,28 +262,28 @@ class MultiExchange implements Cancelable { } private void requestFilters(HttpRequestImpl r) throws IOException { - Log.logTrace("Applying request filters"); + if (Log.trace()) Log.logTrace("Applying request filters"); for (HeaderFilter filter : filters) { - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); filter.request(r, this); } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); } private HttpRequestImpl responseFilters(Response response) throws IOException { - Log.logTrace("Applying response filters"); + if (Log.trace()) Log.logTrace("Applying response filters"); ListIterator reverseItr = filters.listIterator(filters.size()); while (reverseItr.hasPrevious()) { HeaderFilter filter = reverseItr.previous(); - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); HttpRequestImpl newreq = filter.response(response); if (newreq != null) { - Log.logTrace("New request: stopping filters"); + if (Log.trace()) Log.logTrace("New request: stopping filters"); return newreq; } } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); return null; } @@ -293,9 +326,13 @@ class MultiExchange implements Cancelable { return true; } else { if (cancelled) { - debug.log("multi exchange already cancelled: " + interrupted.get()); + if (debug.on()) { + debug.log("multi exchange already cancelled: " + interrupted.get()); + } } else { - debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + if (debug.on()) { + debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + } } } return false; @@ -316,7 +353,7 @@ class MultiExchange implements Cancelable { // and therefore doesn't have to include header information which indicates no // body is present. This is distinct from responses that also do not contain // response bodies (possibly ever) but which are required to have content length - // info in the header (eg 205). Those cases do not have to be handled specially + // info in the header (e.g. 205). Those cases do not have to be handled specially private static boolean bodyNotPermitted(Response r) { return r.statusCode == 204; @@ -344,19 +381,27 @@ class MultiExchange implements Cancelable { if (exception != null) result.completeExceptionally(exception); else { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, nullBody, exch); - result.complete(this.response); + result.complete(setNewResponse(r.request(), r, nullBody, exch)); } }); // ensure that the connection is closed or returned to the pool. return result.whenComplete(exch::nullBody); } + // creates a new HttpResponseImpl object and assign it to this.response + private HttpResponse setNewResponse(HttpRequest request, Response r, T body, Exchange exch) { + HttpResponse previousResponse = this.response; + return this.response = new HttpResponseImpl<>(request, r, previousResponse, body, exch); + } + private CompletableFuture> responseAsync0(CompletableFuture start) { - return start.thenCompose( v -> responseAsyncImpl()) - .thenCompose((Response r) -> { + return start.thenCompose( _ -> { + // this is the first attempt to have the request processed by the server + attempts.set(1); + return responseAsyncImpl(true); + }).thenCompose((Response r) -> { + processAltSvcHeader(r, client(), currentreq); Exchange exch = getExchange(); if (bodyNotPermitted(r)) { if (bodyIsPresent(r)) { @@ -368,15 +413,11 @@ class MultiExchange implements Cancelable { return handleNoBody(r, exch); } return exch.readBodyAsync(responseHandler) - .thenApply((T body) -> { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, body, exch); - return this.response; - }); + .thenApply((T body) -> setNewResponse(r.request, r, body, exch)); }).exceptionallyCompose(this::whenCancelled); } - // returns a CancellationExcpetion that wraps the given cause + // returns a CancellationException that wraps the given cause // if cancel(boolean) was called, the given cause otherwise private Throwable wrapIfCancelled(Throwable cause) { CancellationException interrupt = interrupted.get(); @@ -412,79 +453,100 @@ class MultiExchange implements Cancelable { } } - private CompletableFuture responseAsyncImpl() { - CompletableFuture cf; - if (attempts.incrementAndGet() > max_attempts) { - cf = failedFuture(new IOException("Too many retries", retryCause)); - } else { - if (currentreq.timeout().isPresent()) { - responseTimerEvent = ResponseTimerEvent.of(this); - client.registerTimer(responseTimerEvent); - } - try { - // 1. apply request filters - // if currentreq == previousreq the filters have already - // been applied once. Applying them a second time might - // cause some headers values to be added twice: for - // instance, the same cookie might be added again. - if (currentreq != previousreq) { - requestFilters(currentreq); - } - } catch (IOException e) { - return failedFuture(e); - } - Exchange exch = getExchange(); - // 2. get response - cf = exch.responseAsync() - .thenCompose((Response response) -> { - HttpRequestImpl newrequest; - try { - // 3. apply response filters - newrequest = responseFilters(response); - } catch (Throwable t) { - IOException e = t instanceof IOException io ? io : new IOException(t); - exch.exchImpl.cancel(e); - return failedFuture(e); - } - // 4. check filter result and repeat or continue - if (newrequest == null) { - if (attempts.get() > 1) { - Log.logError("Succeeded on attempt: " + attempts); - } - return completedFuture(response); - } else { - cancelTimer(); - this.response = - new HttpResponseImpl<>(currentreq, response, this.response, null, exch); - Exchange oldExch = exch; - if (currentreq.isWebSocket()) { - // need to close the connection and open a new one. - exch.exchImpl.connection().close(); - } - return exch.ignoreBody().handle((r,t) -> { - previousreq = currentreq; - currentreq = newrequest; - retriedOnce = false; - setExchange(new Exchange<>(currentreq, this)); - return responseAsyncImpl(); - }).thenCompose(Function.identity()); - } }) - .handle((response, ex) -> { - // 5. handle errors and cancel any timer set - cancelTimer(); - if (ex == null) { - assert response != null; - return completedFuture(response); - } - // all exceptions thrown are handled here - CompletableFuture errorCF = getExceptionalCF(ex, exch.exchImpl); - if (errorCF == null) { - return responseAsyncImpl(); - } else { - return errorCF; - } }) - .thenCompose(Function.identity()); + // we call this only when a request is being retried + private CompletableFuture retryRequest() { + // maintain state indicating a request being retried + previousreq = currentreq; + // request is being retried, so the filters have already + // been applied once. Applying them a second time might + // cause some headers values to be added twice: for + // instance, the same cookie might be added again. + final boolean applyReqFilters = false; + return responseAsyncImpl(applyReqFilters); + } + + private CompletableFuture responseAsyncImpl(final boolean applyReqFilters) { + if (currentreq.timeout().isPresent()) { + responseTimerEvent = ResponseTimerEvent.of(this); + client.registerTimer(responseTimerEvent); } + try { + // 1. apply request filters + if (applyReqFilters) { + requestFilters(currentreq); + } + } catch (IOException e) { + return failedFuture(e); + } + final Exchange exch = getExchange(); + // 2. get response + final CompletableFuture cf = exch.responseAsync() + .thenCompose((Response response) -> { + HttpRequestImpl newrequest; + try { + // 3. apply response filters + newrequest = responseFilters(response); + } catch (Throwable t) { + IOException e = t instanceof IOException io ? io : new IOException(t); + exch.exchImpl.cancel(e); + return failedFuture(e); + } + // 4. check filter result and repeat or continue + if (newrequest == null) { + if (attempts.get() > 1) { + if (Log.requests()) { + Log.logResponse(() -> String.format( + "%s #%s Succeeded on attempt %s: statusCode=%s", + request, id, attempts, response.statusCode)); + } + } + return completedFuture(response); + } else { + cancelTimer(); + setNewResponse(currentreq, response, null, exch); + if (currentreq.isWebSocket()) { + // need to close the connection and open a new one. + exch.exchImpl.connection().close(); + } + return exch.ignoreBody().handle((r,t) -> { + previousreq = currentreq; + currentreq = newrequest; + // this is the first attempt to have the new request + // processed by the server + attempts.set(1); + setExchange(new Exchange<>(currentreq, this)); + return responseAsyncImpl(true); + }).thenCompose(Function.identity()); + } }) + .handle((response, ex) -> { + // 5. handle errors and cancel any timer set + cancelTimer(); + if (ex == null) { + assert response != null; + return completedFuture(response); + } + // all exceptions thrown are handled here + final RetryContext retryCtx = checkRetryEligible(ex, exch); + assert retryCtx != null : "retry context is null"; + if (retryCtx.shouldRetry()) { + // increment the request attempt counter and retry the request + assert retryCtx.reqAttemptCounter != null : "request attempt counter is null"; + final int numAttempt = retryCtx.reqAttemptCounter.incrementAndGet(); + if (debug.on()) { + debug.log("Retrying request: " + currentreq + " id: " + id + + " attempt: " + numAttempt + " due to: " + + retryCtx.requestFailureCause); + } + // reset the connect timer if necessary + if (retryCtx.shouldResetConnectTimer && this.connectTimeout != null) { + this.connectTimeout.reset(); + } + return retryRequest(); + } else { + assert retryCtx.requestFailureCause != null : "missing request failure cause"; + return MinimalFuture.failedFuture(retryCtx.requestFailureCause); + } }) + .thenCompose(Function.identity()); return cf; } @@ -492,14 +554,14 @@ class MultiExchange implements Cancelable { String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } private static boolean disableRetryConnect() { String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } /** True if ALL ( even non-idempotent ) requests can be automatic retried. */ @@ -517,7 +579,7 @@ class MultiExchange implements Cancelable { } /** Returns true if the given request can be automatically retried. */ - private static boolean canRetryRequest(HttpRequest request) { + private static boolean isHttpMethodRetriable(HttpRequest request) { if (RETRY_ALWAYS) return true; if (isIdempotentRequest(request)) @@ -534,70 +596,125 @@ class MultiExchange implements Cancelable { return interrupted.get() != null; } - private boolean retryOnFailure(Throwable t) { - if (requestCancelled()) return false; - return t instanceof ConnectionExpiredException - || (RETRY_CONNECT && (t instanceof ConnectException)); - } - - private Throwable retryCause(Throwable t) { - Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t; - return cause == null ? t : cause; + String streamLimitState() { + return id + " attempt:" + streamLimitRetries.get(); } /** - * Takes a Throwable and returns a suitable CompletableFuture that is - * completed exceptionally, or null. + * This method determines if a failed request can be retried. The returned RetryContext + * will contain the {@linkplain RetryContext#shouldRetry() retry decision} and the + * {@linkplain RetryContext#requestFailureCause() underlying + * cause} (computed out of the given {@code requestFailureCause}) of the request failure. + * + * @param requestFailureCause the exception that caused the request to fail + * @param exchg the Exchange + * @return a non-null RetryContext which contains the result of retry eligibility */ - private CompletableFuture getExceptionalCF(Throwable t, ExchangeImpl exchImpl) { - if ((t instanceof CompletionException) || (t instanceof ExecutionException)) { - if (t.getCause() != null) { - t = t.getCause(); + private RetryContext checkRetryEligible(final Throwable requestFailureCause, + final Exchange exchg) { + assert requestFailureCause != null : "request failure cause is missing"; + assert exchg != null : "exchange cannot be null"; + // determine the underlying cause for the request failure + final Throwable t = Utils.getCompletionCause(requestFailureCause); + final Throwable underlyingCause = switch (t) { + case IOException ioe -> { + if (cancelled && !requestCancelled() && !(ioe instanceof HttpTimeoutException)) { + yield toTimeoutException(ioe); + } + yield ioe; } + default -> { + yield t; + } + }; + if (requestCancelled()) { + // request has been cancelled, do not retry + return RetryContext.doNotRetry(underlyingCause); } - final boolean retryAsUnprocessed = exchImpl != null && exchImpl.isUnprocessedByPeer(); - if (cancelled && !requestCancelled() && t instanceof IOException) { - if (!(t instanceof HttpTimeoutException)) { - t = toTimeoutException((IOException)t); + // check if retry limited is reached. if yes then don't retry. + record Limit(int numAttempts, int maxLimit) { + boolean retryLimitReached() { + return Limit.this.numAttempts >= Limit.this.maxLimit; } - } else if (retryAsUnprocessed || retryOnFailure(t)) { - Throwable cause = retryCause(t); - - if (!(t instanceof ConnectException)) { - // we may need to start a new connection, and if so - // we want to start with a fresh connect timeout again. - if (connectTimeout != null) connectTimeout.reset(); - if (!retryAsUnprocessed && !canRetryRequest(currentreq)) { - // a (peer) processed request which cannot be retried, fail with - // the original cause - return failedFuture(cause); - } - } // ConnectException: retry, but don't reset the connectTimeout. - - // allow the retry mechanism to do its work - retryCause = cause; - if (!retriedOnce) { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): retrying " + currentreq + " due to: ", t); - } - retriedOnce = true; - // The connection was abruptly closed. - // We return null to retry the same request a second time. - // The request filters have already been applied to the - // currentreq, so we set previousreq = currentreq to - // prevent them from being applied again. - previousreq = currentreq; - return null; - } else { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): already retried once " + currentreq, t); - } - t = cause; + }; + final Limit limit = switch (underlyingCause) { + case StreamLimitException _ -> { + yield new Limit(streamLimitRetries.get(), max_stream_limit_attempts); } + case ConnectException _ -> { + // for ConnectException (i.e. inability to establish a connection to the server) + // we currently retry the request only once and don't honour the + // "jdk.httpclient.redirects.retrylimit" configuration value. + yield new Limit(attempts.get(), 2); + } + default -> { + yield new Limit(attempts.get(), max_attempts); + } + }; + if (limit.retryLimitReached()) { + if (debug.on()) { + debug.log("request already attempted " + + limit.numAttempts + " times, won't be retried again " + + currentreq + " " + id, underlyingCause); + } + final var x = underlyingCause instanceof ConnectionExpiredException cee + ? cee.getCause() == null ? cee : cee.getCause() + : underlyingCause; + // do not retry anymore + return RetryContext.doNotRetry(x); } - return failedFuture(t); + return switch (underlyingCause) { + case ConnectException _ -> { + // connection attempt itself failed, so the request hasn't reached the server. + // check if retry on connection failure is enabled, if not then we don't retry + // the request. + if (!RETRY_CONNECT) { + // do not retry + yield RetryContext.doNotRetry(underlyingCause); + } + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, attempts, false); + } + case StreamLimitException sle -> { + // make a note that the stream limit was reached for a particular HTTP version + exchg.streamLimitReached(true); + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, streamLimitRetries, false); + } + case ConnectionExpiredException cee -> { + final Throwable cause = cee.getCause() == null ? cee : cee.getCause(); + // check if the request was explicitly marked as unprocessed, in which case + // we retry + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + // the request which failed hasn't been marked as unprocessed which implies that + // it could be processed by the server. check if the request's METHOD allows + // for retry. + if (!isHttpMethodRetriable(currentreq)) { + // request METHOD doesn't allow for retry + yield RetryContext.doNotRetry(cause); + } + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + default -> { + // some other exception that caused the request to fail. + // we check if the request has been explicitly marked as "unprocessed", + // which implies the server hasn't processed the request and is thus OK to retry. + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for resetting the connect timer + yield new RetryContext(underlyingCause, true, attempts, false); + } + // some other cause of failure, do not retry. + yield RetryContext.doNotRetry(underlyingCause); + } + }; } private HttpTimeoutException toTimeoutException(IOException ioe) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java index adbee565297..8aee8ef2230 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java @@ -26,6 +26,7 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.URISyntaxException; import java.util.Locale; import java.util.Objects; @@ -132,6 +133,46 @@ public record Origin(String scheme, String host, int port) { return host + ":" + port; } + /** + * {@return true if the Origin's scheme is considered secure, else returns false} + */ + boolean isSecure() { + // we consider https to be the only secure scheme + return scheme.equals("https"); + } + + /** + * {@return Creates and returns an Origin parsed from the ASCII serialized form as defined + * in section 6.2 of RFC-6454} + * + * @param value The value to be parsed + */ + static Origin fromASCIISerializedForm(final String value) throws IllegalArgumentException { + Objects.requireNonNull(value); + try { + final URI uri = new URI(value); + // the ASCII-serialized form contains scheme://host, optionally followed by :port + if (uri.getScheme() == null || uri.getHost() == null) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + // normalize the origin string, check if we get the same result + String normalized = uri.getScheme() + "://" + uri.getHost(); + if (uri.getPort() != -1) { + normalized += ":" + uri.getPort(); + } + if (!value.equals(normalized)) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + try { + return Origin.from(uri); + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", iae); + } + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", use); + } + } + private static boolean isValidScheme(final String scheme) { // only "http" and "https" literals allowed return "http".equals(scheme) || "https".equals(scheme); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java index d0d64312f1f..e705aae72a1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java @@ -266,24 +266,8 @@ class PlainHttpConnection extends HttpConnection { try { this.chan = SocketChannel.open(); chan.configureBlocking(false); - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Initial receive buffer size is: %d", bufsize); - bufsize = getSoSendBufferSize(); - debug.log("Initial send buffer size is: %d", bufsize); - } - if (trySetReceiveBufferSize(client.getReceiveBufferSize())) { - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Receive buffer size configured: %d", bufsize); - } - } - if (trySetSendBufferSize(client.getSendBufferSize())) { - if (debug.on()) { - int bufsize = getSoSendBufferSize(); - debug.log("Send buffer size configured: %d", bufsize); - } - } + Utils.configureChannelBuffers(debug::log, chan, + client.getReceiveBufferSize(), client.getSendBufferSize()); chan.setOption(StandardSocketOptions.TCP_NODELAY, true); // wrap the channel in a Tube for async reading and writing tube = new SocketTube(client(), chan, Utils::getBuffer, label); @@ -292,54 +276,6 @@ class PlainHttpConnection extends HttpConnection { } } - private int getSoReceiveBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_RCVBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private int getSoSendBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_SNDBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private boolean trySetReceiveBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set receive buffer size to %d on %s", - bufsize, chan); - } - return false; - } - - private boolean trySetSendBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set send buffer size to %d on %s", - bufsize, chan); - } - return false; - } - @Override HttpPublisher publisher() { return writePublisher; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java index 0556214648e..d1019c05629 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -25,46 +25,55 @@ package jdk.internal.net.http; -import java.util.Iterator; import java.util.concurrent.Flow; + import jdk.internal.net.http.common.Demand; import jdk.internal.net.http.common.SequentialScheduler; /** - * A Publisher that publishes items obtained from the given Iterable. Each new - * subscription gets a new Iterator. + * A {@linkplain Flow.Publisher publisher} that publishes items obtained from the given {@link CheckedIterable}. + * Each new subscription gets a new {@link CheckedIterator}. */ class PullPublisher implements Flow.Publisher { - // Only one of `iterable` and `throwable` can be non-null. throwable is + // Only one of `iterable` or `throwable` should be null, and the other non-null. throwable is // non-null when an error has been encountered, by the creator of // PullPublisher, while subscribing the subscriber, but before subscribe has // completed. - private final Iterable iterable; + private final CheckedIterable iterable; private final Throwable throwable; - PullPublisher(Iterable iterable, Throwable throwable) { + PullPublisher(CheckedIterable iterable, Throwable throwable) { + if ((iterable == null) == (throwable == null)) { + String message = String.format( + "only one of `iterable` or `throwable` should be null, and the other non-null, but %s are null", + throwable == null ? "both" : "none"); + throw new IllegalArgumentException(message); + } this.iterable = iterable; this.throwable = throwable; } - PullPublisher(Iterable iterable) { + PullPublisher(CheckedIterable iterable) { this(iterable, null); } @Override public void subscribe(Flow.Subscriber subscriber) { - Subscription sub; - if (throwable != null) { - assert iterable == null : "non-null iterable: " + iterable; - sub = new Subscription(subscriber, null, throwable); - } else { - assert throwable == null : "non-null exception: " + throwable; - sub = new Subscription(subscriber, iterable.iterator(), null); + Throwable failure = throwable; + CheckedIterator iterator = null; + if (failure == null) { + try { + iterator = iterable.iterator(); + } catch (Exception exception) { + failure = exception; + } } + Subscription sub = failure != null + ? new Subscription(subscriber, null, failure) + : new Subscription(subscriber, iterator, null); subscriber.onSubscribe(sub); - - if (throwable != null) { + if (failure != null) { sub.pullScheduler.runOrSchedule(); } } @@ -72,7 +81,7 @@ class PullPublisher implements Flow.Publisher { private class Subscription implements Flow.Subscription { private final Flow.Subscriber subscriber; - private final Iterator iter; + private final CheckedIterator iter; private volatile boolean completed; private volatile boolean cancelled; private volatile Throwable error; @@ -80,7 +89,7 @@ class PullPublisher implements Flow.Publisher { private final Demand demand = new Demand(); Subscription(Flow.Subscriber subscriber, - Iterator iter, + CheckedIterator iter, Throwable throwable) { this.subscriber = subscriber; this.iter = iter; @@ -117,7 +126,18 @@ class PullPublisher implements Flow.Publisher { } subscriber.onNext(next); } - if (!iter.hasNext() && !cancelled) { + + boolean hasNext; + try { + hasNext = iter.hasNext(); + } catch (Exception e) { + completed = true; + pullScheduler.stop(); + subscriber.onError(e); + return; + } + + if (!hasNext && !cancelled) { completed = true; pullScheduler.stop(); subscriber.onComplete(); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java index f2c7a61c9b6..6bf03f195a7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java @@ -25,6 +25,7 @@ package jdk.internal.net.http; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.net.http.HttpRequest; @@ -105,9 +106,21 @@ class PushGroup { } Acceptor acceptPushRequest(HttpRequest pushRequest) { + return doAcceptPushRequest(pushRequest, null); + } + + Acceptor acceptPushRequest(HttpRequest pushRequest, PushId pushId) { + return doAcceptPushRequest(pushRequest, Objects.requireNonNull(pushId)); + } + + private Acceptor doAcceptPushRequest(HttpRequest pushRequest, PushId pushId) { AcceptorImpl acceptor = new AcceptorImpl<>(executor); try { - pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + if (pushId == null) { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + } else { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, pushId, acceptor::accept); + } } catch (Throwable t) { if (acceptor.accepted()) { CompletableFuture cf = acceptor.cf(); @@ -128,6 +141,10 @@ class PushGroup { } } + void acceptPushPromiseId(PushId pushId) { + pushPromiseHandler.notifyAdditionalPromise(initiatingRequest, pushId); + } + void pushCompleted() { stateLock.lock(); try { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java index 88cabe15419..5aea5648e19 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java @@ -73,8 +73,11 @@ public final class RequestPublishers { this(content, offset, length, Utils.BUFSIZE); } - /* bufSize exposed for testing purposes */ - ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + private ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + Objects.checkFromIndexSize(offset, length, content.length); // Implicit null check on `content` + if (bufSize <= 0) { + throw new IllegalArgumentException("Invalid buffer size: " + bufSize); + } this.content = content; this.offset = offset; this.length = length; @@ -99,7 +102,7 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { List copy = copy(content, offset, length); - var delegate = new PullPublisher<>(copy); + var delegate = new PullPublisher<>(CheckedIterable.fromIterable(copy)); delegate.subscribe(subscriber); } @@ -121,7 +124,7 @@ public final class RequestPublishers { // The ByteBufferIterator will iterate over the byte[] arrays in // the content one at the time. // - class ByteBufferIterator implements Iterator { + private final class ByteBufferIterator implements CheckedIterator { final ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); final Iterator iterator = content.iterator(); @Override @@ -166,13 +169,9 @@ public final class RequestPublishers { } } - public Iterator iterator() { - return new ByteBufferIterator(); - } - @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = this::iterator; + CheckedIterable iterable = () -> new ByteBufferIterator(); var delegate = new PullPublisher<>(iterable); delegate.subscribe(subscriber); } @@ -202,13 +201,13 @@ public final class RequestPublishers { public static class StringPublisher extends ByteArrayPublisher { public StringPublisher(String content, Charset charset) { - super(content.getBytes(charset)); + super(content.getBytes(Objects.requireNonNull(charset))); // Implicit null check on `content` } } public static class EmptyPublisher implements BodyPublisher { private final Flow.Publisher delegate = - new PullPublisher(Collections.emptyList(), null); + new PullPublisher<>(CheckedIterable.fromIterable(Collections.emptyList()), null); @Override public long contentLength() { @@ -290,7 +289,7 @@ public final class RequestPublishers { /** * Reads one buffer ahead all the time, blocking in hasNext() */ - public static class StreamIterator implements Iterator { + private static final class StreamIterator implements CheckedIterator { final InputStream is; final Supplier bufSupplier; private volatile boolean eof; @@ -331,20 +330,8 @@ public final class RequestPublishers { return n; } - /** - * Close stream in this instance. - * UncheckedIOException may be thrown if IOE happens at InputStream::close. - */ - private void closeStream() { - try { - is.close(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - @Override - public boolean hasNext() { + public boolean hasNext() throws IOException { stateLock.lock(); try { return hasNext0(); @@ -353,7 +340,7 @@ public final class RequestPublishers { } } - private boolean hasNext0() { + private boolean hasNext0() throws IOException { if (need2Read) { try { haveNext = read() != -1; @@ -363,10 +350,10 @@ public final class RequestPublishers { } catch (IOException e) { haveNext = false; need2Read = false; - throw new UncheckedIOException(e); + throw e; } finally { if (!haveNext) { - closeStream(); + is.close(); } } } @@ -374,7 +361,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { stateLock.lock(); try { if (!hasNext()) { @@ -398,18 +385,23 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - PullPublisher publisher; - InputStream is = streamSupplier.get(); - if (is == null) { - Throwable t = new IOException("streamSupplier returned null"); - publisher = new PullPublisher<>(null, t); - } else { - publisher = new PullPublisher<>(iterableOf(is), null); + InputStream is = null; + Exception exception = null; + try { + is = streamSupplier.get(); + if (is == null) { + exception = new IOException("Stream supplier returned null"); + } + } catch (Exception cause) { + exception = new IOException("Stream supplier has failed", cause); } + PullPublisher publisher = exception != null + ? new PullPublisher<>(null, exception) + : new PullPublisher<>(iterableOf(is), null); publisher.subscribe(subscriber); } - protected Iterable iterableOf(InputStream is) { + private CheckedIterable iterableOf(InputStream is) { return () -> new StreamIterator(is); } @@ -442,13 +434,13 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = () -> new FileChannelIterator(channel, position, limit); + CheckedIterable iterable = () -> new FileChannelIterator(channel, position, limit); new PullPublisher<>(iterable).subscribe(subscriber); } } - private static final class FileChannelIterator implements Iterator { + private static final class FileChannelIterator implements CheckedIterator { private final FileChannel channel; @@ -470,7 +462,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } @@ -487,7 +479,7 @@ public final class RequestPublishers { } } catch (IOException ioe) { terminated = true; - throw new UncheckedIOException(ioe); + throw ioe; } return buffer.flip(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java index 949024453ca..e416773ee61 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2024, 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 @@ -25,13 +25,14 @@ package jdk.internal.net.http; -import java.net.URI; import java.io.IOException; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.InetSocketAddress; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; + import jdk.internal.net.http.common.Utils; /** @@ -71,17 +72,14 @@ class Response { this.statusCode = statusCode; this.isConnectResponse = isConnectResponse; if (connection != null) { - InetSocketAddress a; - try { - a = (InetSocketAddress)connection.channel().getLocalAddress(); - } catch (IOException e) { - a = null; - } - this.localAddress = a; - if (connection instanceof AbstractAsyncSSLConnection) { - AbstractAsyncSSLConnection cc = (AbstractAsyncSSLConnection)connection; + this.localAddress = revealedLocalSocketAddress(connection); + if (connection instanceof AbstractAsyncSSLConnection cc) { SSLEngine engine = cc.getEngine(); sslSession = Utils.immutableSession(engine.getSession()); + } else if (connection instanceof HttpQuicConnection qc) { + // TODO: consider adding Optional getSession() to HttpConnection? + var session = qc.quicConnection().getTLSEngine().getSession(); + sslSession = Utils.immutableSession(session); } else { sslSession = null; } @@ -128,4 +126,12 @@ class Response { sb.append(" Local port: ").append(localAddress.getPort()); return sb.toString(); } + + private static InetSocketAddress revealedLocalSocketAddress(HttpConnection connection) { + try { + return (InetSocketAddress) connection.channel().getLocalAddress(); + } catch (IOException io) { + return null; + } + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java index 04d019e4c81..071c68720ac 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java @@ -526,7 +526,7 @@ public class ResponseSubscribers { @Override public void onError(Throwable thrwbl) { if (debug.on()) - debug.log("onError called: " + thrwbl); + debug.log("onError called", thrwbl); subscription = null; failed = Objects.requireNonNull(thrwbl); // The client process that reads the input stream might @@ -1086,6 +1086,16 @@ public class ResponseSubscribers { bs.getBody().whenComplete((r, t) -> { if (t != null) { cf.completeExceptionally(t); + // if a user-provided BodySubscriber returns + // a getBody() CF completed exceptionally, it's + // the responsibility of that BodySubscriber to cancel + // its subscription in order to cancel the request, + // if operations are still in progress. + // Calling the errorHandler here would ensure that the + // request gets cancelled, but there me cases where that is + // not what the caller wants. Therefore, it's better to + // not call `errorHandler.accept(t);` here, but leave it + // to the provided BodySubscriber implementation. } else { cf.complete(r); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java index b5dada882b2..4b59a777de2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java @@ -59,6 +59,8 @@ import jdk.internal.net.http.common.*; import jdk.internal.net.http.frame.*; import jdk.internal.net.http.hpack.DecodingCallback; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; + import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; /** @@ -96,8 +98,8 @@ import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; * placed on the stream's inputQ which is consumed by the stream's * reader thread. * - * PushedStream sub class - * ====================== + * PushedStream subclass + * ===================== * Sending side methods are not used because the request comes from a PUSH_PROMISE * frame sent by the server. When a PUSH_PROMISE is received the PushedStream * is created. PushedStream does not use responseCF list as there can be only @@ -151,7 +153,7 @@ class Stream extends ExchangeImpl { // Indicates the first reason that was invoked when sending a ResetFrame // to the server. A streamState of 0 indicates that no reset was sent. // (see markStream(int code) - private volatile int streamState; // assigned using STREAM_STATE varhandle. + private volatile int streamState; // assigned while holding the sendLock. private volatile boolean deRegistered; // assigned using DEREGISTERED varhandle. // state flags @@ -219,7 +221,7 @@ class Stream extends ExchangeImpl { List buffers = df.getData(); List dsts = Collections.unmodifiableList(buffers); - int size = Utils.remaining(dsts, Integer.MAX_VALUE); + long size = Utils.remaining(dsts, Long.MAX_VALUE); if (size == 0 && finished) { inputQ.remove(); // consumed will not be called @@ -478,7 +480,9 @@ class Stream extends ExchangeImpl { if (code == 0) return streamState; sendLock.lock(); try { - return (int) STREAM_STATE.compareAndExchange(this, 0, code); + var state = streamState; + if (state == 0) streamState = code; + return state; } finally { sendLock.unlock(); } @@ -534,7 +538,7 @@ class Stream extends ExchangeImpl { this.requestPublisher = request.requestPublisher; // may be null this.responseHeadersBuilder = new HttpHeadersBuilder(); this.rspHeadersConsumer = new HeadersConsumer(); - this.requestPseudoHeaders = createPseudoHeaders(request); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); this.streamWindowUpdater = new StreamWindowUpdateSender(connection); } @@ -587,6 +591,7 @@ class Stream extends ExchangeImpl { case WindowUpdateFrame.TYPE -> incoming_windowUpdate((WindowUpdateFrame) frame); case ResetFrame.TYPE -> incoming_reset((ResetFrame) frame); case PriorityFrame.TYPE -> incoming_priority((PriorityFrame) frame); + case AltSvcFrame.TYPE -> handleAltSvcFrame(streamid, (AltSvcFrame) frame); default -> throw new IOException("Unexpected frame: " + frame); } @@ -745,6 +750,10 @@ class Stream extends ExchangeImpl { } } + void handleAltSvcFrame(int streamid, AltSvcFrame asf) { + processAltSvcFrame(streamid, asf, connection.connection, connection.client()); + } + void handleReset(ResetFrame frame, Flow.Subscriber subscriber) { Log.logTrace("Handling RST_STREAM on stream {0}", streamid); if (!closed) { @@ -763,12 +772,16 @@ class Stream extends ExchangeImpl { // A REFUSED_STREAM error code implies that the stream wasn't processed by the // peer and the client is free to retry the request afresh. if (error == ErrorFrame.REFUSED_STREAM) { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be marked as unprocessed"; // Here we arrange for the request to be retried. Note that we don't call // closeAsUnprocessed() method here because the "closed" state is already set // to true a few lines above and calling close() from within // closeAsUnprocessed() will end up being a no-op. We instead do the additional // bookkeeping here. - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("request unprocessed by peer (REFUSED_STREAM) " + this.request); @@ -1216,6 +1229,7 @@ class Stream extends ExchangeImpl { assert !endStreamSent : "internal error, send data after END_STREAM flag"; } if ((state = streamState) != 0) { + t = errorRef.get(); if (debug.on()) debug.log("trySend: cancelled: %s", String.valueOf(t)); break; } @@ -1521,7 +1535,7 @@ class Stream extends ExchangeImpl { } else cancelImpl(cause); } - // This method sends a RST_STREAM frame + // This method sends an RST_STREAM frame void cancelImpl(Throwable e) { cancelImpl(e, ResetFrame.CANCEL); } @@ -1856,8 +1870,12 @@ class Stream extends ExchangeImpl { */ void closeAsUnprocessed() { try { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; // We arrange for the request to be retried on a new connection as allowed by the RFC-9113 - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("closing " + this.request + " as unprocessed by peer"); @@ -1905,7 +1923,7 @@ class Stream extends ExchangeImpl { streamid, n, v); } } catch (UncheckedIOException uio) { - // reset stream: From RFC 9113, section 8.1 + // reset stream: From RFC 7540, section-8.1.2.6 // Malformed requests or responses that are detected MUST be // treated as a stream error (Section 5.4.2) of type // PROTOCOL_ERROR. @@ -1953,13 +1971,10 @@ class Stream extends ExchangeImpl { } - private static final VarHandle STREAM_STATE; private static final VarHandle DEREGISTERED; static { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); - STREAM_STATE = lookup - .findVarHandle(Stream.class, "streamState", int.class); DEREGISTERED = lookup .findVarHandle(Stream.class, "deRegistered", boolean.class); } catch (Exception x) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java index 66397d93410..5888e6c2de3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java @@ -34,4 +34,9 @@ public final class Alpns { public static final String HTTP_1_1 = "http/1.1"; public static final String H2 = "h2"; public static final String H2C = "h2c"; + public static final String H3 = "h3"; + + public static boolean isSecureALPNName(final String alpnName) { + return H3.equals(alpnName) || H2.equals(alpnName); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java index 07091886533..164e1e5685d 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java @@ -29,7 +29,9 @@ import java.io.IOException; /** * Signals that an end of file or end of stream has been reached - * unexpectedly before any protocol specific data has been received. + * unexpectedly before any protocol specific data has been received, + * or that a new stream creation was rejected because the underlying + * connection was closed. */ public final class ConnectionExpiredException extends IOException { private static final long serialVersionUID = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java index bc9a992b3bd..3ee334885a3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java @@ -88,6 +88,24 @@ public final class Deadline implements Comparable { return of(deadline.truncatedTo(unit)); } + /** + * Returns a copy of this deadline with the specified amount subtracted. + *

      + * This returns a {@code Deadline}, based on this one, with the specified amount subtracted. + * The amount is typically {@link Duration} but may be any other type implementing + * the {@link TemporalAmount} interface. + *

      + * This instance is immutable and unaffected by this method call. + * + * @param amountToSubtract the amount to subtract, not null + * @return a {@code Deadline} based on this deadline with the subtraction made, not null + * @throws DateTimeException if the subtraction cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline minus(TemporalAmount amountToSubtract) { + return Deadline.of(deadline.minus(amountToSubtract)); + } + /** * Returns a copy of this deadline with the specified amount added. *

      @@ -126,6 +144,21 @@ public final class Deadline implements Comparable { return Deadline.of(deadline.plusSeconds(secondsToAdd)); } + /** + * Returns a copy of this deadline with the specified duration in milliseconds added. + *

      + * This instance is immutable and unaffected by this method call. + * + * @param millisToAdd the milliseconds to add, positive or negative + * @return a {@code Deadline} based on this deadline with the specified milliseconds added, not null + * @throws DateTimeException if the result exceeds the maximum or minimum deadline + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline plusMillis(long millisToAdd) { + if (millisToAdd == 0) return this; + return Deadline.of(deadline.plusMillis(millisToAdd)); + } + /** * Returns a copy of this deadline with the specified amount added. *

      @@ -183,7 +216,7 @@ public final class Deadline implements Comparable { /** * Checks if this deadline is before the specified deadline. *

      - * The comparison is based on the time-line position of the deadines. + * The comparison is based on the time-line position of the deadlines. * * @param otherDeadline the other deadine to compare to, not null * @return true if this deadline is before the specified deadine @@ -217,7 +250,26 @@ public final class Deadline implements Comparable { return deadline.hashCode(); } + Instant asInstant() { + return deadline; + } + static Deadline of(Instant instant) { return new Deadline(instant); } + + /** + * Obtains a {@code Duration} representing the duration between two deadlines. + *

      + * The result of this method can be a negative period if the end is before the start. + * + * @param startInclusive the start deadline, inclusive, not null + * @param endExclusive the end deadline, exclusive, not null + * @return a {@code Duration}, not null + * @throws DateTimeException if the seconds between the deadline cannot be obtained + * @throws ArithmeticException if the calculation exceeds the capacity of {@code Duration} + */ + public static Duration between(Deadline startInclusive, Deadline endExclusive) { + return Duration.between(startInclusive.deadline, endExclusive.deadline); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java index 6dc79760b0a..1c483ce99f4 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java @@ -284,6 +284,7 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { */ public final void complete(Throwable t) { if (markCompleted()) { + logComplete(t); tryUnregister(); t = withError = Utils.getCompletionCause(t); if (t == null) { @@ -312,6 +313,10 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { } } + protected void logComplete(Throwable error) { + + } + /** * {@return true if this subscriber has already completed, either normally * or abnormally} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java index 409a8540b68..7c1d2311ba9 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -41,6 +41,15 @@ public class HttpHeadersBuilder { headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } + // used in test library (Http3ServerExchange) + public HttpHeadersBuilder(HttpHeaders headers) { + headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry> entry : headers.map().entrySet()) { + List valuesCopy = new ArrayList<>(entry.getValue()); + headersMap.put(entry.getKey(), valuesCopy); + } + } + public HttpHeadersBuilder structuralCopy() { HttpHeadersBuilder builder = new HttpHeadersBuilder(); for (Map.Entry> entry : headersMap.entrySet()) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java index 48f5a2b06d8..bc89a6e9d8e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023, 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 @@ -28,14 +28,28 @@ package jdk.internal.net.http.common; import java.net.http.HttpHeaders; import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Stream; + import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.internal.net.http.frame.WindowUpdateFrame; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLParameters; @@ -43,7 +57,8 @@ import javax.net.ssl.SSLParameters; /** * -Djdk.httpclient.HttpClient.log= * errors,requests,headers, - * frames[:control:data:window:all..],content,ssl,trace,channel + * frames[:control:data:window:all..],content,ssl,trace,channel, + * quic[:control:processed:retransmit:ack:crypto:data:cc:hs:dbb:ping:all] * * Any of errors, requests, headers or content are optional. * @@ -57,15 +72,17 @@ public abstract class Log implements System.Logger { static final String logProp = "jdk.httpclient.HttpClient.log"; - public static final int OFF = 0; - public static final int ERRORS = 0x1; - public static final int REQUESTS = 0x2; - public static final int HEADERS = 0x4; - public static final int CONTENT = 0x8; - public static final int FRAMES = 0x10; - public static final int SSL = 0x20; - public static final int TRACE = 0x40; - public static final int CHANNEL = 0x80; + public static final int OFF = 0x00; + public static final int ERRORS = 0x01; + public static final int REQUESTS = 0x02; + public static final int HEADERS = 0x04; + public static final int CONTENT = 0x08; + public static final int FRAMES = 0x10; + public static final int SSL = 0x20; + public static final int TRACE = 0x40; + public static final int CHANNEL = 0x80; + public static final int QUIC = 0x0100; + public static final int HTTP3 = 0x0200; static int logging; // Frame types: "control", "data", "window", "all" @@ -75,6 +92,27 @@ public abstract class Log implements System.Logger { public static final int ALL = CONTROL| DATA | WINDOW_UPDATES; static int frametypes; + // Quic message types + public static final int QUIC_CONTROL = 1; + public static final int QUIC_PROCESSED = 2; + public static final int QUIC_RETRANSMIT = 4; + public static final int QUIC_DATA = 8; + public static final int QUIC_CRYPTO = 16; + public static final int QUIC_ACK = 32; + public static final int QUIC_PING = 64; + public static final int QUIC_CC = 128; + public static final int QUIC_TIMER = 256; + public static final int QUIC_DIRECT_BUFFER_POOL = 512; + public static final int QUIC_HANDSHAKE = 1024; + public static final int QUIC_ALL = QUIC_CONTROL + | QUIC_PROCESSED | QUIC_RETRANSMIT + | QUIC_DATA | QUIC_CRYPTO + | QUIC_ACK | QUIC_PING | QUIC_CC + | QUIC_TIMER | QUIC_DIRECT_BUFFER_POOL + | QUIC_HANDSHAKE; + static int quictypes; + + static final System.Logger logger; static { @@ -94,6 +132,12 @@ public abstract class Log implements System.Logger { case "headers": logging |= HEADERS; break; + case "quic": + logging |= QUIC; + break; + case "http3": + logging |= HTTP3; + break; case "content": logging |= CONTENT; break; @@ -107,13 +151,14 @@ public abstract class Log implements System.Logger { logging |= TRACE; break; case "all": - logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL; + logging |= CONTENT | HEADERS | REQUESTS | FRAMES | ERRORS | TRACE | SSL | CHANNEL | QUIC | HTTP3; frametypes |= ALL; + quictypes |= QUIC_ALL; break; default: // ignore bad values } - if (val.startsWith("frames")) { + if (val.startsWith("frames:") || val.equals("frames")) { logging |= FRAMES; String[] types = val.split(":"); if (types.length == 1) { @@ -139,6 +184,56 @@ public abstract class Log implements System.Logger { } } } + if (val.startsWith("quic:") || val.equals("quic")) { + logging |= QUIC; + String[] types = val.split(":"); + if (types.length == 1) { + quictypes = QUIC_ALL & ~QUIC_TIMER & ~QUIC_DIRECT_BUFFER_POOL; + } else { + for (String type : types) { + switch (type.toLowerCase(Locale.US)) { + case "control": + quictypes |= QUIC_CONTROL; + break; + case "data": + quictypes |= QUIC_DATA; + break; + case "processed": + quictypes |= QUIC_PROCESSED; + break; + case "retransmit": + quictypes |= QUIC_RETRANSMIT; + break; + case "crypto": + quictypes |= QUIC_CRYPTO; + break; + case "cc": + quictypes |= QUIC_CC; + break; + case "hs": + quictypes |= QUIC_HANDSHAKE; + break; + case "ack": + quictypes |= QUIC_ACK; + break; + case "ping": + quictypes |= QUIC_PING; + break; + case "timer": + quictypes |= QUIC_TIMER; + break; + case "dbb": + quictypes |= QUIC_DIRECT_BUFFER_POOL; + break; + case "all": + quictypes = QUIC_ALL; + break; + default: + // ignore bad values + } + } + } + } } } if (logging != OFF) { @@ -175,6 +270,119 @@ public abstract class Log implements System.Logger { return (logging & CHANNEL) != 0; } + public static boolean altsvc() { return headers(); } + + public static boolean quicRetransmit() { + return (logging & QUIC) != 0 && (quictypes & QUIC_RETRANSMIT) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicHandshake() { + return (logging & QUIC) != 0 && (quictypes & QUIC_HANDSHAKE) != 0; + } + + public static boolean quicProcessed() { + return (logging & QUIC) != 0 && (quictypes & QUIC_PROCESSED) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicData() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DATA) != 0; + } + + public static boolean quicCrypto() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CRYPTO) != 0; + } + + public static boolean quicCC() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CC) != 0; + } + + public static boolean quicControl() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CONTROL) != 0; + } + + public static boolean quicTimer() { + return (logging & QUIC) != 0 && (quictypes & QUIC_TIMER) != 0; + } + public static boolean quicDBB() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DIRECT_BUFFER_POOL) != 0; + } + + public static boolean quic() { + return (logging & QUIC) != 0; + } + + public static boolean http3() { + return (logging & HTTP3) != 0; + } + + public static void logHttp3(String s, Object... s1) { + if (http3()) { + logger.log(Level.INFO, "HTTP3: " + s, s1); + } + } + + private static boolean isLogging(QuicFrame frame) { + if (frame instanceof StreamFrame sf) + return (quictypes & QUIC_DATA) != 0 + || (quictypes & QUIC_CONTROL) != 0 && sf.isLast() + || (quictypes & QUIC_CONTROL) != 0 && sf.offset() == 0; + if (frame instanceof AckFrame) + return (quictypes & QUIC_ACK) != 0; + if (frame instanceof CryptoFrame) + return (quictypes & QUIC_CRYPTO) != 0 + || (quictypes & QUIC_HANDSHAKE) != 0; + if (frame instanceof PingFrame) + return (quictypes & QUIC_PING) != 0; + if (frame instanceof PaddingFrame) return false; + if (frame instanceof HandshakeDoneFrame && quicHandshake()) + return true; + return (quictypes & QUIC_CONTROL) != 0; + } + + private static final EnumSet HS_TYPES = EnumSet.complementOf( + EnumSet.of(PacketType.ONERTT)); + + private static boolean quicPacketLoggable(QuicPacket packet) { + return (logging & QUIC) != 0 + && (quictypes == QUIC_ALL + || quicHandshake() && HS_TYPES.contains(packet.packetType()) + || stream(packet.frames()).anyMatch(Log::isLogging)); + } + + public static boolean quicPacketOutLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + private static Stream stream(Collection list) { + return list == null ? Stream.empty() : list.stream(); + } + + public static boolean quicPacketInLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + public static void logQuic(String s, Object... s1) { + if (quic()) { + logger.log(Level.INFO, "QUIC: " + s, s1); + } + } + + public static void logQuicPacketOut(String connectionTag, QuicPacket packet) { + if (quicPacketOutLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} OUT: {1}", + connectionTag, packet.prettyPrint()); + } + } + + public static void logQuicPacketIn(String connectionTag, QuicPacket packet) { + if (quicPacketInLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} IN: {1}", + connectionTag, packet.prettyPrint()); + } + } + public static void logError(String s, Object... s1) { if (errors()) { logger.log(Level.INFO, "ERROR: " + s, s1); @@ -237,6 +445,12 @@ public abstract class Log implements System.Logger { } } + public static void logAltSvc(String s, Object... s1) { + if (altsvc()) { + logger.log(Level.INFO, "ALTSVC: " + s, s1); + } + } + public static boolean loggingFrame(Class clazz) { if (frametypes == ALL) { return true; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java index 3aec13b59ec..ef031eef999 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java @@ -55,6 +55,8 @@ public final class OperationTrackers { long getOutstandingHttpOperations(); // The number of active HTTP/2 streams long getOutstandingHttp2Streams(); + // The number of active HTTP/3 streams + long getOutstandingHttp3Streams(); // The number of active WebSockets long getOutstandingWebSocketOperations(); // number of TCP connections still opened diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java index 489fbe7ffd8..c74c67f7d58 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java @@ -25,7 +25,6 @@ package jdk.internal.net.http.common; import java.time.Instant; -import java.time.InstantSource; /** * A {@link TimeLine} based on {@link System#nanoTime()} for the @@ -52,7 +51,7 @@ public final class TimeSource implements TimeLine { // The use of Integer.MAX_VALUE is arbitrary. // Any value not too close to Long.MAX_VALUE // would do. - static final int TIME_WINDOW = Integer.MAX_VALUE; + static final long TIME_WINDOW = Integer.MAX_VALUE; final Instant first; final long firstNanos; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index bcedff8844e..a02506cff5c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -37,33 +37,50 @@ import java.io.PrintStream; import java.io.UncheckedIOException; import java.lang.System.Logger.Level; import java.net.ConnectException; +import java.net.Inet6Address; import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardSocketOptions; import java.net.Proxy; import java.net.URI; import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; import java.net.http.HttpTimeoutException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.CancelledKeyException; +import java.nio.channels.NetworkChannel; import java.nio.channels.SelectionKey; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.text.Normalizer; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HexFormat; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; import java.util.function.BiPredicate; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -152,16 +169,25 @@ public final class Utils { return prop.isEmpty() ? true : Boolean.parseBoolean(prop); } - /** - * Allocated buffer size. Must never be higher than 16K. But can be lower - * if smaller allocation units preferred. HTTP/2 mandates that all - * implementations support frame payloads of at least 16K. - */ - private static final int DEFAULT_BUFSIZE = 16 * 1024; + // A threshold to decide whether to slice or copy. + // see sliceOrCopy + public static final int SLICE_THRESHOLD = 32; + /** + * The capacity of ephemeral {@link ByteBuffer}s allocated to pass data to and from the client. + * It is ensured to have a value between 1 and 2^14 (16,384). + */ public static final int BUFSIZE = getIntegerNetProperty( - "jdk.httpclient.bufsize", DEFAULT_BUFSIZE - ); + "jdk.httpclient.bufsize", 1, + // We cap at 2^14 (16,384) for two main reasons: + // - The initial frame size is 2^14 (RFC 9113) + // - SSL record layer fragments data in chunks of 2^14 bytes or less (RFC 5246) + 1 << 14, + // We choose 2^14 (16,384) as the default, because: + // 1. It maximizes throughput within the limits described above + // 2. It is small enough to not create a GC bottleneck when it is partially filled + 1 << 14, + true); public static final BiPredicate ACCEPT_ALL = (x,y) -> true; @@ -169,7 +195,8 @@ public final class Utils { private static Set getDisallowedHeaders() { Set headers = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade")); + headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade", + "alt-used")); String v = getNetProperty("jdk.httpclient.allowRestrictedHeaders"); if (v != null) { @@ -215,6 +242,56 @@ public final class Utils { return true; }; + public static T addSuppressed(T x, Throwable suppressed) { + if (x != suppressed && suppressed != null) { + var sup = x.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(suppressed)) { + return x; + } + } + sup = suppressed.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(x)) { + return x; + } + } + x.addSuppressed(suppressed); + } + return x; + } + + /** + * {@return a string comparing the given deadline with now, typically + * something like "due since Nms" or "due in Nms"} + * + * @apiNote + * This method recognize deadlines set to Instant.MIN + * and Instant.MAX as special cases meaning "due" and + * "not scheduled". + * + * @param now now + * @param deadline the deadline + */ + public static String debugDeadline(Deadline now, Deadline deadline) { + boolean isDue = deadline.compareTo(now) <= 0; + try { + if (isDue) { + if (deadline.equals(Deadline.MIN)) { + return "due (Deadline.MIN)"; + } else { + return "due since " + deadline.until(now, ChronoUnit.MILLIS) + "ms"; + } + } else if (deadline.equals(Deadline.MAX)) { + return "not scheduled (Deadline.MAX)"; + } else { + return "due in " + now.until(deadline, ChronoUnit.MILLIS) + "ms"; + } + } catch (ArithmeticException x) { + return isDue ? "due since too long" : "due in the far future"; + } + } + public record ProxyHeaders(HttpHeaders userHeaders, HttpHeaders systemHeaders) {} public static final BiPredicate PROXY_TUNNEL_RESTRICTED() { @@ -346,6 +423,7 @@ public final class Utils { } public static String interestOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.interestOps()); } catch (CancelledKeyException x) { @@ -354,6 +432,7 @@ public final class Utils { } public static String readyOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.readyOps()); } catch (CancelledKeyException x) { @@ -438,6 +517,21 @@ public final class Utils { return cause; } + public static IOException toIOException(Throwable cause) { + if (cause == null) return null; + if (cause instanceof CompletionException ce) { + cause = ce.getCause(); + } else if (cause instanceof ExecutionException ee) { + cause = ee.getCause(); + } + if (cause instanceof IOException io) { + return io; + } else if (cause instanceof UncheckedIOException uio) { + return uio.getCause(); + } + return new IOException(cause.getMessage(), cause); + } + public static IOException getIOException(Throwable t) { if (t instanceof IOException) { return (IOException) t; @@ -575,6 +669,10 @@ public final class Utils { return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); } + public static long getLongProperty(String name, long defaultValue) { + return Long.parseLong(System.getProperty(name, String.valueOf(defaultValue))); + } + public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) { int value = Utils.getIntegerNetProperty(property, defaultValue); // use default value if misconfigured @@ -610,6 +708,8 @@ public final class Utils { p1.setSNIMatchers(p.getSNIMatchers()); p1.setServerNames(p.getServerNames()); p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); + p1.setSignatureSchemes(p.getSignatureSchemes()); + p1.setNamedGroups(p.getNamedGroups()); return p1; } @@ -753,6 +853,91 @@ public final class Utils { return remain; } + // + + /** + * Reads as much bytes as possible from the buffer list, and + * write them in the provided {@code data} byte array. + * Returns the number of bytes read and written to the byte array. + * This method advances the position in the byte buffers it reads + * @param bufs A list of byte buffer + * @param data A byte array to write into + * @param offset Where to start writing in the byte array + * @return the amount of bytes read and written to the byte array + */ + public static int read(List bufs, byte[] data, int offset) { + int pos = offset; + for (ByteBuffer buf : bufs) { + if (pos >= data.length) break; + int read = Math.min(buf.remaining(), data.length - pos); + if (read <= 0) continue; + buf.get(data, pos, read); + pos += read; + } + return pos - offset; + } + + /** + * Returns the next buffer that has remaining bytes, or null. + * @param iterator an iterator + * @return the next buffer that has remaining bytes, or null + */ + public static ByteBuffer next(Iterator iterator) { + ByteBuffer next = null; + while (iterator.hasNext() && !(next = iterator.next()).hasRemaining()); + return next == null || !next.hasRemaining() ? null : next; + } + + /** + * Compute the relative consolidated position in bytes at which the two + * input mismatch, or -1 if there is no mismatch. + * @apiNote This method behaves as {@link ByteBuffer#mismatch(ByteBuffer)}. + * @param these a first list of byte buffers + * @param those a second list of byte buffers + * @return the relative consolidated position in bytes at which the two + * input mismatch, or -1L if there is no mismatch. + */ + public static long mismatch(List these, List those) { + if (these.isEmpty()) return those.isEmpty() ? -1 : 0; + if (those.isEmpty()) return 0; + Iterator lefti = these.iterator(), righti = those.iterator(); + ByteBuffer left = next(lefti), right = next(righti); + long parsed = 0; + while (left != null || right != null) { + int m = left == null || right == null ? 0 : left.mismatch(right); + if (m == -1) { + parsed = parsed + left.remaining(); + assert right.remaining() == left.remaining(); + if ((left = next(lefti)) != null) { + if ((right = next(righti)) != null) { + continue; + } + return parsed; + } + return (right = next(righti)) != null ? parsed : -1; + } + if (m == 0) return parsed; + parsed = parsed + m; + if (m < left.remaining()) { + if (m < right.remaining()) { + return parsed; + } + if ((right = next(righti)) != null) { + left = left.slice(m, left.remaining() - m); + continue; + } + return parsed; + } + assert m < right.remaining(); + if ((left = next(lefti)) != null) { + right = right.slice(m, right.remaining() - m); + continue; + } + return parsed; + } + return -1L; + } + public static long synchronizedRemaining(List bufs) { if (bufs == null) return 0L; synchronized (bufs) { @@ -764,12 +949,13 @@ public final class Utils { if (bufs == null) return 0; long remain = 0; for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - if (remain > max) { + int size = buf.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(List bufs, int max) { @@ -781,12 +967,13 @@ public final class Utils { if (refs == null) return 0; long remain = 0; for (ByteBuffer b : refs) { - remain += b.remaining(); - if (remain > max) { + int size = b.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(ByteBuffer[] refs, int max) { @@ -832,6 +1019,50 @@ public final class Utils { return newb; } + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity is less than the {@linkplain #SLICE_THRESHOLD + * default slice threshold}, or if the capacity minus the length to slice + * is less than the {@linkplain #SLICE_THRESHOLD threshold}, returns a slice. + * Otherwise, copy so as not to retain a reference to a big buffer + * for a small slice. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len) { + return sliceOrCopy(src, start, len, SLICE_THRESHOLD); + } + + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity minus the length to slice is less than the threshold, + * returns a slice. + * Otherwise, copy so as not to retain a reference to a buffer + * that contains more bytes than needed. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @param threshold a threshold to decide whether to slice or copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len, int threshold) { + assert src.hasArray(); + int cap = src.array().length; + if (cap - len < threshold) { + return src.slice(start, len); + } else { + byte[] b = new byte[len]; + if (len > 0) { + src.get(start, b, 0, len); + } + return ByteBuffer.wrap(b); + } + } + /** * Get the Charset from the Content-encoding header. Defaults to * UTF_8 @@ -847,7 +1078,9 @@ public final class Utils { if (value == null) return StandardCharsets.UTF_8; return Charset.forName(value); } catch (Throwable x) { - Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + if (Log.trace()) { + Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + } return StandardCharsets.UTF_8; } } @@ -1076,6 +1309,40 @@ public final class Utils { } } + /** + * Creates HTTP/2 HTTP/3 pseudo headers for the given request. + * @param request the request + * @return pseudo headers for that request + */ + public static HttpHeaders createPseudoHeaders(HttpRequest request) { + HttpHeadersBuilder hdrs = new HttpHeadersBuilder(); + String method = request.method(); + hdrs.setHeader(":method", method); + URI uri = request.uri(); + hdrs.setHeader(":scheme", uri.getScheme()); + String host = uri.getHost(); + int port = uri.getPort(); + assert host != null; + if (port != -1) { + hdrs.setHeader(":authority", host + ":" + port); + } else { + hdrs.setHeader(":authority", host); + } + String query = uri.getRawQuery(); + String path = uri.getRawPath(); + if (path == null || path.isEmpty()) { + if (method.equalsIgnoreCase("OPTIONS")) { + path = "*"; + } else { + path = "/"; + } + } + if (query != null) { + path += "?" + query; + } + hdrs.setHeader(":path", Utils.encode(path)); + return hdrs.build(); + } // -- toAsciiString-like support to encode path and query URI segments // Encodes all characters >= \u0080 into escaped, normalized UTF-8 octets, @@ -1119,6 +1386,302 @@ public final class Utils { return sb.toString(); } + /** + * {@return the content of the buffer as an hexadecimal string} + * This method doesn't move the buffer position or limit. + * @param buffer a byte buffer + */ + public static String asHexString(ByteBuffer buffer) { + if (!buffer.hasRemaining()) return ""; + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(buffer.position(), bytes); + return HexFormat.of().formatHex(bytes); + } + + /** + * Converts a ByteBuffer containing bytes encoded using + * the given {@linkplain Charset charset} into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing bytes encoded using + * a charset + * @param charset the charset to use to decode the bytes + * into a string + * + * @return a string built from the bytes contained + * in the buffer decoded using the given charset + */ + public static String asString(ByteBuffer buffer, Charset charset) { + var decoded = charset.decode(buffer); + char[] chars = new char[decoded.length()]; + decoded.get(chars); + return new String(chars); + } + + /** + * Converts a ByteBuffer containing UTF-8 bytes into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing UTF-8 bytes + * + * @return a string built from the decoded UTF-8 bytes contained + * in the buffer + */ + public static String asString(ByteBuffer buffer) { + return asString(buffer, StandardCharsets.UTF_8); + } + + public static String millis(Instant now, Instant deadline) { + if (Instant.MAX.equals(deadline)) return "not scheduled"; + try { + long delay = now.until(deadline, ChronoUnit.MILLIS); + return delay + " ms"; + } catch (ArithmeticException a) { + return "too far away"; + } + } + + public static String millis(Deadline now, Deadline deadline) { + return millis(now.asInstant(), deadline.asInstant()); + } + + public static ExecutorService safeExecutor(ExecutorService delegate, + BiConsumer errorHandler) { + Executor overflow = new CompletableFuture().defaultExecutor(); + return new SafeExecutorService(delegate, overflow, errorHandler); + } + + public static sealed class SafeExecutor implements Executor + permits SafeExecutorService { + final E delegate; + final BiConsumer errorHandler; + final Executor overflow; + + public SafeExecutor(E delegate, Executor overflow, BiConsumer errorHandler) { + this.delegate = delegate; + this.overflow = overflow; + this.errorHandler = errorHandler; + } + + @Override + public void execute(Runnable command) { + ensureExecutedAsync(command); + } + + private void ensureExecutedAsync(Runnable command) { + try { + delegate.execute(command); + } catch (RejectedExecutionException t) { + errorHandler.accept(command, t); + overflow.execute(command); + } + } + + } + + public static final class SafeExecutorService extends SafeExecutor + implements ExecutorService { + + public SafeExecutorService(ExecutorService delegate, + Executor overflow, + BiConsumer errorHandler) { + super(delegate, overflow, errorHandler); + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return delegate.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return delegate.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(tasks); + } + } + + public static T configureChannelBuffers(Consumer logSink, T chan, + int receiveBufSize, int sendBufSize) { + + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Initial receive buffer size is: %d".formatted(bufsize)); + bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Initial send buffer size is: %d".formatted(bufsize)); + } + if (trySetReceiveBufferSize(logSink, chan, receiveBufSize)) { + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Receive buffer size configured: %d".formatted(bufsize)); + } + } + if (trySetSendBufferSize(logSink, chan, sendBufSize)) { + if (logSink != null) { + int bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Send buffer size configured: %d".formatted(bufsize)); + } + } + return chan; + } + + public static boolean trySetReceiveBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set receive buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static boolean trySetSendBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set send buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static int getSoReceiveBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_RCVBUF); + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + public static int getSoSendBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_SNDBUF); + } catch (IOException x) { + if (logSink!= null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + + /** + * Try to figure out whether local and remote addresses are compatible. + * Used to diagnose potential communication issues early. + * This is a best effort, and there is no guarantee that all potential + * conflicts will be detected. + * @param local local address + * @param peer peer address + * @return a message describing the conflict, if any, or {@code null} if no + * conflict was detected. + */ + public static String addressConflict(SocketAddress local, SocketAddress peer) { + if (local == null || peer == null) return null; + if (local.equals(peer)) { + return "local endpoint and remote endpoint are bound to the same IP address and port"; + } + if (!(local instanceof InetSocketAddress li) || !(peer instanceof InetSocketAddress pi)) { + return null; + } + var laddr = li.getAddress(); + var paddr = pi.getAddress(); + if (!laddr.isAnyLocalAddress() && !paddr.isAnyLocalAddress()) { + if (laddr.getClass() != paddr.getClass()) { // IPv4 vs IPv6 + if ((laddr instanceof Inet6Address laddr6 && !laddr6.isIPv4CompatibleAddress()) + || (paddr instanceof Inet6Address paddr6 && !paddr6.isIPv4CompatibleAddress())) { + return "local endpoint IP (%s) and remote endpoint IP (%s) don't match" + .formatted(laddr.getClass().getSimpleName(), + paddr.getClass().getSimpleName()); + } + } + } + if (li.getPort() != pi.getPort()) return null; + if (li.getAddress().isAnyLocalAddress() && pi.getAddress().isLoopbackAddress()) { + return "local endpoint (wildcard) and remote endpoint (loopback) ports conflict"; + } + if (pi.getAddress().isAnyLocalAddress() && li.getAddress().isLoopbackAddress()) { + return "local endpoint (loopback) and remote endpoint (wildcard) ports conflict"; + } + return null; + } + /** * {@return the exception the given {@code cf} was completed with, * or a {@link CancellationException} if the given {@code cf} was diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java new file mode 100644 index 00000000000..46d4ebeb772 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.frame; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Optional; + +public final class AltSvcFrame extends Http2Frame { + + public static final int TYPE = 0xa; + + private final int length; + private final String origin; + private final String altSvcValue; + + private static final Charset encoding = StandardCharsets.US_ASCII; + + // Strings should be US-ASCII. This is checked by the FrameDecoder. + public AltSvcFrame(int streamid, int flags, Optional originVal, String altValue) { + super(streamid, flags); + this.origin = originVal.orElse(""); + this.altSvcValue = Objects.requireNonNull(altValue); + this.length = 2 + origin.length() + altValue.length(); + assert origin.length() == origin.getBytes(encoding).length; + assert altSvcValue.length() == altSvcValue.getBytes(encoding).length; + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return length; + } + + public String getOrigin() { + return origin; + } + + public String getAltSvcValue() { + return altSvcValue; + } + + @Override + public String toString() { + return super.toString() + + ", origin=" + this.origin + + ", alt-svc: " + altSvcValue; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java index 7ebfa090830..da05f6392c1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -26,11 +26,13 @@ package jdk.internal.net.http.frame; import java.io.IOException; -import java.lang.System.Logger.Level; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; @@ -344,6 +346,8 @@ public class FramesDecoder { return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); case ContinuationFrame.TYPE: return parseContinuationFrame(frameLength, frameStreamid, frameFlags); + case AltSvcFrame.TYPE: + return parseAltSvcFrame(frameLength, frameStreamid, frameFlags); default: // RFC 7540 4.1 // Implementations MUST ignore and discard any frame that has a type that is unknown. @@ -557,4 +561,32 @@ public class FramesDecoder { return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); } + private Http2Frame parseAltSvcFrame(int frameLength, int frameStreamid, int frameFlags) { + var len = getShort(); + byte[] origin; + Optional originUri = Optional.empty(); + if (len > 0) { + origin = getBytes(len); + if (!isUSAscii(origin)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + originUri = Optional.of(new String(origin, StandardCharsets.US_ASCII)); + } + byte[] altbytes = getBytes(frameLength - 2 - len); + if (!isUSAscii(altbytes)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + String altSvc = new String(altbytes, StandardCharsets.US_ASCII); + return new AltSvcFrame(frameStreamid, 0, originUri, altSvc); + } + + static boolean isUSAscii(byte[] bytes) { + for (int i=0; i < bytes.length; i++) { + if (bytes[i] < 0) return false; + } + return true; + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java index 4fdd4acd661..2ee2083c22c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java @@ -26,6 +26,7 @@ package jdk.internal.net.http.frame; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -70,6 +71,7 @@ public class FramesEncoder { case GoAwayFrame.TYPE -> encodeGoAwayFrame((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> encodeWindowUpdateFrame((WindowUpdateFrame) frame); case ContinuationFrame.TYPE -> encodeContinuationFrame((ContinuationFrame) frame); + case AltSvcFrame.TYPE -> encodeAltSvcFrame((AltSvcFrame) frame); default -> throw new UnsupportedOperationException("Not supported frame " + frame.type() + " (" + frame.getClass().getName() + ")"); }; @@ -227,6 +229,20 @@ public class FramesEncoder { return join(buf, frame.getHeaderBlock()); } + private List encodeAltSvcFrame(AltSvcFrame frame) { + final int length = frame.length(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, AltSvcFrame.TYPE, NO_FLAGS, frame.streamid); + final String origin = frame.getOrigin(); + assert (origin.length() & 0xffff0000) == 0; + buf.putShort((short)origin.length()); + if (!origin.isEmpty()) + buf.put(frame.getOrigin().getBytes(StandardCharsets.US_ASCII)); + buf.put(frame.getAltSvcValue().getBytes(StandardCharsets.US_ASCII)); + buf.flip(); + return List.of(buf); + } + private List joinWithPadding(ByteBuffer buf, List data, int padLength) { int len = data.size(); if (len == 0) return List.of(buf, getPadding(padLength)); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java index f837645696f..469d06cef0c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java @@ -91,8 +91,9 @@ public abstract class Http2Frame { case PingFrame.TYPE -> "PING"; case PushPromiseFrame.TYPE -> "PUSH_PROMISE"; case WindowUpdateFrame.TYPE -> "WINDOW_UPDATE"; + case AltSvcFrame.TYPE -> "ALTSVC"; - default -> "UNKNOWN"; + default -> "UNKNOWN"; }; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java index 881be12c67c..9cdd604efd6 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java @@ -282,7 +282,7 @@ public final class Decoder { if (endOfHeaderBlock && state != State.READY) { logger.log(NORMAL, () -> format("unexpected end of %s representation", state)); - throw new IOException("Unexpected end of header block"); + throw new ProtocolException("Unexpected end of header block"); } if (endOfHeaderBlock) { size = indexed = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java index a233e0f3a38..979c3ded2bc 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2022, 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 @@ -40,9 +40,10 @@ import java.nio.ByteBuffer; // // The encoding is simple and well known: 1 byte <-> 1 char // -final class ISO_8859_1 { +public final class ISO_8859_1 { - private ISO_8859_1() { } + private ISO_8859_1() { + } public static final class Reader { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java index 427c2504de5..c6b4c51761b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java @@ -619,7 +619,7 @@ public final class QuickHuffman { } } - static final class Reader implements Huffman.Reader { + public static final class Reader implements Huffman.Reader { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -703,7 +703,7 @@ public final class QuickHuffman { } } - static final class Writer implements Huffman.Writer { + public static final class Writer implements Huffman.Writer { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -782,12 +782,26 @@ public final class QuickHuffman { @Override public int lengthOf(CharSequence value, int start, int end) { - int len = 0; - for (int i = start; i < end; i++) { - char c = value.charAt(i); - len += codeLengthOf(c); - } - return bytesForBits(len); + return QuickHuffman.lengthOf(value, start, end); } } + + public static int lengthOf(CharSequence value, int start, int end) { + int len = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + len += codeLengthOf(c); + } + return bytesForBits(len); + } + + public static int lengthOf(CharSequence value) { + return lengthOf(value, 0, value.length()); + } + + /* Used to calculate the number of bytes required for Huffman encoding */ + + public static boolean isHuffmanBetterFor(CharSequence input) { + return lengthOf(input) < input.length(); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java new file mode 100644 index 00000000000..ca734d74ae7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3; + +import java.util.Objects; + +import jdk.internal.net.http.http3.frames.SettingsFrame; + +/** + * Represents the settings that are conveyed in a HTTP3 SETTINGS frame for a HTTP3 connection + */ +public record ConnectionSettings( + long maxFieldSectionSize, + long qpackMaxTableCapacity, + long qpackBlockedStreams) { + + // we use -1 (an internal value) to represent unlimited + public static final long UNLIMITED_MAX_FIELD_SECTION_SIZE = -1; + + public static ConnectionSettings createFrom(final SettingsFrame frame) { + Objects.requireNonNull(frame); + // default is unlimited as per RFC-9114 section 7.2.4.1 + final long maxFieldSectionSize = getOrDefault(frame, SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, + UNLIMITED_MAX_FIELD_SECTION_SIZE); + // default is zero as per RFC-9204 section 5 + final long qpackMaxTableCapacity = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0); + // default is zero as per RFC-9204, section 5 + final long qpackBlockedStreams = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, 0); + return new ConnectionSettings(maxFieldSectionSize, qpackMaxTableCapacity, qpackBlockedStreams); + } + + private static long getOrDefault(final SettingsFrame frame, final int paramId, final long defaultValue) { + final long val = frame.getParameter(paramId); + if (val == -1) { + return defaultValue; + } + return val; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java new file mode 100644 index 00000000000..423fb27f844 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3; + +import java.util.HexFormat; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * This enum models HTTP/3 error codes as specified in + * RFC 9114, Section 8, + * augmented with QPack error codes as specified in + * RFC 9204, Section 6. + */ +public enum Http3Error { + + /** + * No error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * This is used when the connection or stream
      +     * needs to be closed, but there is no error to signal.
      +     * }
      + */ + H3_NO_ERROR (0x0100), // 256 + + /** + * General protocol error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * Peer violated protocol requirements in a way that does
      +     * not match a more specific error code, or endpoint declines
      +     * to use the more specific error code.
      +     * }
      + */ + H3_GENERAL_PROTOCOL_ERROR (0x0101), // 257 + + /** + * Internal error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * An internal error has occurred in the HTTP stack.
      +     * }
      + */ + H3_INTERNAL_ERROR (0x0102), // 258 + + /** + * Stream creation error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The endpoint detected that its peer created a stream that
      +     * it will not accept.
      +     * }
      + */ + H3_STREAM_CREATION_ERROR (0x0103), // 259 + + /** + * Critical stream closed error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * A stream required by the HTTP/3 connection was closed or reset.
      +     * }
      + */ + H3_CLOSED_CRITICAL_STREAM (0x0104), // 260 + + /** + * Frame unexpected error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * A frame was received that was not permitted in the
      +     * current state or on the current stream.
      +     * }
      + */ + H3_FRAME_UNEXPECTED (0x0105), // 261 + + /** + * Frame error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * A frame that fails to satisfy layout requirements or with
      +     * an invalid size was received.
      +     * }
      + */ + H3_FRAME_ERROR (0x0106), // 262 + + /** + * Excessive load error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The endpoint detected that its peer is exhibiting a behavior
      +     * that might be generating excessive load.
      +     * }
      + */ + H3_EXCESSIVE_LOAD (0x0107), // 263 + + /** + * Stream ID or Push ID error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * A Stream ID or Push ID was used incorrectly, such as exceeding
      +     * a limit, reducing a limit, or being reused.
      +     * }
      + */ + H3_ID_ERROR (0x0108), // 264 + + /** + * Settings error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * An endpoint detected an error in the payload of a SETTINGS frame.
      +     * }
      + */ + H3_SETTINGS_ERROR (0x0109), // 265 + + /** + * Missing settings error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * No SETTINGS frame was received at the beginning of the control
      +     * stream.
      +     * }
      + */ + H3_MISSING_SETTINGS (0x010a), // 266 + + /** + * Request rejected error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * A server rejected a request without performing any application
      +     * processing.
      +     * }
      + */ + H3_REQUEST_REJECTED (0x010b), // 267 + + /** + * Request cancelled error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The request or its response (including pushed response) is
      +     * cancelled.
      +     * }
      + */ + H3_REQUEST_CANCELLED (0x010c), // 268 + + /** + * Request incomplete error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The client's stream terminated without containing a
      +     * fully-formed request.
      +     * }
      + */ + H3_REQUEST_INCOMPLETE (0x010d), //269 + + /** + * Message error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * An HTTP message was malformed and cannot be processed.
      +     * }
      + */ + H3_MESSAGE_ERROR (0x010e), // 270 + + /** + * Connect error. + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The TCP connection established in response to a CONNECT
      +     * request was reset or abnormally closed.
      +     * }
      + */ + H3_CONNECT_ERROR (0x010f), // 271 + + /** + * Version fallback error + *

      + * From + * RFC 9114, Section 8.1: + *

      {@code
      +     * The requested operation cannot be served over HTTP/3.
      +     * The peer should retry over HTTP/1.1.
      +     * }
      + */ + H3_VERSION_FALLBACK (0x0110), // 272 + + /** + * QPack decompression error + *

      + * From + * RFC 9204, Section 6: + *

      {@code
      +     * The decoder failed to interpret an encoded field section
      +     * and is not able to continue decoding that field section.
      +     * }
      + */ + QPACK_DECOMPRESSION_FAILED (0x0200), // 512 + + /** + * Qpack encoder stream error. + *

      + * From + * RFC 9204, Section 6: + *

      {@code
      +     * The decoder failed to interpret an encoder instruction
      +     * received on the encoder stream.
      +     * }
      + */ + QPACK_ENCODER_STREAM_ERROR (0x0201), // 513 + + /** + * Qpack decoder stream error + *

      + * From + * RFC 9204, Section 6: + *

      {@code
      +     * The encoder failed to interpret a decoder instruction
      +     * received on the decoder stream.
      +     * }
      + */ + QPACK_DECODER_STREAM_ERROR (0x0202); // 514 + + final long errorCode; + Http3Error(long errorCode) { + this.errorCode = errorCode; + } + + public long code() { + return errorCode; + } + + public static Optional fromCode(long code) { + return Stream.of(values()).filter((v) -> v.code() == code) + .findFirst(); + } + + public static String stringForCode(long code) { + return fromCode(code).map(Http3Error::name).orElse(unknown(code)); + } + + private static String unknown(long code) { + return "UnknownError(code=0x" + HexFormat.of().withUpperCase().toHexDigits(code) + ")"; + } + + /** + * {@return true if the given code is {@link Http3Error#H3_NO_ERROR} or equivalent} + * Unknown error codes are treated as equivalent to {@code H3_NO_ERROR} + * @param code an HTTP/3 code error code + */ + public static boolean isNoError(long code) { + return fromCode(code).orElse(H3_NO_ERROR) == Http3Error.H3_NO_ERROR; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java new file mode 100644 index 00000000000..4cb8aa051fd --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Random; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; + +/** + * Super class for all HTTP/3 frames. + */ +public abstract non-sealed class AbstractHttp3Frame implements Http3Frame { + public static final Random RANDOM = new Random(); + final long type; + public AbstractHttp3Frame(long type) { + this.type = type; + } + + public final String typeAsString() { + return asString(type()); + } + + @Override + public long type() { + return type; + } + + + /** + * Computes the size of this frame. This corresponds to + * the {@linkplain #length()} of the frame's payload, plus the + * size needed to encode this length, plus the size needed to + * encode the frame type. + * + * @return the size of this frame. + */ + public long size() { + var len = length(); + return len + VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + public int headersSize() { + var len = length(); + return VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + @Override + public long streamingLength() { + return 0; + } + + protected static long decodeRequiredType(final BuffersReader reader, final long expectedType) { + final long type = VariableLengthEncoder.decode(reader); + if (type < 0) throw new BufferUnderflowException(); + // TODO: throw an exception instead? + assert type == expectedType : "bad frame type: " + type + " expected: " + expectedType; + return type; + } + + protected static MalformedFrame checkPayloadSize(long frameType, + BuffersReader reader, + long start, + long length) { + // check position after reading payload + long read = reader.position() - start; + if (length != read) { + reader.position(start + length); + reader.release(); + return new MalformedFrame(frameType, + Http3Error.H3_FRAME_ERROR.code(), + "payload length mismatch (length=%s, read=%s)" + .formatted(length, start)); + + } + + assert length == reader.position() - start; + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(typeAsString()) + .append(": length=") + .append(length()); + return sb.toString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java new file mode 100644 index 00000000000..f470efb9b27 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents the CANCEL_PUSH HTTP3 frame + */ +public final class CancelPushFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.CANCEL_PUSH_FRAME; + private final long length; + private final long pushId; + + public CancelPushFrame(final long pushId) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId); + } + + // only used when constructing the frame during decoding content over a stream + private CancelPushFrame(final long pushId, final long length) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return pushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the push id that needs to be cancelled + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed pushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in CANCEL_PUSH frame: " + length); + } + + long pushId = VariableLengthEncoder.decode(reader); + if (pushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new CancelPushFrame(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java new file mode 100644 index 00000000000..97b475774b2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + + +/** + * This class models an HTTP/3 DATA frame. + * @apiNote + * An instance of {@code DataFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see PartialFrame + */ +public final class DataFrame extends PartialFrame { + + /** + * The DATA frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.DATA_FRAME; + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public DataFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java new file mode 100644 index 00000000000..a51c71e0a05 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongPredicate; +import java.util.function.Supplier; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; + +/** + * A FramesDecoder accumulates buffers until a frame can be + * decoded. It also supports decoding {@linkplain PartialFrame + * partial frames} and {@linkplain #readPayloadBytes() reading + * their payload} incrementally. + * @apiNote + * When the frame decoder {@linkplain #poll() returns} a partial + * frame, the same frame will be returned until its payload has been + * {@linkplain PartialFrame#remaining() fully} {@linkplain #readPayloadBytes() + * read}. + * The caller is supposed to call {@link #readPayloadBytes()} until + * {@link #poll()} returns a different frame. At this point there will be no + * {@linkplain PartialFrame#remaining() remaining} payload bytes to read for + * the previous frame. + *
      + * The sequence of calls: {@snippet : + * framesDecoder.submit(buffer); + * while ((frame = framesDecoder.poll()) != null) { + * if (frame instanceof PartialFrame partial) { + * var nextPayloadBytes = framesDecoder.readPayloadBytes(); + * if (nextPayloadBytes == null || nextPayloadBytes.isEmpty()) { + * // no more data is available at this moment + * break; + * } + * // nextPayloadBytes are the next bytes for the payload + * // of the partial frame + * deliverBytes(partial, nextPayloadBytes); + * } else ... + * // got a full frame... + * } + * } + * makes it possible to incrementally deliver payload bytes for + * a frame - since {@code poll()} will always return the same partial + * frame until all its payload has been read. + */ +public class FramesDecoder { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final ListBuffersReader framesReader = BuffersReader.list(); + private final ReentrantLock lock = new ReentrantLock(); + + private final Supplier dbgTag; + private final LongPredicate isAllowed; + + // the current partial frame or null + PartialFrame partialFrame; + boolean eof; + + /** + * A new {@code FramesDecoder} that accepts all frames. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag) { + this(dbgTag, FramesDecoder::allAllowed); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag, LongPredicate isAllowed) { + this(() -> dbgTag, Objects.requireNonNull(isAllowed)); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(Supplier dbgTag, LongPredicate isAllowed) { + this.dbgTag = dbgTag; + this.isAllowed = Objects.requireNonNull(isAllowed); + } + + String dbgTag() { return dbgTag.get(); } + + /** + * Submit a new buffer to this frames decoder + * @param buffer a new buffer from the stream + */ + public void submit(ByteBuffer buffer) { + lock.lock(); + try { + if (buffer == QuicStreamReader.EOF) { + eof = true; + } else { + framesReader.add(buffer); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return an {@code Http3Frame}, possibly {@linkplain PartialFrame partial}, + * or {@code null} if not enough bytes have been receive to decode (at least + * partially) a frame} + * If a frame is illegal or not allowed, a {@link MalformedFrame} is + * returned. The caller is supposed to {@linkplain #clear() clear} all data + * and proceed to close the connection in that case. + */ + public Http3Frame poll() { + lock.lock(); + try { + if (partialFrame != null) { + if (partialFrame.remaining() != 0) { + return partialFrame; + } else partialFrame = null; + } + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + if (frame instanceof PartialFrame partial) { + partialFrame = partial; + } + return frame; + } finally { + lock.unlock(); + } + } + + /** + * {@return the next payload bytes for the current partial frame, + * or {@code null} if no partial frame} + * If EOF has been reached ({@link QuicStreamReader#EOF EOF} was + * {@linkplain #submit(ByteBuffer) submitted}, and all buffers have + * been read, the returned list will contain {@link QuicStreamReader#EOF + * EOF} + */ + public List readPayloadBytes() { + lock.lock(); + try { + if (partialFrame == null || partialFrame.remaining() == 0) { + partialFrame = null; + return null; + } + if (eof && !framesReader.hasRemaining()) { + return List.of(QuicStreamReader.EOF); + } + return partialFrame.nextPayloadBytes(framesReader); + } finally { + lock.unlock(); + } + } + + /** + * {@return true if EOF has been reached and all buffers have been read} + */ + public boolean eof() { + lock.lock(); + try { + if (!eof) return false; + if (!framesReader.hasRemaining()) return true; + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + var pos = framesReader.position(); + try { + // if there's not enough data to decode a new frame or a new + // partial frame then since no more data will ever come, we do have + // reached EOF. If however, we can read a frame from the remaining + // data in the buffer, then EOF is not reached yet. + // The next call to poll() will return that frame. + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + return frame == null; + } finally { + // restore position for the next call to poll. + framesReader.position(pos); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return true if all buffers have been read} + */ + public boolean clean() { + lock.lock(); + try { + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + return !framesReader.hasRemaining(); + } finally { + lock.unlock(); + } + } + + /** + * Clears any unconsumed buffers. + */ + public void clear() { + lock.lock(); + try { + partialFrame = null; + framesReader.clear(); + } finally { + lock.unlock(); + } + } + + /** + * Can be overridden by subclasses to avoid parsing a frame + * fully if the frame is not allowed on this stream, or + * according to the stream state. + * + * @implSpec + * This method delegates to the {@linkplain #FramesDecoder(String, LongPredicate) + * predicate} given at construction time. If {@linkplain #FramesDecoder(String) + * no predicate} was given this method returns true. + * + * @param frameType the frame type + * @return true if the frame is allowed + */ + protected boolean isAllowed(long frameType) { + return isAllowed.test(frameType); + } + + /** + * A predicate that returns true for all frames types allowed + * on the server->client control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on the client->server control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnClientControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on a request/response stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnRequestStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + + /** + * A predicate that returns true for all frames types allowed + * on a push promise stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnPromiseStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + private static boolean allAllowed(long frameType) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java new file mode 100644 index 00000000000..274a1af3a56 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a GOAWAY HTTP3 frame + */ +public final class GoAwayFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.GOAWAY_FRAME; + private final long length; + // represents either a stream id or a push id depending on the context + // of the frame + private final long id; + + public GoAwayFrame(final long id) { + super(TYPE); + this.id = id; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.id); + } + + // only used when constructing the frame during decoding content over a stream + private GoAwayFrame(final long length, final long id) { + super(Http3FrameType.GOAWAY.type()); + this.length = length; + this.id = id; + } + + @Override + public long length() { + return this.length; + } + + /** + * {@return the id of either the stream or a push promise, depending on the context + * of this frame} + */ + public long getTargetId() { + return this.id; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the stream id/push id + VariableLengthEncoder.encode(buf, this.id); + } + + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + final long position = reader.position(); + // read the frame type + decodeRequiredType(reader, Http3FrameType.GOAWAY.type()); + // read length of the payload + final long length = VariableLengthEncoder.decode(reader); + if (length < 0 || length > reader.remaining()) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed targetId + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in GOAWAY frame: " + length); + } + + // read stream id / push id + final long targetId = VariableLengthEncoder.decode(reader); + if (targetId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new GoAwayFrame(length, targetId); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(super.toString()).append(" stream/push id: ").append(this.id); + return sb.toString(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java new file mode 100644 index 00000000000..5d7672458a3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +/** + * This class models an HTTP/3 HEADERS frame. + * @apiNote + * An instance of {@code HeadersFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + */ +public final class HeadersFrame extends PartialFrame { + + /** + * The HEADERS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.HEADERS_FRAME; + + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public HeadersFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java new file mode 100644 index 00000000000..c4b234553ad --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import static jdk.internal.net.http.http3.frames.Http3FrameType.DATA; +import static jdk.internal.net.http.http3.frames.Http3FrameType.HEADERS; +import static jdk.internal.net.http.http3.frames.Http3FrameType.PUSH_PROMISE; +import static jdk.internal.net.http.http3.frames.Http3FrameType.UNKNOWN; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; +import static jdk.internal.net.http.http3.frames.Http3FrameType.isIllegalType; + +/** + * An HTTP/3 frame + */ +public sealed interface Http3Frame permits AbstractHttp3Frame { + + /** + * {@return the type of this frame} + */ + long type(); + + /** + * {@return the length of this frame} + */ + long length(); + + + /** + * {@return the portion of the frame payload that can be read + * after the frame was created, when the current frame + * can be read as a partial frame, otherwise 0, if + * the payload can't be streamed} + */ + default long streamingLength() { return 0;} + + /** + * Attempts to decode an HTTP/3 frame from the bytes accumulated + * in the reader. + * + * @apiNote + * + * If an error is detected while parsing the frame, a {@link MalformedFrame} + * error will be returned + * + * @param reader the reader containing the bytes + * @param isFrameTypeAllowed a predicate to test whether a given + * frame type is allowed in this context + * @param debug a logger to log debug traces + * @return the decoded frame, or {@code null} if some bytes are + * missing to decode the frame + */ + static Http3Frame decode(BuffersReader reader, LongPredicate isFrameTypeAllowed, Logger debug) { + long pos = reader.position(); + long limit = reader.limit(); + long remaining = reader.remaining(); + long type = -1; + long before = reader.read(); + Http3Frame frame; + try { + int tsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos); + if (tsize == -1 || remaining - tsize < 0) return null; + type = VariableLengthEncoder.peekEncodedValue(reader, pos); + if (type == -1) return null; + if (isIllegalType(type) || !isFrameTypeAllowed.test(type)) { + var msg = "H3_FRAME_UNEXPECTED: Frame " + + asString(type) + + " is not allowed on this stream"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_UNEXPECTED.code(), msg); + reader.clear(); + return frame; + } + + int lsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize); + if (lsize == -1 || remaining - tsize - lsize < 0) return null; + final long length = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize); + var frameType = Http3FrameType.forType(type); + if (debug.on()) { + debug.log("Decoding %s(length=%s)", frameType, length); + } + if (frameType == UNKNOWN) { + if (debug.on()) { + debug.log("decode partial unknown frame: " + + "pos:%s, limit:%s, remaining:%s," + + " tsize:%s, lsize:%s, length:%s", + pos, limit, remaining, tsize, lsize, length); + } + reader.position(pos + tsize + lsize); + reader.release(); + return new UnknownFrame(type, length); + } else if (frameType.maxLength() < length) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too long"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + + if (frameType == HEADERS) { + if (length == 0) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + reader.position(pos + tsize + lsize); + reader.release(); + return new HeadersFrame(length); + } + + if (frameType == DATA) { + reader.position(pos + tsize + lsize); + reader.release(); + return new DataFrame(length); + } + + if (frameType == PUSH_PROMISE) { + int pidsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize + lsize); + if (length == 0 || length < pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too short to fit pushID"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (length == pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (pidsize == -1 || remaining - tsize - lsize - pidsize < 0) return null; + long pushId = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize + lsize); + reader.position(pos + tsize + lsize + pidsize); + reader.release(); + return new PushPromiseFrame(pushId, length - pidsize); + } + + if (length + tsize + lsize > reader.remaining()) { + // we haven't moved the reader's position. + // we'll be called back when new bytes are available and + // we'll resume reading type + length from the same position + // again, until we have enough to read the frame. + return null; + } + + assert isFrameTypeAllowed.test(type); + + frame = switch(frameType) { + case SETTINGS -> SettingsFrame.decodeFrame(reader, debug); + case GOAWAY -> GoAwayFrame.decodeFrame(reader, debug); + case CANCEL_PUSH -> CancelPushFrame.decodeFrame(reader, debug); + case MAX_PUSH_ID -> MaxPushIdFrame.decodeFrame(reader, debug); + default -> { + reader.position(pos + tsize + lsize); + reader.release(); + yield new UnknownFrame(type, length); + } + }; + + long read; + if (frame instanceof MalformedFrame || frame == null) { + return frame; + } else if ((read = (reader.read() - before - tsize - lsize)) != length) { + String msg = ("H3_FRAME_ERROR: Frame %s payload length does not match" + + " frame length (length=%s, payload=%s)") + .formatted(asString(type), length, read); + if (debug.on()) debug.log(msg); + reader.release(); // mark reader read + reader.position(reader.position() + tsize + lsize + length); + reader.release(); + return new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + } else { + return frame; + } + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to decode frame", t); + reader.clear(); // mark reader read + return new MalformedFrame(type, Http3Error.H3_INTERNAL_ERROR.code(), t.getMessage(), t); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java new file mode 100644 index 00000000000..858e10fa6a0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; +import java.util.stream.Stream; + +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_INTEGER_LENGTH; + +/** + * An enum to model HTTP/3 frame types. + */ +public enum Http3FrameType { + + /** + * Used to identify an HTTP/3 frame whose type is unknown + */ + UNKNOWN(-1, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 DATA frame + */ + DATA(TYPE.DATA_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 HEADERS frame + */ + HEADERS(TYPE.HEADERS_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 CANCEL_PUSH frame + */ + CANCEL_PUSH(TYPE.CANCEL_PUSH_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 SETTINGS frame + */ + SETTINGS(TYPE.SETTINGS_FRAME, TYPE.MAX_SETTINGS_LENGTH), + /** + * Used to identify an HTTP/3 PUSH_PROMISE frame + */ + PUSH_PROMISE(TYPE.PUSH_PROMISE_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 GOAWAY frame + */ + GOAWAY(TYPE.GOAWAY_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 MAX_PUSH_ID_FRAME frame + */ + MAX_PUSH_ID(TYPE.MAX_PUSH_ID_FRAME, MAX_INTEGER_LENGTH); + + /** + * A class to hold type constants + */ + static final class TYPE { + private TYPE() { throw new InternalError(); } + + // Frames types + public static final int DATA_FRAME = 0x00; + public static final int HEADERS_FRAME = 0x01; + public static final int CANCEL_PUSH_FRAME = 0x03; + public static final int SETTINGS_FRAME = 0x04; + public static final int PUSH_PROMISE_FRAME = 0x05; + public static final int GOAWAY_FRAME = 0x07; + public static final int MAX_PUSH_ID_FRAME = 0x0d; + + // The maximum size a settings frame can have. + // This is a limit imposed by our implementation. + // There are only 7 settings defined in the current + // specification, but we will allow for a frame to + // contain up to 80. Past that limit, we will consider + // the frame to be malformed: + // 8 x 10 x (max sizeof(id) + max sizeof(value)) = 80 x 16 bytes + public static final long MAX_SETTINGS_LENGTH = + 10L * 8L * MAX_INTEGER_LENGTH * 2L; + } + + + // This is one of the values defined in TYPE above, or + // -1 for the UNKNOWN frame types. + private final int type; + private final long maxLength; + private Http3FrameType(int type, long maxLength) { + this.type = type; + this.maxLength = maxLength; + } + + /** + * {@return the frame type, as defined by HTTP/3} + */ + public long type() { return type;} + + /** + * {@return the maximum length a frame of this type + * can take} + */ + public long maxLength() { + return maxLength; + } + + /** + * {@return the HTTP/3 frame type, as an int} + * + * @apiNote + * HTTP/3 defines frames type as variable length integers + * in the range [0, 2^62-1]. However, the few standard frame + * types registered for HTTP/3 and modeled by this enum + * class can be coded as an int. + * This method provides a convenient way to access the frame + * type as an int, which avoids having to cast when using + * the value in switch statements. + */ + public int intType() { return type;} + + /** + * {@return the {@link Http3FrameType} corresponding to the given + * {@code type}, or {@link #UNKNOWN} if no corresponding + * {@link Http3FrameType} instance is found} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static Http3FrameType forType(long type) { + return Stream.of(values()) + .filter(x -> x.type == type) + .findFirst() + .orElse(UNKNOWN); + } + + /** + * {@return a string representation of the given type, suited for inclusion + * in log messages, exceptions, etc...} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static String asString(long type) { + String str = null; + if (type >= Integer.MIN_VALUE && type <= Integer.MAX_VALUE) { + str = switch ((int)type) { + case TYPE.DATA_FRAME -> DATA.name(); // 0x00 + case TYPE.HEADERS_FRAME -> HEADERS.name(); // 0x01 + case 0x02 -> "RESERVED(0x02)"; + case TYPE.CANCEL_PUSH_FRAME -> CANCEL_PUSH.name(); // 0x03 + case TYPE.SETTINGS_FRAME -> SETTINGS.name(); // 0x04 + case TYPE.PUSH_PROMISE_FRAME -> PUSH_PROMISE.name(); // 0x05 + case 0x06 -> "RESERVED(0x06)"; + case TYPE.GOAWAY_FRAME -> GOAWAY.name(); // 0x07 + case 0x08 -> "RESERVED(0x08)"; + case 0x09 -> "RESERVED(0x09)"; + case TYPE.MAX_PUSH_ID_FRAME -> MAX_PUSH_ID.name(); // 0x0d + default -> null; + }; + } + if (str != null) return str; + if (isReservedType(type)) { + return "RESERVED(type=" + type + ")"; + } + return "UNKNOWN(type=" + type + ")"; + } + + /** + * {@return whether this frame type is illegal} + * This corresponds to HTTP/2 frame types that have no equivalent in + * HTTP/3. + * @param type the frame type + */ + public static boolean isIllegalType(long type) { + return type == 0x02 || type == 0x06 || type == 0x08 || type == 0x09; + } + + /** + * Whether the given type is one of the reserved frame + * types defined by HTTP/3. For any non-negative integer N: + * {@code 0x21 + 0x1f * N } + * is a reserved frame type that has no meaning. + * + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + * + * @return true if the given type matches the {@code 0x21 + 0x1f * N} + * pattern + */ + public static boolean isReservedType(long type) { + return type >= 0x21L && type <= MAX_ENCODED_INTEGER + && (type - 0x21L) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java new file mode 100644 index 00000000000..0d89666ec26 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.quic.BuffersReader; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * An instance of MalformedFrame can be returned by + * {@link AbstractHttp3Frame#decode(BuffersReader, LongPredicate, Logger)} + * when a malformed frame is detected. This should cause the caller + * to send an error to its peer, and possibly throw an + * exception to the higher layer. + */ +public class MalformedFrame extends AbstractHttp3Frame { + + private final long errorCode; + private final String msg; + private final Throwable cause; + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + */ + public MalformedFrame(long type, long errorCode, String msg) { + this(type, errorCode, msg, null); + } + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + * @param cause - internal cause for the error, if available + * (can be null) + */ + public MalformedFrame(long type, long errorCode, String msg, Throwable cause) { + super(type); + this.errorCode = errorCode; + this.msg = msg; + this.cause = cause; + } + + @Override + public String toString() { + return super.toString() + " MalformedFrame, Error: " + + Http3Error.stringForCode(errorCode) + + " reason: " + msg; + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long length() { + return 0; // Not Applicable + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long size() { + return 0; // Not applicable + } + + /** + * {@return the {@linkplain Http3Error#code() HTTP/3 error code} that + * should be reported to the peer} + */ + public long getErrorCode() { + return errorCode; + } + + /** + * {@return a message that describe the error} + */ + public String getMessage() { + return msg; + } + + /** + * {@return the cause of the error, if available, {@code null} otherwise} + * + * @apiNote + * This is useful for logging and diagnosis purpose, typically when the + * error is an {@linkplain Http3Error#H3_INTERNAL_ERROR internal error}. + */ + public Throwable getCause() { + return cause; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java new file mode 100644 index 00000000000..b4f35064da1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a MAX_PUSH_ID HTTP3 frame + */ +public final class MaxPushIdFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.MAX_PUSH_ID_FRAME; + + private final long length; + private final long maxPushId; + + public MaxPushIdFrame(final long maxPushId) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.maxPushId); + } + + // only used when constructing the frame during decoding content over a stream + private MaxPushIdFrame(final long maxPushId, final long length) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getMaxPushId() { + return this.maxPushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the max push id value + VariableLengthEncoder.encode(buf, this.maxPushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed maxPushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in MAX_PUSH_ID frame: " + length); + } + + long maxPushId = VariableLengthEncoder.decode(reader); + if (maxPushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new MaxPushIdFrame(maxPushId); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java new file mode 100644 index 00000000000..3cbbb814b82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; + +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; + +/** + * A PartialFrame helps to read the payload of a frame. + * This class is not multi-thread safe. + */ +public abstract sealed class PartialFrame + extends AbstractHttp3Frame + permits HeadersFrame, + DataFrame, + PushPromiseFrame, + UnknownFrame { + + private static final List NONE = List.of(); + private final long streamingLength; + private long remaining; + PartialFrame(long frameType, long streamingLength) { + super(frameType); + this.remaining = this.streamingLength = streamingLength; + } + + @Override + public final long streamingLength() { + return streamingLength; + } + + /** + * {@return the number of payload bytes that remains to read} + */ + public final long remaining() { + return remaining; + } + + /** + * Reads remaining payload bytes from the given {@link BuffersReader}. + * This method must not run concurrently with any code that submit + * new buffers to the {@link BuffersReader}. + * @param buffers a {@link BuffersReader} that contains payload bytes. + * @return the payload bytes available so far, an empty list if no + * bytes are available or the whole payload has already been + * read + */ + public final List nextPayloadBytes(BuffersReader buffers) { + var remaining = this.remaining; + if (remaining > 0) { + long available = buffers.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + this.remaining = remaining - read; + return buffers.getAndRelease(read); + } + } + return NONE; + } + + /** + * Reads remaining payload bytes from the given {@link ByteBuffer}. + * @param buffer a {@link ByteBuffer} that contains payload bytes. + * @return the payload bytes available in the given buffer, or + * {@code null} if all payload has been read. + */ + public final ByteBuffer nextPayloadBytes(ByteBuffer buffer) { + var remaining = this.remaining; + if (remaining > 0) { + int available = buffer.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + remaining -= read; + this.remaining = remaining; + assert read <= available; + int pos = buffer.position(); + int len = (int) read; + // always create a slice, so that we can move the position + // of the original buffer, as if the data had been read. + ByteBuffer next = buffer.slice(pos, len); + buffer.position(pos + len); + return next; + } else return buffer == QuicStreamReader.EOF ? buffer : buffer.slice(); + } + return null; + } + + /** + * Write the frame headers to the given buffer. + * + * @apiNote + * The caller will be responsible for writing the + * remaining {@linkplain #length() length} bytes of + * the frame content after writing the frame headers. + * + * @implSpec + * Usually the header of a frame is assumed to simply + * contain the frame type and frame length. + * Some subclasses of {@code AbstractHttp3Frame} may + * however include some additional information. + * For instance, {@link PushPromiseFrame} may consider + * the {@link PushPromiseFrame#getPushId() pushId} as + * being in part of the headers, and write it along + * in this method after the frame type and length. + * In such a case, a subclass would also need to + * override {@link #headersSize()} in order to add + * the size of the additional information written + * by {@link #writeHeaders(ByteBuffer)}. + * + * @param buf a buffer to write the headers into + */ + public void writeHeaders(ByteBuffer buf) { + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, type()); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + assert pos1 - pos0 == super.headersSize(); + } + + @Override + public String toString() { + var len = length(); + return "%s (partial: %s/%s)".formatted(this.getClass().getSimpleName(), len - remaining, len); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java new file mode 100644 index 00000000000..f95fa7f964d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a PUSH_PROMISE HTTP3 frame + */ +public final class PushPromiseFrame extends PartialFrame { + + /** + * The PUSH_PROMISE frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.PUSH_PROMISE_FRAME; + + private final long length; + private final long pushId; + + public PushPromiseFrame(final long pushId, final long fieldLength) { + super(TYPE, fieldLength); + if (pushId < 0 || pushId > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("invalid pushId: " + pushId); + } + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId) + fieldLength; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return this.pushId; + } + + /** + * Write the frame header and the promise {@link #getPushId() + * pushId} to the given buffer. The caller will be responsible + * for writing the remaining {@link #streamingLength()} bytes + * that constitutes the field section length. + * @param buf a buffer to write the headers into + */ + @Override + public void writeHeaders(ByteBuffer buf) { + super.writeHeaders(buf); + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * {@return the number of bytes needed to write the headers and + * the promised {@link #getPushId() pushId}}. + */ + @Override + public int headersSize() { + return super.headersSize() + VariableLengthEncoder.getEncodedSize(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java new file mode 100644 index 00000000000..90fabd47e68 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; + +/** + * This class models an HTTP/3 SETTINGS frame + */ +public class SettingsFrame extends AbstractHttp3Frame { + + // An array of setting parameters. + // The index is the parameter id, minus 1, the value is the parameter value + private final long[] parameters; + // HTTP/3 specifies some reserved identifier for which the parameter + // has no semantics and the value is undefined and should be ignored. + // It's excepted that at least one such parameter should be included + // in the settings frame to exercise the fact that undefined parameters + // should be ignored + private long undefinedId; + private long undefinedValue; + + /** + * The SETTINGS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.SETTINGS_FRAME; + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()) + .append(" Settings: "); + + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + sb.append(name(i+1)) + .append("=") + .append(parameters[i]) + .append(' '); + } + } + if (undefinedId != -1) { + sb.append(name(undefinedId)).append("=") + .append(undefinedValue).append(' '); + } + return sb.toString(); + } + + // TODO: should we use an enum instead? + // HTTP/2 only Parameters - receiving one of those should be + // considered as a protocol error of type SETTINGS_ERROR + public static final int ENABLE_PUSH = 0x2; + public static final int MAX_CONCURRENT_STREAMS = 0x3; + public static final int INITIAL_WINDOW_SIZE = 0x4; + public static final int MAX_FRAME_SIZE = 0x5; + // HTTP/3 Parameters + // This parameter was defined as HEADER_TABLE_SIZE in HTTP/2 + public static final int SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x1; + public static final int DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0; + // This parameter was defined as MAX_HEADER_LIST_SIZE in HTTP/2 + public static final int SETTINGS_MAX_FIELD_SECTION_SIZE = 0x6; + public static final long DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE = -1; + // Allow compression efficiency by allowing referencing dynamic table entries + // that are still in transit. This parameter specifies the number of streams + // that could become blocked. + public static final int SETTINGS_QPACK_BLOCKED_STREAMS = 0x7; + public static final int DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS = 0; + + public static final int MAX_PARAM = 0x7; + + // maps a parameter id to a parameter name + private String name(long i) { + if (i <= MAX_PARAM) { + return switch ((int)i) { + case SETTINGS_QPACK_MAX_TABLE_CAPACITY -> "SETTINGS_QPACK_MAX_TABLE_CAPACITY"; // 0x01 + case ENABLE_PUSH -> "ENABLE_PUSH"; // 0x02 + case MAX_CONCURRENT_STREAMS -> "MAX_CONCURRENT_STREAMS"; // 0x03 + case INITIAL_WINDOW_SIZE -> "INITIAL_WINDOW_SIZE"; // 0x04 + case MAX_FRAME_SIZE -> "MAX_FRAME_SIZE"; // 0x05 + case SETTINGS_MAX_FIELD_SECTION_SIZE -> "SETTINGS_MAX_FIELD_SECTION_SIZE"; // 0x06 + case SETTINGS_QPACK_BLOCKED_STREAMS -> "SETTINGS_QPACK_BLOCKED_STREAMS"; // 0x07 + default -> "UNKNOWN(0x00)"; // 0x00 ? + }; + } else if (isReservedId(i)) { + return "RESERVED(" + i + ")"; + } else { + return "UNKNOWN(" + i +")"; + } + } + + /** + * Creates a new HTTP/3 SETTINGS frame, including the given + * reserved identifier id and value pair. + * + * @implNote + * We only keep one reserved id/value pair - there's no + * reason to keep more... + * + * @param undefinedId the id of an undefined (reserved) parameter + * @param undefinedValue a random value for the undefined parameter + */ + public SettingsFrame(long undefinedId, long undefinedValue) { + super(TYPE); + parameters = new long [MAX_PARAM]; + Arrays.fill(parameters, -1); + assert undefinedId == -1 || isReservedId(undefinedId); + assert undefinedId != -1 || undefinedValue == -1; + this.undefinedId = undefinedId; + this.undefinedValue = undefinedValue; + } + + /** + * Creates a new empty SETTINGS frame, and allocate a random + * reserved id and value pair. + */ + public SettingsFrame() { + this(nextRandomReservedParameterId(), nextRandomParameterValue()); + } + + /** + * Get the parameter value for the given parameter id + * + * @param paramID the parameter id + * + * @return the value of the given parameter, if present, + * {@code -1}, if absent + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + * + */ + public synchronized long getParameter(int paramID) { + if (isIllegal(paramID)) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (undefinedId != -1 && paramID == undefinedId) + return undefinedValue; + if (paramID > MAX_PARAM) return -1; + return parameters[paramID - 1]; + } + + /** + * Sets the given parameter to the given value. + * + * @param paramID the parameter id + * @param value the parameter value + * + * @return this + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + */ + public synchronized SettingsFrame setParameter(long paramID, long value) { + // subclasses can override this to actually send + // an illegal parameter + if (isIllegal(paramID) || paramID < 1 || paramID > MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (paramID <= MAX_PARAM) { + parameters[(int)paramID - 1] = value; + } else if (isReservedId(paramID)) { + this.undefinedId = paramID; + this.undefinedValue = value; + } + return this; + } + + @Override + public long length() { + int len = 0; + int i = 0; + for (long p : parameters) { + if (p != -1) { + len += VariableLengthEncoder.getEncodedSize(i+1); + len += VariableLengthEncoder.getEncodedSize(p); + } + } + if (undefinedId != -1) { + assert isReservedId(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedValue); + } + return len; + } + + /** + * Writes this frame to the given buffer. + * + * @param buf a byte buffer to write this frame into + * + * @throws java.nio.BufferUnderflowException if the buffer + * doesn't have enough space + */ + public void writeFrame(ByteBuffer buf) { + long size = size(); + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, TYPE); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + VariableLengthEncoder.encode(buf, i+1); + VariableLengthEncoder.encode(buf, parameters[i]); + } + } + if (undefinedId != -1) { + // Setting identifiers of the format 0x1f * N + 0x21 for + // non-negative integer values of N are reserved to exercise + // the requirement that unknown identifiers be ignored. + // Such settings have no defined meaning. Endpoints SHOULD + // include at least one such setting in their SETTINGS frame + assert isReservedId(undefinedId); + VariableLengthEncoder.encode(buf, undefinedId); + VariableLengthEncoder.encode(buf, undefinedValue); + } + assert buf.position() - pos1 == len; + assert buf.position() == pos0 + size; + } + + /** + * Decodes a SETTINGS frame from the given reader. + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * + * @param reader a reader containing bytes + * + * @return a new SettingsFrame frame, or a MalformedFrame. + * + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + public static AbstractHttp3Frame decodeFrame(BuffersReader reader, Logger debug) { + final long pos = reader.position(); + decodeRequiredType(reader, TYPE); + final SettingsFrame frame = new SettingsFrame(-1, -1); + long length = VariableLengthEncoder.decode(reader); + + // is that OK? Find what's the actual limit for + // a frame length... + if (length > reader.remaining()) { + reader.position(pos); + throw new BufferUnderflowException(); + } + + // position before reading payload + long start = reader.position(); + + while (length > reader.position() - start) { + long id = VariableLengthEncoder.decode(reader); + long value = VariableLengthEncoder.decode(reader); + if (id == -1 || value == -1) { + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid SETTINGS frame contents."); + } + try { + frame.setParameter(id, value); + } catch (IllegalArgumentException iae) { + String msg = "H3_SETTINGS_ERROR: " + iae.getMessage(); + if (debug.on()) debug.log(msg, iae); + reader.position(start + length); + reader.release(); + return new MalformedFrame(TYPE, + Http3Error.H3_SETTINGS_ERROR.code(), + iae.getMessage(), + iae); + } + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return frame; + } + + public static SettingsFrame defaultRFCSettings() { + SettingsFrame f = new SettingsFrame() + .setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE) + .setParameter(SETTINGS_QPACK_MAX_TABLE_CAPACITY, + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY) + .setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + return f; + } + + public boolean isIllegal(long parameterId) { + // Parameters with 0x0, 0x2, 0x3, 0x4 and 0x5 ids are reserved, + // 0x6 is the legal one: + // https://www.rfc-editor.org/rfc/rfc9114.html#name-settings-parameters + // 0x1 and 0x7 defined by QPACK as a legal one: + // https://www.rfc-editor.org/rfc/rfc9204.html#name-configuration + return parameterId < SETTINGS_MAX_FIELD_SECTION_SIZE && + parameterId != SETTINGS_QPACK_MAX_TABLE_CAPACITY; + } + + public static long nextRandomParameterValue() { + long value = RANDOM.nextLong(0, MAX_ENCODED_INTEGER + 1); + assert value >= 0 && value <= MAX_ENCODED_INTEGER; + return value; + } + + private static final long MAX_N = (MAX_ENCODED_INTEGER - 0x21L) / 0x1fL; + public static long nextRandomReservedParameterId() { + long N = RANDOM.nextLong(0, MAX_N + 1); + long id = 0x1fL * N + 0x21L; + assert id <= MAX_ENCODED_INTEGER; + assert id >= 0x21L; + assert isReservedId(id) : "generated id is not undefined: " + id; + return id; + } + + /** + * Tells whether the given id is one of the undefined parameter ids that + * are reserved and have no meaning. + * + * @apiNote + * Setting identifiers of the format 0x1f * N + 0x21 + * for non-negative integer values of N are reserved to + * exercise the requirement that unknown identifiers be + * ignored + * + * @param id the parameter id + * + * @return true if this is one of the reserved identifiers + */ + public static boolean isReservedId(long id) { + return id >= 0x21 && id < MAX_ENCODED_INTEGER && (id - 0x21) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java new file mode 100644 index 00000000000..4426493ed7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.frames; + +/** + * A class to model an unknown or reserved frame. + * @apiNote + * From RFC 9114: + *
      + * Frame types of the format 0x1f * N + 0x21 for non-negative integer + * values of N are reserved to exercise the requirement that + * unknown types be ignored (Section 9). These frames have no semantics, + * and MAY be sent on any stream where frames are allowed to be sent. + * This enables their use for application-layer padding. Endpoints MUST NOT + * consider these frames to have any meaning upon receipt. + *
      + * + * @apiNote + * An instance of {@code UnknownFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + * */ +public final class UnknownFrame extends PartialFrame { + final long length; + UnknownFrame(long type, long length) { + super(type, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + + /** + * {@return true if this frame type is one of the reserved + * types} + */ + public boolean isReserved() { + return Http3FrameType.isReservedType(type); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java new file mode 100644 index 00000000000..8399a4550ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.streams; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; + +public final class Http3Streams { + public static final int CONTROL_STREAM_CODE = 0x00; + public static final int PUSH_STREAM_CODE = 0x01; + public static final int QPACK_ENCODER_STREAM_CODE = 0x02; + public static final int QPACK_DECODER_STREAM_CODE = 0x03; + + private Http3Streams() { throw new InternalError(); } + + public enum StreamType { + CONTROL(CONTROL_STREAM_CODE), + PUSH(PUSH_STREAM_CODE), + QPACK_ENCODER(QPACK_ENCODER_STREAM_CODE), + QPACK_DECODER(QPACK_DECODER_STREAM_CODE); + final int code; + StreamType(int code) { + this.code = code; + } + public final int code() { + return code; + } + public static Optional ofCode(long code) { + return EnumSet.allOf(StreamType.class).stream() + .filter(s -> s.code() == code) + .findFirst(); + } + } + + /** + * {@return an optional string that represents the error state of the + * stream, or {@code Optional.empty()} if no error code + * has been received or sent} + * @param stream a quic stream that may have errors + */ + public static Optional errorCodeAsString(QuicStream stream) { + long sndErrorCode = -1; + long rcvErrorCode = -1; + if (stream instanceof QuicReceiverStream rcv) { + rcvErrorCode = rcv.rcvErrorCode(); + } + if (stream instanceof QuicSenderStream snd) { + sndErrorCode = snd.sndErrorCode(); + } + if (rcvErrorCode >= 0 || sndErrorCode >= 0) { + Stream rcv = rcvErrorCode >= 0 + ? Stream.of("RCV: " + Http3Error.stringForCode(rcvErrorCode)) + : Stream.empty(); + Stream snd = sndErrorCode >= 0 + ? Stream.of("SND: " + Http3Error.stringForCode(sndErrorCode)) + : Stream.empty(); + return Optional.of(Stream.concat(rcv, snd) + .collect(Collectors.joining(",", "errorCode(", ")" ))); + } + return Optional.empty(); + } + + /** + * If the stream has errors, prints a message recording the + * {@linkplain #errorCodeAsString(QuicStream) error state} of the + * stream through the given logger. The message is of the form: + * {@code : }. + * If the given {@code name} is null or empty, {@code "Stream"} is substituted + * to {@code }. + * @param logger the logger to log through + * @param stream a quic stream that may have errors + * @param name a name for the stream, e.g {@code "Control stream"}, or {@code null}. + */ + public static void debugErrorCode(Logger logger, QuicStream stream, String name) { + if (logger.on()) { + var errorCodeStr = errorCodeAsString(stream); + if (errorCodeStr.isPresent()) { + var what = (name == null || name.isEmpty()) ? "Stream" : name; + logger.log("%s %s: %s", what, stream.streamId(), errorCodeStr.get()); + } + } + } + + public static boolean isReserved(long streamType) { + return streamType % 31 == 2 && streamType > 31; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java new file mode 100644 index 00000000000..5db767902f9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +/** + * A class that analyzes the first byte of the stream to figure + * out where to dispatch it. + */ +public abstract class PeerUniStreamDispatcher { + private final QuicStreamIntReader reader; + private final QuicReceiverStream stream; + private final CompletableFuture cf = new MinimalFuture<>(); + + /** + * Creates a {@code PeerUniStreamDispatcher} for the given stream. + * @param stream a new unidirectional stream opened by the peer + */ + protected PeerUniStreamDispatcher(QuicReceiverStream stream) { + this.reader = new QuicStreamIntReader(checkStream(stream), debug()); + this.stream = stream; + } + + private static QuicReceiverStream checkStream(QuicReceiverStream stream) { + if (!stream.isRemoteInitiated()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not peer initiated"); + } + if (stream.isBidirectional()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not unidirectional"); + } + return stream; + } + + /** + * {@return a completable future that will contain the dispatched stream, + * once dispatched, or a throwable if dispatching the stream failed} + */ + public CompletableFuture dispatchCF() { + return cf; + } + + // The dispatch function. + private void dispatch(Long result, Throwable error) { + if (result != null && result == Http3Streams.PUSH_STREAM_CODE) { + reader.readInt().whenComplete(this::dispatchPushStream); + return; + } + reader.stop(); + if (result != null) { + cf.complete(stream); + if (Http3Streams.isReserved(result)) { + // reserved stream type, 0x1f * N + 0x21 + reservedStreamType(result, stream); + return; + } + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } + if (result > Integer.MAX_VALUE) { + unknownStreamType(result, stream); + return; + } + int code = (int)(long)result; + switch (code) { + case Http3Streams.CONTROL_STREAM_CODE -> { + controlStream("peer control stream", StreamType.CONTROL); + } + case Http3Streams.QPACK_ENCODER_STREAM_CODE -> { + qpackEncoderStream("peer qpack encoder stream", StreamType.QPACK_ENCODER); + } + case Http3Streams.QPACK_DECODER_STREAM_CODE -> { + qpackDecoderStream("peer qpack decoder stream", StreamType.QPACK_DECODER); + } + default -> { + unknownStreamType(code, stream); + } + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before stream type received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + } + + private void dispatchPushStream(Long result, Throwable error) { + reader.stop(); + if (result != null) { + cf.complete(stream); + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } else { + pushStream("push stream", StreamType.PUSH, result); + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before push stream ID received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + + } + + // dispatches the peer control stream + private void controlStream(String description, StreamType type) { + assert type.code() == Http3Streams.CONTROL_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onControlStreamCreated(description, stream); + } + + // dispatches the peer encoder stream + private void qpackEncoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_ENCODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onEncoderStreamCreated(description, stream); + } + + // dispatches the peer decoder stream + private void qpackDecoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_DECODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onDecoderStreamCreated(description, stream); + } + + // dispatches a push stream initiated by the peer + private void pushStream(String description, StreamType type, long pushId) { + assert type.code() == Http3Streams.PUSH_STREAM_CODE; + debug().log("dispatching %s %s(%s, %s)", description, type, type.code(), pushId); + onPushStreamCreated(description, stream, pushId); + } + + // dispatches a stream whose stream type was recognized as a reserved stream type + private void reservedStreamType(long code, QuicReceiverStream stream) { + onReservedStreamType(code, stream); + } + + // dispatches a stream whose stream type was not recognized + private void unknownStreamType(long code, QuicReceiverStream stream) { + onUnknownStreamType(code, stream); + // if an exception is thrown above, abort will be called. + } + + /** + * {@return the debug logger that should be used} + */ + protected abstract Logger debug(); + + /** + * Starts the dispatcher. + * @apiNote + * The dispatcher should be explicitly started after + * creating the dispatcher. + */ + protected void start() { + reader.readInt().whenComplete(this::dispatch); + } + + /** + * This method disconnects the reader, stops the dispatch, and unless + * the stream type could be decoded and was a {@linkplain Http3Streams#isReserved(long) + * reserved type}, calls {@link #onStreamAbandoned(QuicReceiverStream)} + */ + protected void abandon() { + onStreamAbandoned(stream); + } + + /** + * Aborts the dispatch - for instance, if the stream type + * can't be read, or isn't recognized. + *

      + * This method requests the peer to stop sending this stream, + * and completes the {@link #dispatchCF() dispatchCF} exceptionally + * with the provided throwable. + * + * @param throwable the reason for aborting the dispatch + */ + private void abort(Throwable throwable) { + try { + var debug = debug(); + if (debug.on()) debug.log("aborting dispatch: " + throwable, throwable); + if (!stream.receivingState().isReset() && !stream.isStopSendingRequested()) { + stream.requestStopSending(Http3Error.H3_INTERNAL_ERROR.code()); + } + } finally { + abandon(); + cf.completeExceptionally(throwable); + } + } + + /** + * Called when a reserved stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onReservedStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring reserved stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + } + + /** + * Called when an unrecognized stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * abandon(); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onUnknownStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring unknown stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + abandon(); + } + + /** + * Called after disconnecting to abandon a peer initiated stream. + * @param stream a peer initiated stream which was abandoned due to having an + * unknown type, or which was abandoned due to being reset + * before being dispatched. + * @apiNote + * A subclass may want to override this method in order to, e.g, emit a + * QPack Stream Cancellation instruction; + * See https://www.rfc-editor.org/rfc/rfc9204.html#name-abandonment-of-a-stream + */ + protected void onStreamAbandoned(QuicReceiverStream stream) {} + + /** + * Called after disconnecting to handle the peer control stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer control stream + */ + protected abstract void onControlStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer encoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer encoder stream + */ + protected abstract void onEncoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer decoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer decoder stream + */ + protected abstract void onDecoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle a peer initiated push stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream a peer initiated push stream + */ + protected abstract void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java new file mode 100644 index 00000000000..bff353a648a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * A class that models a pair of unidirectional streams, where + * data to be written is simply submitted to a queue. + */ +public class QueuingStreamPair extends UniStreamPair { + + // a queue of ByteBuffers submitted for writing. + protected final ConcurrentLinkedQueue writerQueue; + + /** + * Creates a new {@code QueuingStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

      + * This class implements a read loop and a write loop. + *

      + * The read loop will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

      + * Data can be written to the stream simply by {@linkplain + * #submitData(ByteBuffer) submitting} it to the + * internal unbounded queue managed by this stream. + * When the stream becomes writable, the write loop is invoked and all + * pending data in the queue is written to the stream, until the stream + * is blocked or the queue is empty. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public QueuingStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + // initialize writer queue before the parent constructor starts the writer loop + writerQueue = new ConcurrentLinkedQueue<>(); + super(streamType, quicConnection, receiver, errorHandler, logger); + } + + /** + * {@return the available credit, taking into account data that has + * not been submitted yet} + * This is only weakly consistent. + */ + public long credit() { + var writer = localWriter(); + long credit = (writer == null) ? 0 : writer.credit(); + if (writerQueue.isEmpty()) return credit; + return credit - writerQueue.stream().mapToLong(Buffer::remaining).sum(); + } + + /** + * Submit data to be written to the sending stream via this + * object's internal queue. + * @param buffer the data to submit + */ + public final void submitData(ByteBuffer buffer) { + writerQueue.offer(buffer); + localWriteScheduler().runOrSchedule(); + } + + // The local control stream write loop + @Override + void localWriterLoop() { + var writer = localWriter(); + if (writer == null) return; + assert !(writer instanceof QueuingWriter); + ByteBuffer buffer; + if (debug.on()) + debug.log("start control writing loop: credit=" + writer.credit()); + while (writer.credit() > 0 && (buffer = writerQueue.poll()) != null) { + try { + if (debug.on()) + debug.log("schedule %s bytes for writing on control stream", buffer.remaining()); + writer.scheduleForWriting(buffer, buffer == QuicStreamReader.EOF); + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to write to control stream", t); + } + errorHandler.onError(writer.stream(), this, t); + } + } + } + + @Override + QuicStreamWriter wrap(QuicStreamWriter writer) { + return new QueuingWriter(writer); + } + + /** + * A class that wraps the actual {@code QuicStreamWriter} + * and redirect everything to the QueuingStreamPair's + * writerQueue - so that data is not sent out of order. + */ + class QueuingWriter extends QuicStreamWriter { + final QuicStreamWriter writer; + QueuingWriter(QuicStreamWriter writer) { + super(QueuingStreamPair.this.localWriteScheduler()); + this.writer = writer; + } + + @Override + public SendingStreamState sendingState() { + return writer.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + if (!last || buffer.hasRemaining()) submitData(buffer); + if (last) submitData(QuicStreamReader.EOF); + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + QueuingStreamPair.this.writerQueue.offer(buffer); + } + + @Override + public long credit() { + return QueuingStreamPair.this.credit(); + } + + @Override + public void reset(long errorCode) throws IOException { + writer.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + return writer.stream(); + } + + @Override + public boolean connected() { + return writer.connected(); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java new file mode 100644 index 00000000000..a9101b48ad9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.http3.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +/** + * A class that reads VL integers from a QUIC stream. + *

      + * After constructing an instance of this class, the application + * is can call {@link #readInt()} to read one VL integer off the stream. + * When the read operation completes, the application can call {@code readInt} + * again, or call {@link #stop()} to disconnect the reader. + */ +final class QuicStreamIntReader { + private final SequentialScheduler scheduler = SequentialScheduler.lockingScheduler(this::dispatch); + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Logger debug; + private CompletableFuture cf; + private ByteBuffer vlongBuf; // accumulate bytes until stream type can be decoded + + /** + * Creates a {@code QuicStreamIntReader} for the given stream. + * @param stream a receiver stream with no connected reader + * @param debug a logger + */ + public QuicStreamIntReader(QuicReceiverStream stream, Logger debug) { + this.stream = stream; + this.reader = stream.connectReader(scheduler); + this.debug = debug; + debug.log("int reader created for stream " + stream.streamId()); + } + + // The read loop. Attempts to read a VL int, and completes the CF when done. + private void dispatch() { + if (cf == null) return; // not reading anything at the moment + try { + ByteBuffer buffer; + while ((buffer = reader.peek()) != null) { + if (buffer == QuicStreamReader.EOF) { + debug.log("stream %s EOF, cannot complete!", + stream.streamId()); + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(-1L); + return; + } + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + continue; + } + if (vlongBuf == null) { + long vlong = VariableLengthEncoder.decode(buffer); + if (vlong >= 0) { + // happy case: we have enough bytes in the buffer + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + } + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } + // we don't have enough bytes: start accumulating them + int vlongSize = VariableLengthEncoder.peekEncodedValueSize(buffer, buffer.position()); + assert vlongSize > 0 && vlongSize <= VariableLengthEncoder.MAX_INTEGER_LENGTH + : vlongSize + " is out of bound for a variable integer size (should be in [1..8]"; + assert buffer.remaining() < vlongSize; + vlongBuf = ByteBuffer.allocate(vlongSize); + vlongBuf.put(buffer); + assert buffer.remaining() == 0; + var polled = reader.poll(); + assert polled == buffer; + // continue and wait for more + } else { + // there wasn't enough bytes the first time around, accumulate + // missing bytes + int missing = vlongBuf.remaining(); + int available = Math.min(missing, buffer.remaining()); + for (int i = 0; i < available; i++) { + vlongBuf.put(buffer.get()); + } + // if we have exhausted the buffer, poll it. + if (!buffer.hasRemaining()) { + var polled = reader.poll(); + assert polled == buffer; + } + // if we have all bytes, we can proceed and decode the stream type + if (!vlongBuf.hasRemaining()) { + vlongBuf.flip(); + long vlong = VariableLengthEncoder.decode(vlongBuf); + assert !vlongBuf.hasRemaining(); + vlongBuf = null; + assert vlong >= 0; + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } // otherwise, wait for more + } + } + } catch (Throwable throwable) { + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.completeExceptionally(throwable); + } + } + + /** + * Stops and disconnects this reader. This operation must not be done when a read operation + * is in progress. If cancelling a read operation is intended, use + * {@link QuicReceiverStream#requestStopSending(long)}. + * @throws IllegalStateException if a read operation is currently in progress. + */ + public synchronized void stop() { + if (cf != null) { + // if a read is in progress, some bytes might have been read + // off the stream already, and stopping the reader could corrupt the data. + throw new IllegalStateException("Reading in progress"); + } + if (!reader.connected()) return; + stream.disconnectReader(reader); + scheduler.stop(); + } + + /** + * Starts a read operation to decode a single number. + * @return a {@link CompletableFuture} that will be completed + * with the decoded number, or -1 if the stream is terminated before + * the complete number could be read, or an exception + * if the stream is reset or decoding fails. + * @throws IllegalStateException if the reader is stopped, or if a read + * operation is already in progress + */ + public synchronized CompletableFuture readInt() { + if (cf != null) { + throw new IllegalStateException("Read in progress"); + } + if (!reader.connected()) { + throw new IllegalStateException("Reader stopped"); + } + var cf0 = cf = new MinimalFuture<>(); + reader.start(); + scheduler.runOrSchedule(); + return cf0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java new file mode 100644 index 00000000000..403b26f244d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2022, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import jdk.internal.net.http.Http3Connection; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; + +/** + * A class that models a pair of HTTP/3 unidirectional streams. + * This class implements a read loop that calls a {@link + * #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable, StreamErrorHandler, Logger) + * receiver} every time a {@code ByteBuffer} is read from + * the receiver part. + * The {@linkplain #futureSenderStreamWriter() sender stream writer}, + * when available, can be used to write to the sender part. + * The {@link #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable,StreamErrorHandler, Logger) + * writerLoop} is invoked whenever the writer part becomes unblocked, and + * writing can be resumed. + *

      + * @apiNote + * The creator of the stream pair (typically {@link Http3Connection}) is expected + * to complete the {@link #futureReceiverStream()} completable future when the remote + * part of the stream pair is created by the remote peer. This class will not + * listen directly for creation of new remote streams. + *

      + * The {@link QueuingStreamPair} class is a subclass of this class which + * implements a writer loop over an unbounded queue of {@code ByteBuffer}, and + * can be used when unlimited buffering of data for writing is not an issue. + */ +public class UniStreamPair { + + // The sequential scheduler for the local control stream (LCS) writer loop + private final SequentialScheduler localWriteScheduler; + // The QuicStreamWriter for the local control stream + private volatile QuicStreamWriter localWriter; + private final CompletableFuture streamWriterCF; + // A completable future that will be completed when the local sender + // stream is opened and the stream type has been queued to the + // writer queue. + private volatile CompletableFuture localSenderStreamCF; + + // The sequential scheduler for the peer receiver stream (PRS) reader loop + final SequentialScheduler peerReadScheduler = + SequentialScheduler.lockingScheduler(this::peerReaderLoop); + // The QuicStreamReader for the peer control stream + volatile QuicStreamReader peerReader; + private final CompletableFuture streamReaderCF; + // A completable future that will be completed when the peer opens + // the receiver part of the stream pair + private final CompletableFuture peerReceiverStreamCF = new MinimalFuture<>(); + private final ReentrantLock lock = new ReentrantLock(); + + + private final StreamType localStreamType; // The HTTP/3 stream type of the sender part + private final StreamType remoteStreamType; // The HTTP/3 stream type of the receiver part + private final QuicConnection quicConnection; // the underlying quic connection + private final Consumer receiver; // called when a ByteBuffer is received + final StreamErrorHandler errorHandler; // used by QueuingStreamPair + final Logger debug; // the debug logger + + /** + * Creates a new {@code UniStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

      + * This class implements a read loop that will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

      + * Writing to the sender part can be done by interacting directly with + * the writer. If the writer is blocked due to flow control, and becomes + * unblocked again, the {@code writeLoop} is invoked. + * The {@link QueuingStreamPair} subclass provides a convenient implementation + * of a {@code writeLoop} based on an unbounded queue of {@code ByteBuffer}. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param writerLoop the writer loop + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + Runnable writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.of(writerLoop), + Objects.requireNonNull(errorHandler), + Objects.requireNonNull(logger)); + } + + /** + * A constructor used by the {@link QueuingStreamPair} subclass + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case + * of read or write errors + * @param logger + */ + UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.empty(), + errorHandler, + Objects.requireNonNull(logger)); + } + + // all constructors delegate here + private UniStreamPair(StreamType localStreamType, + StreamType remoteStreamType, + QuicConnection quicConnection, + Consumer receiver, + Optional writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + assert this.getClass() != UniStreamPair.class + || writerLoop.isPresent(); + this.debug = logger; + this.localStreamType = localStreamType; + this.remoteStreamType = remoteStreamType; + this.quicConnection = quicConnection; + this.receiver = receiver; + this.errorHandler = errorHandler; + var localWriterLoop = writerLoop.orElse(this::localWriterLoop); + this.localWriteScheduler = + SequentialScheduler.lockingScheduler(localWriterLoop); + this.streamWriterCF = startSending(); + this.streamReaderCF = startReceiving(); + } + + private static StreamType local(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> localStreamType; + case QPACK_DECODER -> localStreamType; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + private static StreamType remote(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> StreamType.QPACK_DECODER; + case QPACK_DECODER -> StreamType.QPACK_ENCODER; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + /** + * {@return the HTTP/3 stream type of the sender part of the stream pair} + */ + public final StreamType localStreamType() { + return localStreamType; + } + + /** + * {@return the HTTP/3 stream type of the receiver part of the stream pair} + */ + public final StreamType remoteStreamType() { + return remoteStreamType; + } + + /** + * {@return a completable future that will be completed with a writer connected + * to the sender part of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public final CompletableFuture futureSenderStreamWriter() { + return streamWriterCF; + } + + /** + * {@return a completable future that will be completed with a reader connected + * to the receiver part of this stream pair after the remote HTTP/3 stream + * type has been read off the remote initiated stream} + */ + public final CompletableFuture futureReceiverStreamReader() { + return streamReaderCF; + } + + /** + * {@return a completable future that will be completed with the sender part + * of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public CompletableFuture futureSenderStream() { + return localSenderStream(); + } + + /** + * {@return a completable future that will be completed with the receiver part + * of this stream pair after the remote HTTP/3 stream type has been read off + * the remote initiated stream} + */ + public CompletableFuture futureReceiverStream() { + return peerReceiverStreamCF; + } + + /** + * {@return the scheduler for the local writer loop} + */ + public SequentialScheduler localWriteScheduler() { + return localWriteScheduler; + } + + /** + * {@return the writer connected to the sender part of this stream or + * {@code null} if no writer is connected yet} + */ + public QuicStreamWriter localWriter() {return localWriter; } + + /** + * Stops schedulers. Can be called when the connection is + * closed to stop the reading and writing loops. + */ + public void stopSchedulers() { + peerReadScheduler.stop(); + localWriteScheduler.stop(); + } + + // Hooks for QueuingStreamPair + // ============================ + + /** + * This method is overridden by {@link QueuingStreamPair} to implement + * a writer loop for this stream. It is only called when the concrete + * subclass is {@link QueuingStreamPair}. + */ + void localWriterLoop() { + if (debug.on()) debug.log("writing loop not implemented"); + } + + + /** + * Used by subclasses to redirect queuing of data to the + * subclass queue. + * @param writer the downstream writer + * @return a writer that can be safely used. + */ + QuicStreamWriter wrap(QuicStreamWriter writer) { + return writer; + } + + // Undidirectional Stream Pair Implementation + // ========================================== + + + /** + * This method is called to process bytes received on the peer + * control stream. + * @param buffer the bytes received + */ + private void processPeerControlBytes(ByteBuffer buffer) { + receiver.accept(buffer); + } + + /** + * Creates the local sender stream and queues the stream + * type code in its writer queue. + * @return a completable future that will be completed with the + * local sender stream + */ + private CompletableFuture localSenderStream() { + CompletableFuture lcs = localSenderStreamCF; + if (lcs != null) return lcs; + StreamType type = localStreamType(); + lock.lock(); + try { + if ((lcs = localSenderStreamCF) != null) return lcs; + if (debug.on()) { + debug.log("Opening local stream: %s(%s)", + type, type.code()); + } + // TODO: review this duration + final Duration streamLimitIncreaseDuration = Duration.ZERO; + localSenderStreamCF = lcs = quicConnection + .openNewLocalUniStream(streamLimitIncreaseDuration) + .thenApply( s -> openLocalStream(s, type.code())); + // TODO: use thenApplyAsync with the executor instead + } finally { + lock.unlock(); + } + return lcs; + } + + + /** + * Schedules sending of client settings. + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private CompletableFuture startSending() { + return localSenderStream().thenApply((stream) -> { + if (debug.on()) { + debug.log("stream %s is ready for sending", stream.streamId()); + } + var controlWriter = stream.connectWriter(localWriteScheduler); + localWriter = controlWriter; + localWriteScheduler.runOrSchedule(); + return wrap(controlWriter); + }); + } + + /** + * Schedules the receiving of server settings + * @return a completable future that will be completed with the + * {@link QuicStreamReader} allowing to read from the remote control + * stream. + */ + private CompletableFuture startReceiving() { + if (debug.on()) { + debug.log("prepare to receive"); + } + return peerReceiverStreamCF.thenApply(this::connectReceiverStream); + } + + /** + * Connects the peer control stream reader and + * schedules the receiving of the peer settings from the given + * {@code peerControlStream}. + * @param peerControlStream the peer control stream + * @return the peer control stream reader + */ + private QuicStreamReader connectReceiverStream(QuicReceiverStream peerControlStream) { + var reader = peerControlStream.connectReader(peerReadScheduler); + var streamType = remoteStreamType(); + if (debug.on()) { + debug.log("peer %s stream reader connected (stream %s)", + streamType, peerControlStream.streamId()); + } + peerReader = reader; + reader.start(); + return reader; + } + + // The peer receiver stream reader loop + private void peerReaderLoop() { + var reader = peerReader; + if (reader == null) return; + ByteBuffer buffer; + long bytes = 0; + var streamType = remoteStreamType(); + try { + // TODO: Revisit: if the underlying quic connection is closed + // by the peer, we might get a ClosedChannelException from poll() + // here before the upper layer connection (HTTP/3 connection) is + // marked closed. + if (debug.on()) { + debug.log("start reading from peer %s stream", streamType); + } + while ((buffer = reader.poll()) != null) { + final int remaining = buffer.remaining(); + if (remaining == 0 && buffer != QuicStreamReader.EOF) { + continue; // not yet EOF, so poll more + } + bytes += remaining; + processPeerControlBytes(buffer); + if (buffer == QuicStreamReader.EOF) { + // a EOF was processed, don't poll anymore + break; + } + } + if (debug.on()) { + debug.log("stop reading peer %s stream after %s bytes", + streamType, bytes); + } + } catch (IOException | RuntimeException | Error throwable) { + if (debug.on()) { + debug.log("Reading peer %s stream failed: %s", streamType, throwable); + } + // call the error handler and pass it the stream on which the error happened + errorHandler.onError(reader.stream(), this, throwable); + } + } + + /** + * Queues the given HTTP/3 stream type code on the given local unidirectional + * stream writer queue. + * @param stream a new local unidirectional stream + * @param code the code to queue up on the stream writer queue + * @return the given {@code stream} + */ + private QuicSenderStream openLocalStream(QuicSenderStream stream, int code) { + var streamType = localStreamType(); + if (debug.on()) { + debug.log("Opening local stream: %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = stream.connectWriter(scheduler); + try { + if (debug.on()) { + debug.log("Writing local stream type: stream %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var buffer = ByteBuffer.allocate(VariableLengthEncoder.getEncodedSize(code)); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + writer.queueForWriting(buffer); + scheduler.stop(); + stream.disconnectWriter(writer); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to create stream %s %s(code=%s): %s", + stream.streamId(), streamType, code, t); + } + try { + switch (streamType) { + case CONTROL, QPACK_ENCODER, QPACK_DECODER -> { + final String logMsg = "stream %s %s(code=%s)" + .formatted(stream.streamId(), streamType, code); + // TODO: revisit - we should probably invoke a method + // on the HttpQuicConnection or H3Connection instead of + // dealing directly with QuicConnection here. + final TerminationCause terminationCause = + appLayerClose(H3_STREAM_CREATION_ERROR.code()).loggedAs(logMsg); + quicConnection.connectionTerminator().terminate(terminationCause); + } + default -> writer.reset(H3_STREAM_CREATION_ERROR.code()); + } + } catch (Throwable suppressed) { + if (debug.on()) { + debug.log("couldn't close connection or reset stream: " + suppressed); + } + Utils.addSuppressed(t, suppressed); + throw new CompletionException(t); + } + } + return stream; + } + + public static interface StreamErrorHandler { + + /** + * Will be invoked when there is an error on a {@code QuicStream} handled by + * the {@code UniStreamPair} + * + * @param stream the stream on which the error occurred + * @param uniStreamPair the UniStreamPair to which the stream belongs + * @param error the error that occurred + */ + void onError(QuicStream stream, UniStreamPair uniStreamPair, Throwable error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java new file mode 100644 index 00000000000..8487100c8ca --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.DecoderInstructionsWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Decodes headers from their binary representation. + * + *

      Typical lifecycle looks like this: + * + *

      {@link #Decoder(StreamPairSupplier, QPACKErrorHandler) new Decoder} + * ({@link #configure(ConnectionSettings)} called once from our HTTP/3 settings + * {@link #decodeHeader(ByteBuffer, boolean, HeaderFrameReader) decodeHeader} + * + *

      {@code Decoder} does not require a complete header block in a single + * {@code ByteBuffer}. The header block can be spread across many buffers of any + * size and decoded one-by-one the way it makes most sense for the user. This + * way also allows not to limit the size of the header block. + * + *

      Headers are delivered to the {@linkplain DecodingCallback callback} as + * soon as they become decoded. Using the callback also gives the user freedom + * to decide how headers are processed. The callback does not limit the number + * of headers decoded during single decoding operation. + */ + +public final class Decoder { + + private final QPACK.Logger logger; + private final DynamicTable dynamicTable; + private final EncoderInstructionsReader encoderInstructionsReader; + private final QueuingStreamPair decoderStreamPair; + private static final AtomicLong DECODERS_IDS = new AtomicLong(); + // ID of last acknowledged entry acked by Insert Count Increment + // or section acknowledgement instruction + private long acknowledgedInsertsCount; + private final ReentrantLock ackInsertCountLock = new ReentrantLock(); + private final AtomicLong blockedStreamsCounter = new AtomicLong(); + private volatile long maxBlockedStreams; + private final QPACKErrorHandler qpackErrorHandler; + private volatile long maxFieldSectionSize = DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; + private final AtomicLong concurrentDynamicTableInsertions = + new AtomicLong(); + private static final long MAX_LITERAL_WITH_INDEXING = + Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing", 512); + + /** + * Constructs a {@code Decoder} with zero initial capacity of the dynamic table. + * + *

      Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

      Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

      An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @see Decoder#configure(ConnectionSettings) + */ + public Decoder(StreamPairSupplier streams, QPACKErrorHandler errorHandler) { + long id = DECODERS_IDS.incrementAndGet(); + logger = QPACK.getLogger().subLogger("Decoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New decoder"); + } + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), false); + decoderStreamPair = streams.create(this::processEncoderInstruction); + qpackErrorHandler = errorHandler; + encoderInstructionsReader = new EncoderInstructionsReader(new DecoderTableCallback(), logger); + } + + public QueuingStreamPair decoderStreams() { + return decoderStreamPair; + } + + /** + * {@return a new {@link HeaderFrameReader} that will hold the decoding + * state for a new request/response stream} + */ + public HeaderFrameReader newHeaderFrameReader(DecodingCallback decodingCallback) { + return new HeaderFrameReader(dynamicTable, decodingCallback, + blockedStreamsCounter, maxBlockedStreams, + maxFieldSectionSize, logger); + } + + public void ackTableInsertions() { + ackInsertCountLock.lock(); + try { + long insertCount = dynamicTable.insertCount(); + assert acknowledgedInsertsCount <= insertCount; + long incrementValue = insertCount - acknowledgedInsertsCount; + if (incrementValue > 0) { + // Write "Insert Count Increment" to the decoder stream + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instructionSize = decoderInstructionsWriter.configureForInsertCountInc(incrementValue); + submitDecoderInstruction(decoderInstructionsWriter, instructionSize); + } + // Update lastAck value + acknowledgedInsertsCount = insertCount; + } finally { + ackInsertCountLock.unlock(); + } + } + + /** + * Submit "Section Acknowledgment" instruction to the decoder stream. + * A field line section needs to be acknowledged after completion of + * section decoding. + * @param streamId stream ID associated with the field section's + * @param headerFrameReader header frame reader used to read + * the field line section + */ + public void ackSection(long streamId, HeaderFrameReader headerFrameReader) { + + FieldSectionPrefix prefix = headerFrameReader.decodedSectionPrefix(); + + // 4.4.1. Section Acknowledgment: If an encoder receives a Section Acknowledgment instruction + // referring to a stream on which every encoded field section with a non-zero Required Insert + // Count has already been acknowledged, this MUST be treated as a connection error of type + // QPACK_DECODER_STREAM_ERROR. + long prefixInsertCount = prefix.requiredInsertCount(); + if (prefixInsertCount == 0) return; + ackInsertCountLock.lock(); + try { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForSectionAck(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + if (prefixInsertCount > acknowledgedInsertsCount) { + acknowledgedInsertsCount = prefixInsertCount; + } + } finally { + ackInsertCountLock.unlock(); + } + } + + public void cancelStream(long streamId) { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForStreamCancel(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + dynamicTable.cleanupStreamInsertCountNotifications(streamId); + } + + /** + * Configures maximum capacity of the decoder's dynamic table based on connection settings of + * the HTTP client, also configures the number of allowed blocked streams. + * The decoder's dynamic table capacity can only be changed via + * {@linkplain EncoderInstructionsReader.Callback encoder instructions callback}. + * + * @param ourSettings connection settings + */ + public void configure(ConnectionSettings ourSettings) { + long maxCapacity = ourSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + maxBlockedStreams = ourSettings.qpackBlockedStreams(); + long maxFieldSS = ourSettings.maxFieldSectionSize(); + if (maxFieldSS > 0) { + maxFieldSectionSize = maxFieldSS; + } else { + // Unlimited field section size + maxFieldSectionSize = -1L; + } + } + + /** + * Decodes a header block from the given buffer to the given callback. + * + *

      Suppose a header block is represented by a sequence of + * {@code ByteBuffer}s in the form of {@code Iterator}. And the + * consumer of decoded headers is represented by {@linkplain DecodingCallback the callback} + * registered within the provided {@code headerFrameReader}. + * Then to decode the header block, the following approach might be used: + * {@snippet : + * HeaderFrameReader headerFrameReader = + * newHeaderFrameReader(decodingCallback); + * while (buffers.hasNext()) { + * ByteBuffer input = buffers.next(); + * decoder.decodeHeader(input, !buffers.hasNext(), headerFrameReader); + * } + * } + * + *

      The decoder reads as much as possible of the header block from the + * given buffer, starting at the buffer's position, and increments its + * position to reflect the bytes read. The buffer's mark and limit will not + * be modified. + * + *

      Once the method is invoked with {@code endOfHeaderBlock == true}, the + * current header block is deemed ended, and inconsistencies, if any, are + * reported immediately via a callback registered within the {@code + * headerFrameReader} instance. + * + *

      Each callback method is called only after the implementation has + * processed the corresponding bytes. If the bytes revealed a decoding + * error it is reported via a callback registered within the {@code + * headerFrameReader} instance. + * + * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of + * returning it for two reasons. The first one is that the user of the + * decoder always knows which chunk is the last. The second one is to throw + * the most detailed exception possible, which might be useful for + * diagnosing issues. + * + * @implNote This implementation is not atomic in respect to decoding + * errors. In other words, if the decoding operation has thrown a decoding + * error, the decoder is no longer usable. + * + * @param headerBlock + * the chunk of the header block, may be empty + * @param endOfHeaderBlock + * true if the chunk is the final (or the only one) in the sequence + * @param headerFrameReader the stateful header frame reader + * @throws NullPointerException + * if either {@code headerBlock} or {@code headerFrameReader} are null + */ + public void decodeHeader(ByteBuffer headerBlock, boolean endOfHeaderBlock, + HeaderFrameReader headerFrameReader) { + requireNonNull(headerFrameReader, "headerFrameReader"); + headerFrameReader.read(headerBlock, endOfHeaderBlock); + } + + /** + * This method is invoked when the {@linkplain + * UniStreamPair#futureReceiverStreamReader() decoder's stream reader} + * has data available for reading. + */ + private void processEncoderInstruction(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + decoderStreamPair.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + int stringLengthLimit = Math.clamp(dynamicTable.capacity() - ENTRY_SIZE, + 0, Integer.MAX_VALUE - (int) ENTRY_SIZE); + encoderInstructionsReader.read(buffer, stringLengthLimit); + } catch (QPackException qPackException) { + qpackErrorHandler.closeOnError(qPackException.getCause(), qPackException.http3Error()); + } + } + + private void submitDecoderInstruction(DecoderInstructionsWriter decoderInstructionsWriter, + int size) { + if (size > decoderStreamPair.credit()) { + qpackErrorHandler.closeOnError( + new IOException("QPACK not enough credit on a decoder stream " + + decoderStreamPair.remoteStreamType()), H3_CLOSED_CRITICAL_STREAM); + return; + } + // All decoder instructions contain only one variable length integer. + // Which could take up to 9 bytes max. + ByteBuffer buffer = ByteBuffer.allocate(size); + boolean done = decoderInstructionsWriter.write(buffer); + // Assert that instruction is fully written, ie the correct + // instruction size estimation was supplied. + assert done; + buffer.flip(); + decoderStreamPair.submitData(buffer); + } + + void incrementAndCheckDynamicTableInsertsCount() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + long concurrentNumberOfInserts = concurrentDynamicTableInsertions.incrementAndGet(); + if (concurrentNumberOfInserts > MAX_LITERAL_WITH_INDEXING) { + String exceptionMessage = "Too many literal with indexing: %s > %s" + .formatted(concurrentNumberOfInserts, MAX_LITERAL_WITH_INDEXING); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> exceptionMessage); + } + throw QPackException.encoderStreamError(new ProtocolException(exceptionMessage)); + } + } + } + + public void resetInsertionsCounter() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + concurrentDynamicTableInsertions.set(0); + } + } + + private class DecoderTableCallback implements EncoderInstructionsReader.Callback { + + private void ensureInstructionsAllowed() { + // RFC9204 3.2.3. Maximum Dynamic Table Capacity: + // "When the maximum table capacity is zero, the encoder MUST NOT + // insert entries into the dynamic table and MUST NOT send any encoder + // instructions on the encoder stream." + if (dynamicTable.maxCapacity() == 0) { + throw new IllegalStateException("Unexpected encoder instruction"); + } + } + + @Override + public void onCapacityUpdate(long capacity) { + if (capacity == 0 && dynamicTable.maxCapacity() == 0) { + return; + } + ensureInstructionsAllowed(); + dynamicTable.setCapacity(capacity); + } + + @Override + public void onInsert(String name, String value) { + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(name, value) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough evictable space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this MUST be + // treated as a stream error of type QPACK_DECOMPRESSION_FAILED if on a request stream or + // a connection error of the appropriate type if on the encoder or decoder stream." + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(nameIndex, indexInStaticTable, valueString) != + DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onDuplicate(long l) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this + // MUST be treated as a stream error of type QPACK_DECOMPRESSION_FAILED" + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, + () -> format("Processing duplicate instruction (%d)", l)); + } + if (dynamicTable.duplicate(l) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to duplicate entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java new file mode 100644 index 00000000000..bb9dbdadf59 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; + +import java.nio.ByteBuffer; + +/** + * Delivers results of the {@link Decoder#decodeHeader(ByteBuffer, boolean, HeaderFrameReader)} + * decoding operation. + * + *

      Methods of the callback are never called by a decoder with any of the + * arguments being {@code null}. + * + * @apiNote + * + *

      The callback provides methods for all possible + * + * field line representations. + * + *

      Names and values are {@link CharSequence}s rather than {@link String}s in + * order to allow users to decide whether they need to create objects. A + * {@code CharSequence} might be used in-place, for example, to be appended to + * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. + * + *

      That said, if a passed {@code CharSequence} needs to outlast the method + * call, it needs to be copied. + * + */ +public interface DecodingCallback { + + /** + * A method the more specific methods of the callback forward their calls + * to. + * + * @param name + * header name + * @param value + * header value + */ + void onDecoded(CharSequence name, CharSequence value); + + /** + * A header fields decoding is completed. + */ + void onComplete(); + + /** + * A connection-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + void onConnectionError(Throwable throwable, Http3Error http3Error); + + /** + * A stream-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + default void onStreamError(Throwable throwable, Http3Error http3Error) { + onConnectionError(throwable, http3Error); + } + + /** + * Reports if {@linkplain #onConnectionError(Throwable, Http3Error) a connection} + * or {@linkplain #onStreamError(Throwable, Http3Error) a stream} error has been + * observed during the decoding process + * @return true - if error was observed; false - otherwise + */ + default boolean hasError() { + return false; + } + + /** + * Returns request/response stream id or push stream id associated with a decoding callback. + */ + long streamId(); + + /** + * A more finer-grained version of {@link #onDecoded(CharSequence, + * CharSequence)} that also reports on value sensitivity. + * + *

      Value sensitivity must be considered, for example, when implementing + * an intermediary. A {@code value} is sensitive if it was represented as Literal Header + * Field Never Indexed. + * + * @implSpec + * + *

      The default implementation invokes {@code onDecoded(name, value)}. + * + * @param name + * header name + * @param value + * header value + * @param sensitive + * whether the value is sensitive + */ + default void onDecoded(CharSequence name, + CharSequence value, + boolean sensitive) { + onDecoded(name, value); + } + + /** + * An Indexed + * Field Line decoded. + * + * @implSpec + * + *

      The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of a name/value pair in static or dynamic table + * @param name + * header name + * @param value + * header value + */ + default void onIndexed(long index, CharSequence name, CharSequence value) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Field Line with Name Reference decoded, where a {@code name} was + * referred by an {@code index}. + * + * @implSpec + * + *

      The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + * @param hideIntermediary + * if the header field should be written to intermediary nodes + */ + default void onLiteralWithNameReference(long index, + CharSequence name, + CharSequence value, + boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } + + /** + * A Literal Field + * Line with Literal Name decoded, where both a {@code name} and a {@code value} + * were literal. + * + * @implSpec + * + *

      The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithLiteralName(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java new file mode 100644 index 00000000000..6f5e567e182 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java @@ -0,0 +1,1069 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.Encoder.EncodingContext; +import jdk.internal.net.http.qpack.Encoder.SectionReference; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; + +/* + * The dynamic table to store header fields. Implements dynamic table described + * in "QPACK: Header Compression for HTTP/3" RFC. + * The size of the table is the sum of the sizes of its entries. + */ +public final class DynamicTable implements HeadersTable { + + // QPACK Section 3.2.1: + // The size of an entry is the sum of its name's length in bytes, + // its value's length in bytes, and 32 additional bytes. + public static final long ENTRY_SIZE = 32L; + + // Initial length of the elements array + // It is required for this value to be a power of 2 integer + private static final int INITIAL_HOLDER_ARRAY_LENGTH = 64; + + final Logger logger; + + // Capacity (Maximum size) in bytes (or capacity in RFC 9204) of the dynamic table + private long capacity; + + // RFC-9204: 3.2.3. Maximum Dynamic Table Capacity + private long maxCapacity; + + // Max entries is required to implement encoding of Required Insert Count + // in Field Lines Prefix: + // RFC-9204: 4.5.1.1. Required Insert Count: + // "This encoding limits the length of the prefix on long-lived connections." + private long maxEntries; + + // Size of the dynamic table in bytes - calculated as the sum of the sizes of its entries. + private long size; + + // Table elements holder and its state variables + // Absolute ID of tail and head elements. + // tail id - is an id of the oldest element in the table + // head id - is an id of the next element that will be added to the table. + // head element id is head - 1. + // drain id - is the lowest element id that encoder can reference + private long tail, head, drain = -1; + + // Used space percentage threshold when to start increasing the drain index + private final int drainUsedSpaceThreshold = QPACK.ENCODER_DRAINING_THRESHOLD; + + // true - table is used by the QPack encoder, otherwise used by the + // QPack decoder + private final boolean encoderTable; + + // Array that holds dynamic table entries + private HeaderField[] elements; + + // name -> (value -> [index]) + private final Map>> indicesMap; + + private record TableInsertCountNotification(long streamId, long minimumRIC, + CompletableFuture completion) { + public boolean isStreamId(long streamId) { + return this.streamId == streamId; + } + public boolean isFulfilled(long insertionCount) { + return insertionCount >= minimumRIC; + } + } + + private final Queue insertCountNotifications = + new PriorityQueue<>( + Comparator.comparingLong(TableInsertCountNotification::minimumRIC) + ); + + public CompletableFuture awaitFutureInsertCount(long streamId, + long valueToAwait) { + if (encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var completion = new CompletableFuture(); + long insertCount = insertCount(); + if (insertCount >= valueToAwait) { + completion.complete(null); + } else { + insertCountNotifications + .add(new TableInsertCountNotification( + streamId, valueToAwait, completion)); + } + return completion; + } finally { + writeLock.unlock(); + } + } + + private void notifyInsertCountChange() { + assert lock.isWriteLockedByCurrentThread(); + if (insertCountNotifications.isEmpty()) { + return; + } + long insertCount = insertCount(); + Predicate isFulfilled = + icn -> icn.isFulfilled(insertCount); + insertCountNotifications.removeIf(icn -> completeIf(isFulfilled, icn)); + } + + public boolean cleanupStreamInsertCountNotifications(long streamId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + Predicate isSameStreamId = + icn -> icn.isStreamId(streamId); + return insertCountNotifications.removeIf(icn -> completeIf(isSameStreamId, icn)); + } finally { + writeLock.unlock(); + } + } + + private static boolean completeIf(Predicate predicate, + TableInsertCountNotification insertCountNotification) { + if (predicate.test(insertCountNotification)) { + insertCountNotification.completion.complete(null); + return true; + } + return false; + } + + // Read-Write lock to manage access to table entries + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + public DynamicTable(Logger logger) { + this(logger, true); + } + + public DynamicTable(Logger logger, boolean encoderTable) { + this.logger = logger; + this.encoderTable = encoderTable; + elements = new HeaderField[INITIAL_HOLDER_ARRAY_LENGTH]; + indicesMap = new HashMap<>(); + // -1 signifies that max table capacity was not yet initialized + maxCapacity = -1L; + maxEntries = 0L; + } + + /** + * Returns size of the dynamic table in bytes + * @return size of the dynamic table + */ + public long size() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return size; + } finally { + readLock.unlock(); + } + } + + /** + * Returns current capacity of the dynamic table + * @return current capacity + */ + public long capacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return capacity; + } finally { + readLock.unlock(); + } + } + + /** + * Returns a maximum capacity in bytes of the dynamic table. + * @return maximum capacity + */ + public long maxCapacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxCapacity; + } finally { + readLock.unlock(); + } + } + + /** + * Sets a maximum capacity in bytes of the dynamic table. + * + *

      The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 3.2.3 Maximum Dynamic Table Capacity). + * + *

      May be called only once to set maximum dynamic table capacity. + *

      This method doesn't change the actual capacity of the dynamic table. + * + * @see #setCapacity(long) + * @param maxCapacity a non-negative long + * @throws IllegalArgumentException if max capacity is negative + * @throws IllegalStateException if max capacity was already set + */ + public void setMaxTableCapacity(long maxCapacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (maxCapacity < 0) { + throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity); + } + if (this.maxCapacity != -1L) { + // Max table capacity is initialized from SETTINGS frame which can be only received once: + // "If an endpoint receives a second SETTINGS frame on the control stream, + // the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED" + // [RFC 9114 https://www.rfc-editor.org/rfc/rfc9114.html#name-settings] + throw new IllegalStateException("Max Table Capacity can only be set once"); + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("setting maximum allowed dynamic table capacity to %s", + maxCapacity)); + } + this.maxCapacity = maxCapacity; + this.maxEntries = maxCapacity / ENTRY_SIZE; + } finally { + writeLock.unlock(); + } + } + + /** + * Returns maximum possible number of entries that could be stored in the dynamic table + * with respect to MAX_CAPACITY setting. + * @return max entries + */ + public long maxEntries() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxEntries; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its absolute index. Entry referenced by an absolute + * index does not depend on the state of the dynamic table. + * @param uniqueID an entry unique index + * @return retrieved header field + * @throws IllegalArgumentException if entry is not received yet, + * already evicted or invalid entry index is specified. + */ + @Override + public HeaderField get(long uniqueID) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (uniqueID < 0) { + throw new IllegalArgumentException("Entry index invalid"); + } + // Not yet received entry + if (uniqueID >= head) { + throw new IllegalArgumentException("Entry not received yet"); + } + // Already evicted entry + if (uniqueID < tail) { + throw new IllegalArgumentException("Entry already evicted"); + } + return elements[(int) (uniqueID & (elements.length - 1))]; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its relative index. Entry referenced by a relative index depends + * on the state of the dynamic table. + * @param relativeId index relative to the most recently inserted entry + * @return retrieved header field + */ + public HeaderField getRelative(long relativeId) { + // RFC 9204: 3.2.5. Relative Indexing + // "Relative indices begin at zero and increase in the opposite direction from the absolute index. + // Determining which entry has a relative index of 0 depends on the context of the reference. + // In encoder instructions (Section 4.3), a relative index of 0 refers to the most recently inserted + // value in the dynamic table." + var readLock = lock.readLock(); + readLock.lock(); + try { + return get(insertCount() - 1 - relativeId); + } finally { + readLock.unlock(); + } + } + + /** + * Converts absolute entry index to relative index that can be used + * in the encoder instructions. + * Relative index of 0 refers to the most recently inserted entry. + * + * @param absoluteId absolute index of an entry + * @return relative entry index + */ + public long toRelative(long absoluteId) { + var readLock = lock.readLock(); + readLock.lock(); + try { + assert absoluteId < head; + return head - 1 - absoluteId; + } finally { + readLock.unlock(); + } + } + + /** + * Search an absolute id of a name:value pair in the dynamic table. + * @param name a name to search for + * @param value a value to search for + * @return positive index if name:value match found, + * negative index if only name match found, + * 0 if no match found + */ + @Override + public long search(String name, String value) { + // This method is only designated for encoder use + if (!encoderTable) { + return 0; + } + var readLock = lock.readLock(); + readLock.lock(); + try { + Map> values = indicesMap.get(name); + if (values == null) { + return 0; + } + Deque indexes = values.get(value); + if (indexes != null) { + // "+1" since the index range [0..id] is mapped to [1..id+1] + return indexes.peekLast() + 1; + } else { + assert !values.isEmpty(); + Long any = values.values().iterator().next().peekLast(); // Iterator allocation + // Use last entry in found values with matching name, and use its index for + // encoding with name reference. + // Negation and "-1" since name-only matches are mapped from [0..id] to + // [-1..-id-1] region + return -any - 1; + } + } finally { + readLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param name header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + @Override + public long insert(String name, String value) { + // Invoking toString() will possibly allocate Strings. But that's + // unavoidable at this stage. If a CharSequence is going to be stored in + // the table, it must not be mutable (e.g. for the sake of hashing). + return insert(new HeaderField(name, value), SectionReference.noReferences()); + } + + + /** + * Add entry to the dynamic table with name specified as index in static + * or dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param nameIndex index of the header name to add + * @param isStaticIndex if name index references static table header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(long nameIndex, boolean isStaticIndex, String value) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Inserting with name index (nameIndex='%s' isStaticIndex='%s' value=%s)", + nameIndex, isStaticIndex, value)); + } + String name = isStaticIndex ? + StaticTable.HTTP3.get(nameIndex).name() : + getRelative(nameIndex).name(); + return insert(name, value); + } finally { + writeLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * The supplied unacknowledged section references are checked by this method to check + * if entries are evictable. + * Such checks are performed when there is not enough space in the dynamic table to insert + * the requested header. + * This method is intended to be used by the encoder only. + * + * @param name header name + * @param value header value + * @param sectionReference unacknowledged section references + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(String name, String value, SectionReference sectionReference) { + return insert(new HeaderField(name, value), sectionReference); + } + + /** + * Inserts an entry to the dynamic table and sends encoder insert instruction bytes + * to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * If an entry with matching name:value is available, its index is returned + * and no insert instruction is generated on encoder stream. If duplicate entry is required + * due to entry being non-referencable then {@link DynamicTable#duplicateWithEncoderStreamUpdate( + * EncoderInstructionsWriter, long, QueuingStreamPair, EncodingContext)} is used. + * + * @param entry table entry to add + * @param writer non-configured encoder instruction writer for generating encoder + * instruction + * @param encoderStreams encoder stream pair + * @param encodingContext encoder encoding context + * @return absolute id of inserted entry OR already available entry, -1L if entry cannot + * be added + */ + public long insertWithEncoderStreamUpdate(TableEntry entry, + EncoderInstructionsWriter writer, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + String name = entry.name().toString(); + String value = entry.value().toString(); + // Entry with name only match in dynamic table + boolean nameOnlyDynamicEntry = !entry.isStaticTable() && entry.type() == NAME; + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // First, check if entry is in the table already - + // no need to add a new one. + long index = search(name, value); + if (index > 0) { + long absIndex = index - 1; + // Check if found entry can be referenced, + // if not issue duplicate instruction + if (!canReferenceEntry(absIndex)) { + return duplicateWithEncoderStreamUpdate(writer, + absIndex, encoderStreams, encodingContext); + } + return absIndex; + } + SectionReference evictionLimitSR = encodingContext.evictionLimit(); + if (nameOnlyDynamicEntry) { + long nameIndex = entry.index(); + if (!canReferenceEntry(nameIndex)) { + return ENTRY_NOT_INSERTED; + } + evictionLimitSR = evictionLimitSR.reduce(nameIndex); + encodingContext.registerSessionReference(nameIndex); + } + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeNameIndex = + nameOnlyDynamicEntry ? toRelative(entry.index()) : -1; + + // Insert new entry to the table with respect to entry + // references range provided by the encoding context + long idx = insert(name, value, evictionLimitSR); + if (idx == ENTRY_NOT_INSERTED) { + // Insertion requires eviction of entries from unacknowledged + // sections therefore entry is not added + return ENTRY_NOT_INSERTED; + } + // Entry was successfully inserted + if (nameOnlyDynamicEntry) { + // Absolute index only needs to be replaced with the relative one + // when it references a name in the dynamic table. + entry = entry.relativizeDynamicTableEntry(relativeNameIndex); + } + int instructionSize = writer.configureForEntryInsertion(entry); + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private long insert(HeaderField h, SectionReference sectionReference) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("adding ('%s', '%s')", h.name(), h.value())); + } + long entrySize = headerSize(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s", h.name(), h.value(), entrySize)); + } + + long availableEvictableSpace = availableEvictableSpace(sectionReference); + if (availableEvictableSpace < entrySize) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Header size exceeds available evictable space=%s." + + " Combined section reference=%s", + availableEvictableSpace, sectionReference)); + } + // Evicting entries won't help to gather enough space to insert the requested one + return ENTRY_NOT_INSERTED; + } + while (entrySize > capacity - size && size != 0) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("insufficient space %s, must evict entry", (capacity - size))); + } + // Only Encoder will supply section with referenced + // entries + if (sectionReference.referencesEntries()) { + // Check if tail element is evictable + if (tail < sectionReference.min()) { + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } else { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Cannot evict entry: sectionRef=%s tail=%s", + sectionReference, tail)); + } + // For now -1 is returned to notify the Encoder that entry + // cannot be inserted to the dynamic table + return ENTRY_NOT_INSERTED; + } + } else { + // This call can be called by both Encoder and Decoder: + // - Encoder when add new entry with no unacked section references + // - Decoder when processing insert entry instructions. + // Entries are evicted until there is enough space OR until table + // is empty. + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } + } + size += entrySize; + // At this stage it is clear that there are enough bytes (max capacity is not exceeded) in the dynamic + // table to add new header field + addWithInverseMapping(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("('%s, '%s') added", h.name(), h.value())); + logger.log(EXTRA, this::toString); + } + notifyInsertCountChange(); + return head - 1; + } finally { + writeLock.unlock(); + } + } + + public long duplicate(long relativeId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = getRelative(relativeId); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Duplicate entry with absId=%s" + + " insertCount=%s ('%s', '%s')", + insertCount() - 1 - relativeId, insertCount(), + entry.name(), entry.value())); + } + return insert(entry.name(), entry.value()); + } finally { + writeLock.unlock(); + } + } + + public long duplicateWithEncoderStreamUpdate(EncoderInstructionsWriter writer, + long absoluteEntryId, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = get(absoluteEntryId); + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeEntryId = toRelative(absoluteEntryId); + + // Make entry id that needs to be duplicated non-evictable + SectionReference evictionLimit = encodingContext.evictionLimit() + .reduce(absoluteEntryId); + + // Put duplicated entry to our dynamic table first + long idx = insert(entry.name(), entry.value(), + evictionLimit); + if (idx == ENTRY_NOT_INSERTED) { + return ENTRY_NOT_INSERTED; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Issuing entry duplication instruction" + + " for absId=%s relId=%s ('%s', '%s')", + absoluteEntryId, relativeEntryId, entry.name(), entry.value())); + } + + // Configure writer for entry duplication + int instructionSize = + writer.configureForEntryDuplication(relativeEntryId); + + // Write instruction to the encoder stream + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private HeaderField remove() { + assert lock.isWriteLockedByCurrentThread(); + // Remove element from the holder array first + if (getElementsCount() == 0) { + throw new IllegalStateException("Empty table"); + } + + int tailIdx = (int) (tail++ & (elements.length - 1)); + HeaderField f = elements[tailIdx]; + elements[tailIdx] = null; + + // Log the removal event + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("removing ('%s', '%s')", f.name(), f.value())); + } + + // Update indices map on the encoder table only + if (encoderTable) { + Map> values = indicesMap.get(f.name()); + Deque indexes = values.get(f.value()); + // Remove the oldest index of the name:value pair + Long index = indexes.pollFirst(); + // Clean-up indexes associated with a value from values map + if (indexes.isEmpty()) { + values.remove(f.value()); + } + assert index != null; + // If indexes map associated with name is empty remove name + // entry from indices map + if (values.isEmpty()) { + indicesMap.remove(f.name()); + } + } + return f; + } + + /** + * Sets the dynamic table capacity in bytes. + * The new capacity must be lower than or equal to the limit defined by + * SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 settings parameter. This limit is + * enforced by {@linkplain DynamicTable#setMaxTableCapacity(long)}. + * + * @param capacity dynamic table capacity to set + */ + public void setCapacity(long capacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (capacity > maxCapacity) { + // Calling code catches IllegalArgumentException and generates the connection error: + // 4.3.1. Set Dynamic Table Capacity: + // "The decoder MUST treat a new dynamic table capacity value that exceeds + // this limit as a connection error of type QPACK_ENCODER_STREAM_ERROR." + throw new IllegalArgumentException("Illegal dynamic table capacity"); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: capacity=" + capacity); + } + while (capacity < size && size != 0) { + // Evict entries until existing elements fit into + // new table capacity + boolean entryEvicted = evictEntry(); + assert entryEvicted; + } + this.capacity = capacity; + if (usedSpace() < drainUsedSpaceThreshold) { + if (drain != -1) { + drain = -1; + } + } else if (drain == -1 || tail > drain) { + drain = tail; + } + } finally { + writeLock.unlock(); + } + } + + /** + * Updates the capacity of the dynamic table and sends encoder capacity update instruction + * bytes to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * @param writer non-configured encoder instruction writer for generating encoder instruction + * @param capacity new capacity value + * @param encoderStreams encoder stream pair + */ + public void setCapacityWithEncoderStreamUpdate(EncoderInstructionsWriter writer, long capacity, + QueuingStreamPair encoderStreams) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // Configure writer for capacity update + int instructionSize = writer.configureForTableCapacityUpdate(capacity); + // Check and set our capacity + setCapacity(capacity); + // Write instruction + writeEncoderInstruction(writer, instructionSize, encoderStreams); + } finally { + writeLock.unlock(); + } + } + + + /** + * Evicts one entry from the table tail. + * @return {@code true} if entry was evicted, + * {@code false} if nothing to remove + */ + private boolean evictEntry() { + assert lock.isWriteLockedByCurrentThread(); + try { + HeaderField f = remove(); + long s = headerSize(f); + this.size -= s; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("evicted entry ('%s', '%s') of size %s with absId=%s", + f.name(), f.value(), s, tail - 1)); + } + } catch (IllegalStateException ise) { + // Entry cannot be evicted from empty table + return false; + } + return true; + } + + public long availableEvictableSpace(SectionReference sectionReference) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (!sectionReference.referencesEntries()) { + return capacity; + } + // Size that can be reclaimed in the dynamic table by evicting + // non-referenced entries + long availableEvictableCapacity = 0; + for (long absId = tail; absId < sectionReference.min(); absId++) { + HeaderField field = get(absId); + availableEvictableCapacity += headerSize(field); + } + // (capacity - size) - free space in the dynamic table + return availableEvictableCapacity + (capacity - size); + } finally { + readLock.unlock(); + } + } + + public boolean tryReferenceEntry(TableEntry tableEntry, EncodingContext context) { + var readLock = lock.readLock(); + readLock.lock(); + try { + long absId = tableEntry.index(); + if (canReferenceEntry(absId)) { + context.registerSessionReference(absId); + context.referenceEntry(tableEntry); + return true; + } else { + return false; + } + } finally { + readLock.unlock(); + } + } + + @Override + public String toString() { + var readLock = lock.readLock(); + readLock.lock(); + try { + double used = usedSpace(); + return format("full length: %s, used space: %s/%s (%.1f%%)", + getElementsCount(), size, capacity, used); + } finally { + readLock.unlock(); + } + } + + private boolean canReferenceEntry(long absId) { + // The dynamic table lock is acquired by the calling methods + return absId > drain; + } + + private double usedSpace() { + return capacity == 0 ? 0 : 100 * (((double) size) / capacity); + } + + public static long headerSize(HeaderField f) { + return headerSize(f.name(), f.value()); + } + + public static long headerSize(String name, String value) { + return name.length() + value.length() + ENTRY_SIZE; + } + + // To quickly find an index of an entry in the dynamic table with the + // given contents an effective inverse mapping is needed. + private void addWithInverseMapping(HeaderField field) { + assert lock.isWriteLockedByCurrentThread(); + // Check if holder array has at least one free slot to add header field + // The method below can increase elements.length if no free slot found + ensureElementsArrayLength(); + long counterSnapshot = head++; + elements[(int) (counterSnapshot & (elements.length - 1))] = field; + if (encoderTable) { + // Allocate unique index and use it to store in indicesMap + Map> values = indicesMap.computeIfAbsent( + field.name(), _ -> new HashMap<>()); + Deque indexes = values.computeIfAbsent( + field.value(), _ -> new LinkedList<>()); + indexes.add(counterSnapshot); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("added '%s' header field with '%s' unique id", + field, counterSnapshot)); + } + assert indexesUniqueAndOrdered(indexes); + // Draining index is only used by the Encoder + updateDrainIndex(); + } + } + + private void updateDrainIndex() { + if (!encoderTable) { + return; + } + assert lock.isWriteLockedByCurrentThread(); + if (usedSpace() > drainUsedSpaceThreshold) { + if (drain == -1L) { + drain = tail; + } else { + drain++; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Draining index changed: %d", drain)); + } + } + } + + private void ensureElementsArrayLength() { + assert lock.isWriteLockedByCurrentThread(); + + int currentArrayLength = elements.length; + if (getElementsCount() == currentArrayLength) { + if (currentArrayLength == (1 << 30)) { + throw new IllegalStateException("No room for more elements"); + } + // Increase elements array by factor of 2 + resize(currentArrayLength << 1); + } + } + + private boolean indexesUniqueAndOrdered(Deque indexes) { + long maxIndexSoFar = -1L; + for (long l : indexes) { + if (l <= maxIndexSoFar) { + return false; + } else { + maxIndexSoFar = l; + } + } + return true; + } + + private int getElementsCount() { + // head and tail are unique and monotonic indexes - therefore we can just use their + // difference to determine number of header:value pairs stored in the dynamic table. + // Since head points to the next unused element head == 0 means that there is + // no elements in the dynamic table + return head > 0 ? (int) (head - tail) : 0; + } + + private void resize(int newSize) { + // newSize is always a power of 2: + // - its initial size is a power of 2 + // - it is shifted 1 bit left every + // time when there is not enough space in the 'elements' array + assert lock.isWriteLockedByCurrentThread(); + int elementsCnt = getElementsCount(); + final int oldSize = elements.length; + + if (newSize < elementsCnt) { + throw new IllegalArgumentException("New size is too low to hold existing elements"); + } + + HeaderField[] newElements = new HeaderField[newSize]; + if (elementsCnt == 0) { + elements = newElements; + return; + } + long headID = head - 1; + final int oldTailIdx = (int) (tail & (oldSize - 1)); + final int oldHeadIdx = (int) (headID & (oldSize - 1)); + final int newTailIdx = (int) (tail & (newSize - 1)); + final int newHeadIdx = (int) (headID & (newSize - 1)); + + if (oldTailIdx <= oldHeadIdx) { + // Elements in an old array are stored in a continuous segment + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, elementsCnt); + } else { + // Elements in a new array will split in two segments due to wrapping around + // the end of a new array. + int sizeFromNewTailToEnd = newSize - newTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, sizeFromNewTailToEnd); + System.arraycopy(elements, oldTailIdx + sizeFromNewTailToEnd, + newElements, 0, newHeadIdx + 1); + } + } else { + // Elements in an old array are split in two segments + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + int firstSegmentSize = oldSize - oldTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, firstSegmentSize); + System.arraycopy(elements, 0, + newElements, newTailIdx + firstSegmentSize, oldHeadIdx + 1); + } else { + // Elements in a new array will be stored in two segments + // Size from the tail to the end in an old array + int oldPart1Size = oldSize - oldTailIdx; + // Size from the tail to the end in a new array + int newPart1Size = newSize - newTailIdx; + if (oldPart1Size <= newPart1Size) { + // Segment from tail to the end of an old array + // fits into the corresponding segment in a new array + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, oldPart1Size); + int leftToCopyToNewPart1 = newPart1Size - oldPart1Size; + System.arraycopy(elements, 0, newElements, + newTailIdx + oldPart1Size, leftToCopyToNewPart1); + System.arraycopy(elements, leftToCopyToNewPart1, + newElements, 0, newHeadIdx + 1); + } else { // oldPart1Size > newPart1Size + // Not possible given two restrictions: + // - we do not allow rewriting of entries if size is not enough, + // IAE is thrown above. + // - the size of elements holder array can only be a power of 2 + throw new AssertionError("Not possible dynamic table indexes configuration"); + } + } + } + elements = newElements; + } + + /** + * Method returns number of elements inserted to the dynamic table. + * Since element ids start from 0 the returned value is equal + * to the id of the head element plus one. + * @return number of elements in the dynamic table + */ + public long insertCount() { + var rl = lock.readLock(); + rl.lock(); + try { + // head points to the next unallocated element + return head; + } finally { + rl.unlock(); + } + } + + // Writes an encoder instruction to the encoder stream associated with dynamic table. + // This method is kept in DynamicTable class since most instructions depend on + // and/or update the dynamic table state. + // Also, we want to send encoder instruction and update the dynamic table state while + // holding the write-lock. + private void writeEncoderInstruction(EncoderInstructionsWriter writer, int instructionSize, + QueuingStreamPair encoderStreams) { + if (instructionSize > encoderStreams.credit()) { + throw new QPackException(H3_CLOSED_CRITICAL_STREAM, + new IOException("QPACK not enough credit on an encoder stream " + + encoderStreams.remoteStreamType()), true); + } + boolean done; + ByteBuffer buffer; + do { + if (instructionSize > MAX_BUFFER_SIZE) { + buffer = ByteBuffer.allocate(MAX_BUFFER_SIZE); + instructionSize -= MAX_BUFFER_SIZE; + } else { + buffer = ByteBuffer.allocate(instructionSize); + } + done = writer.write(buffer); + buffer.flip(); + encoderStreams.submitData(buffer); + } while (!done); + } + private static final int MAX_BUFFER_SIZE = 1024 * 16; + static final long ENTRY_NOT_INSERTED = -1L; +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java new file mode 100644 index 00000000000..26da27a702a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java @@ -0,0 +1,672 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.TableEntry.EntryType; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.internal.net.http.qpack.writers.FieldLineSectionPrefixWriter; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Encodes headers to their binary representation. + */ +public class Encoder { + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + // RFC 9204 7.1.3. Never-Indexed Literals: + // "Implementations can also choose to protect sensitive fields by not + // compressing them and instead encoding their value as literals" + private static final Set SENSITIVE_HEADER_NAMES = + Set.of("cookie", "authorization", "proxy-authorization"); + + private final Logger logger; + private final InsertionPolicy policy; + private final TablesIndexer tablesIndexer; + private final DynamicTable dynamicTable; + private final QueuingStreamPair encoderStreams; + private final DecoderInstructionsReader decoderInstructionsReader; + // RFC-9204: 2.1.4. Known Received Count + private long knownReceivedCount; + + // Lock for Known Received Count variable + private final ReentrantReadWriteLock krcLock = new ReentrantReadWriteLock(); + + // Max blocked streams setting value received from the peer decoder + // can be set only once + private long maxBlockedStreams = -1L; + + // Number of streams in process of headers encoding that expected to be blocked + // but their unacknowledged section is not registered yet + private long blockedStreamsInFlight; + + private final ReentrantLock blockedStreamsCounterLock = new ReentrantLock(); + + // stream id -> fifo list of max and min ids referenced from field sections for each stream id + private final ConcurrentMap> unacknowledgedSections = + new ConcurrentHashMap<>(); + + // stream id -> set of referenced entry absolute indexes from a field line section that currently + // are in process of encoding and not added to the unacknowledged field sections map yet. + private final ConcurrentMap> liveContextReferences = + new ConcurrentHashMap<>(); + + private final QPACKErrorHandler qpackErrorHandler; + + public HeaderFrameWriter newHeaderFrameWriter() { + return new HeaderFrameWriter(logger); + } + + /** + * Constructs an {@code Encoder} with zero initial capacity of the dynamic table. + * Maximum dynamic table capacity is not initialized until peer (decoder) HTTP/3 settings frame is + * received (see {@link Encoder#configure(ConnectionSettings)}). + * + *

      Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

      Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

      An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @param streamPairs supplier of the encoder unidirectional stream pair + * @throws IllegalArgumentException if maxCapacity is negative + * @see Encoder#configure(ConnectionSettings) + */ + public Encoder(InsertionPolicy policy, StreamPairSupplier streamPairs, QPACKErrorHandler codingError) { + this.policy = policy; + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("Encoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New encoder"); + } + if (logger.isLoggable(EXTRA)) { + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(EXTRA, () -> { + String hashCode = Integer.toHexString( + System.identityHashCode(this)); + /* Since Encoder can be subclassed hashCode AND identity + hashCode might be different. So let's print both. */ + return format("toString='%s', hashCode=%s, identityHashCode=%s", + this, hashCode(), hashCode); + }); + } + // Set maximum dynamic table to 0, postpone setting of max capacity until peer + // settings frame is received + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), true); + tablesIndexer = new TablesIndexer(StaticTable.HTTP3, dynamicTable); + encoderStreams = streamPairs.create(this::processDecoderAcks); + decoderInstructionsReader = new DecoderInstructionsReader(new TableUpdatesCallback(), + logger); + qpackErrorHandler = codingError; + } + + /** + * Configures encoder according to the settings received from the peer. + * + * @param peerSettings the peer settings + */ + public void configure(ConnectionSettings peerSettings) { + blockedStreamsCounterLock.lock(); + try { + if (maxBlockedStreams == -1) { + maxBlockedStreams = peerSettings.qpackBlockedStreams(); + } else { + throw new IllegalStateException("Encoder already configured"); + } + } finally { + blockedStreamsCounterLock.unlock(); + } + // Set max dynamic table capacity + long maxCapacity = peerSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + // Send DT capacity update instruction if the peer negotiated non-zero + // max table capacity, and limit the value with encoder's table capacity + // limit system property value + if (QPACK.ENCODER_TABLE_CAPACITY_LIMIT > 0 && maxCapacity > 0) { + long encoderCapacity = Math.min(maxCapacity, QPACK.ENCODER_TABLE_CAPACITY_LIMIT); + setTableCapacity(encoderCapacity); + } + } + + public QueuingStreamPair encoderStreams() { + return encoderStreams; + } + + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive) throws IllegalStateException { + header(context, name, value, sensitive, knownReceivedCount()); + } + + /** + * Sets up the given header {@code (name, value)} with possibly sensitive + * value. + * + *

      If the {@code value} is sensitive (think security, secrecy, etc.) + * this encoder will compress it using a special representation + * (see + * 7.1.3. Never-Indexed Literals). + * + *

      Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param context the encoding context + * @param name the name + * @param value the value + * @param sensitive whether the value is sensitive + * @param knownReceivedCount the count of received entries known to a peer decoder or + * {@code -1} to skip the dynamic table entry index check during header encoding. + * @throws NullPointerException if any of the arguments are {@code null} + * @throws IllegalStateException if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) + */ + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive, long knownReceivedCount) throws IllegalStateException { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", + name, value, sensitive)); + } + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + // TablesIndexer.entryOf checks if the found entry is a dynamic table entry, + // and if its insertion was already ACKed. If not - use literal or name index encoding. + var tableEntry = tablesIndexer.entryOf(name, value, knownReceivedCount); + + // NAME_VALUE table entry type means that one of dynamic or static tables contain + // exact name:value pair. + if (dynamicTable.capacity() > 0L + && tableEntry.type() != EntryType.NAME_VALUE + && !sensitive && policy.shouldUpdateDynamicTable(tableEntry)) { + // We should check if we have an entry in dynamic table: + // - If we have it - do nothing + // - if we do not have it - insert it and use the index straight-away + // when blocking encoding is allowed + tableEntry = context.tryInsertEntry(tableEntry); + } + + // First, check that found/newly inserted entry is in the dynamic table + // and can be referenced + if (!tableEntry.isStaticTable() && tableEntry.index() >= 0 && + tableEntry.type() != EntryType.NEITHER) { + if (!dynamicTable.tryReferenceEntry(tableEntry, context)) { + // If entry cannot be referenced - use literal encoding instead + tableEntry = tableEntry.toLiteralsEntry(); + } + } + + // Configure header frame writer to write header field to the headers frame. One of the following + // writers is selected based on entry type, the base value and the referenced table (static or dynamic): + // - static table and name:value match - "Indexed Field Line" + // - static table and name match - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index < base - "Indexed Field Line" + // - dynamic table, name match and index < base - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index >= base - "Indexed Field Line with Post-Base Index" + // - dynamic table, name match and index >= base - "Literal Field Line with Post-Base Name Reference" + // - not in dynamic or static tables - "Literal Field Line with Literal Name" + context.writer.configure(tableEntry, sensitive, context.base); + } + + /** + * Sets the capacity of the encoder's dynamic table and notifies the decoder by + * issuing "Set Dynamic Table Capacity" instruction. + * + *

      The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 4.3.1. Set Dynamic Table Capacity). + * + * @param capacity a non-negative long + * @throws IllegalArgumentException if capacity is negative or exceeds the negotiated max capacity HTTP/3 setting + */ + public void setTableCapacity(long capacity) { + dynamicTable.setCapacityWithEncoderStreamUpdate(new EncoderInstructionsWriter(logger), + capacity, encoderStreams); + } + + /** + * This method is called when the peer decoder sends + * data on the peer's decoder stream + * + * @param buffer data sent by the peer's decoder + */ + private void processDecoderAcks(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + encoderStreams.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + decoderInstructionsReader.read(buffer); + } catch (QPackException e) { + qpackErrorHandler.closeOnError(e.getCause(), e.http3Error()); + } + } + + public List encodeHeaders(HeaderFrameWriter writer, long streamId, + int bufferSize, HttpHeaders... headers) { + List buffers = new ArrayList<>(); + ByteBuffer buffer = getByteBuffer(bufferSize); + + try (EncodingContext encodingContext = newEncodingContext(streamId, + dynamicTable.insertCount(), writer)) { + for (HttpHeaders header : headers) { + for (Map.Entry> e : header.map().entrySet()) { + // RFC-9114, section 4.2: Field names are strings containing a subset of + // ASCII characters. .... Characters in field names MUST be converted to + // lowercase prior to their encoding. + final String lKey = e.getKey().toLowerCase(Locale.ROOT); + final List values = e.getValue(); + // An encoder might also choose not to index values for fields that are + // considered to be highly valuable or sensitive to recovery, such as the + // Cookie or Authorization header fields + final boolean sensitive = SENSITIVE_HEADER_NAMES.contains(lKey); + for (String value : values) { + header(encodingContext, lKey, value, sensitive); + while (!writer.write(buffer)) { + buffer.flip(); + buffers.add(buffer); + buffer = getByteBuffer(bufferSize); + } + } + } + } + buffer.flip(); + buffers.add(buffer); + + // Put field line section prefix as the first byte buffer + generateFieldLineSectionPrefix(encodingContext, buffers); + + // Register field line section as unacked if it uses references to the + // dynamic table entries + registerUnackedFieldLineSection(streamId, SectionReference.of(encodingContext)); + } + return buffers; + } + + public void generateFieldLineSectionPrefix(EncodingContext encodingContext, List buffers) { + // Write field section prefix according to RFC 9204: "4.5.1. Encoded Field Section Prefix" + FieldLineSectionPrefixWriter prefixWriter = new FieldLineSectionPrefixWriter(); + FieldSectionPrefix fsp = encodingContext.sectionPrefix(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoding Field Section Prefix - required insert" + + " count: %d base: %d", + fsp.requiredInsertCount(), fsp.base())); + } + int requiredSize = prefixWriter.configure(fsp, dynamicTable.maxEntries()); + var fspBuffer = getByteBuffer(requiredSize); + if (!prefixWriter.write(fspBuffer)) { + throw new IllegalStateException("Field Line Section Prefix"); + } + fspBuffer.flip(); + buffers.addFirst(fspBuffer); + } + + public void registerUnackedFieldLineSection(long streamId, SectionReference sectionReference) { + if (sectionReference.referencesEntries()) { + unacknowledgedSections + .computeIfAbsent(streamId, k -> new ConcurrentLinkedQueue<>()) + .add(sectionReference); + } + } + + // This one is for tracking evict-ability of dynamic table entries + public SectionReference unackedFieldLineSectionsRange(EncodingContext context) { + SectionReference referenceNotRegisteredYet = SectionReference.of(context); + return unackedFieldLineSectionsRange(referenceNotRegisteredYet); + } + + private SectionReference unackedFieldLineSectionsRange(SectionReference initial) { + return unacknowledgedSections.values().stream() + .flatMap(Queue::stream) + .reduce(initial, SectionReference::reduce); + } + + long blockedStreamsCount() { + long blockedStreams = 0; + long krc = knownReceivedCount(); + for (var streamSections : unacknowledgedSections.values()) { + boolean hasBlockedSection = streamSections.stream() + .anyMatch(sectionReference -> !sectionReference.fullyAcked(krc)); + blockedStreams = hasBlockedSection ? blockedStreams + 1 : blockedStreams; + } + return blockedStreams; + } + + + public long knownReceivedCount() { + krcLock.readLock().lock(); + try { + return knownReceivedCount; + } finally { + krcLock.readLock().unlock(); + } + } + + private void updateKrcSectionAck(long streamId) { + krcLock.writeLock().lock(); + try { + var queue = unacknowledgedSections.get(streamId); + // max() + 1 - since it is "Required Insert Count" not entry ID + SectionReference oldestSectionRef = queue != null ? queue.poll() : null; + long oldestNonAckedRic = oldestSectionRef != null ? oldestSectionRef.max() + 1 : -1L; + if (oldestNonAckedRic == -1L) { + // RFC 9204 4.4.1. Section Acknowledgment: + // If an encoder receives a Section Acknowledgment instruction referring + // to a stream on which every encoded field section with a non-zero + // Required Insert Count has already been acknowledged, this MUST be treated + // as a connection error of type QPACK_DECODER_STREAM_ERROR. + var qPackException = QPackException.decoderStreamError( + new IllegalStateException("No unacknowledged sections found" + + " for stream id = " + streamId)); + throw qPackException; + } + // "2.1.4. Known Received Count": + // If the Required Insert Count of the acknowledged field section is greater + // than the current Known Received Count, the Known Received Count is updated + // to that Required Insert Count value. + if (oldestNonAckedRic != -1 && knownReceivedCount < oldestNonAckedRic) { + knownReceivedCount = oldestNonAckedRic; + } + } finally { + krcLock.writeLock().unlock(); + } + } + + private void updateKrcInsertCountIncrement(long increment) { + long insertCount = dynamicTable.insertCount(); + krcLock.writeLock().lock(); + try { + // An encoder that receives an Increment field equal to zero, or one that increases + // the Known Received Count beyond what the encoder has sent, MUST treat this as + // a connection error of type QPACK_DECODER_STREAM_ERROR. + if (increment == 0 || knownReceivedCount > insertCount - increment) { + var qpackException = QPackException.decoderStreamError( + new IllegalStateException("Invalid increment field value: " + increment)); + throw qpackException; + } + knownReceivedCount += increment; + } finally { + krcLock.writeLock().unlock(); + } + } + + private void cleanupStreamData(long streamId) { + liveContextReferences.remove(streamId); + unacknowledgedSections.remove(streamId); + } + + private class TableUpdatesCallback implements DecoderInstructionsReader.Callback { + @Override + public void onSectionAck(long streamId) { + updateKrcSectionAck(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + updateKrcInsertCountIncrement(increment); + } + + @Override + public void onStreamCancel(long streamId) { + cleanupStreamData(streamId); + } + } + + public class EncodingContext implements AutoCloseable { + final long base; + final long streamId; + final ConcurrentSkipListSet referencedIndexes; + long maxIndex; + long minIndex; + boolean blockedDecoderExpected; + final HeaderFrameWriter writer; + final EncoderInstructionsWriter encoderInstructionsWriter; + + public EncodingContext(long streamId, long base, HeaderFrameWriter writer) { + this.base = base; + this.encoderInstructionsWriter = new EncoderInstructionsWriter(logger); + this.writer = writer; + this.maxIndex = -1L; + this.minIndex = Long.MAX_VALUE; + this.streamId = streamId; + this.referencedIndexes = liveContextReferences.computeIfAbsent(streamId, + _ -> new ConcurrentSkipListSet<>()); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Begin encoding session with base = %s stream-id = %s", base, streamId)); + } + } + + public void registerSessionReference(long absoluteEntryId) { + referencedIndexes.add(absoluteEntryId); + } + + @Override + public void close() { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Closing encoding context for stream-id=%s" + + " session references:%s", + streamId, referencedIndexes)); + } + liveContextReferences.remove(streamId); + // Deregister if this stream was marked as in-flight blocked + blockedStreamsCounterLock.lock(); + try { + if (blockedDecoderExpected) { + blockedStreamsInFlight--; + } + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public FieldSectionPrefix sectionPrefix() { + // RFC 9204: 2.1.2. Blocked Streams + // "the Required Insert Count is one larger than the largest absolute index + // of all referenced dynamic table entries" + // largestAbsoluteIndex is initialized to -1, and if there is no dynamic + // table entry references - RIC will be set to 0. + return new FieldSectionPrefix(maxIndex + 1, base); + } + + public SectionReference evictionLimit() { + // In-flight references - a set with entry ids referenced from all + // active header encoding sessions not fully encoded yet + SectionReference inFlightReferences = SectionReference.singleReference( + liveContextReferences.values().stream() + .filter(Predicate.not(ConcurrentSkipListSet::isEmpty)) + .map(ConcurrentSkipListSet::first) + .min(Long::compare) + .orElse(-1L)); + + // Calculate the eviction limit with respect to: + // - in-flight references + // - acknowledged dynamic table insertions + // - range of unacknowledged sections which already fully encoded + // and sent as part of other request/response streams + return inFlightReferences + .reduce(knownReceivedCount()) + .reduce(unackedFieldLineSectionsRange(this)); + } + + public TableEntry tryInsertEntry(TableEntry entry) { + long idx = dynamicTable.insertWithEncoderStreamUpdate(entry, + encoderInstructionsWriter, encoderStreams, + this); + if (idx == DynamicTable.ENTRY_NOT_INSERTED) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Not adding entry '%s' to the dynamic " + + "table - not enough space, or unacknowledged entry needs to be evicted", + entry)); + } + // Return what we previously found in the dynamic or static table + return entry; + } + + if (QPACK.ALLOW_BLOCKING_ENCODING && canReferenceNewEntry()) { + // Create a new TableEntry that describes newly added header field + return entry.toNewDynamicTableEntry(idx); + } else { + return entry; + } + } + + private boolean canReferenceNewEntry() { + blockedStreamsCounterLock.lock(); + try { + // If current encoding context is already marked as blocked we can + // reference new entries without analyzing number of blocked streams + if (blockedDecoderExpected) { + return true; + } + // Number of streams with unacknowledged field line section + long alreadyBlocked = blockedStreamsCount(); + // Other streams might be in progress of headers encoding + boolean canReferenceNewEntry = maxBlockedStreams - alreadyBlocked - blockedStreamsInFlight > 0; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("%s reference to newly added header. " + + "Number of blocked streams based on unAcked sections: %d " + + "Number of blocked streams in progress of encoding: %d " + + "Max allowed by HTTP/3 settings: %d", + canReferenceNewEntry ? "Allowing" : "Restricting", + alreadyBlocked, blockedStreamsInFlight, maxBlockedStreams)); + } + if (canReferenceNewEntry && !blockedDecoderExpected) { + blockedStreamsInFlight++; + blockedDecoderExpected = true; + } + return canReferenceNewEntry; + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public void referenceEntry(TableEntry tableEntry) { + assert tableEntry.index() >= 0; + if (!tableEntry.isStaticTable()) { + long index = tableEntry.index(); + maxIndex = Long.max(maxIndex, index); + minIndex = Long.min(minIndex, index); + } + } + } + + /** + * Descriptor of entries range referenced from a field lines section. + * + * @param min minimum entry id referenced from a field lines section + * @param max maximum entry id referenced from a field lines section + */ + public record SectionReference(long min, long max) { + public static SectionReference of(EncodingContext context) { + if (context.maxIndex == -1L) { + return SectionReference.noReferences(); + } + return new SectionReference(context.minIndex, context.maxIndex); + } + + public SectionReference reduce(SectionReference other) { + if (!referencesEntries()) { + return other; + } else if (!other.referencesEntries()) { + return this; + } + long newMin = Long.min(this.min, other.min); + long newMax = Long.max(this.max, other.max); + return new SectionReference(newMin, newMax); + } + + public SectionReference reduce(long entryId) { + return reduce(singleReference(entryId)); + } + + public static SectionReference singleReference(long entryId) { + return new SectionReference(entryId, entryId); + } + + public boolean fullyAcked(long knownReceiveCount) { + return max < knownReceiveCount; + } + + public static SectionReference noReferences() { + return new SectionReference(-1L, -1L); + } + + public boolean referencesEntries() { + return max != -1L; + } + } + + public EncodingContext newEncodingContext(long streamId, long base, HeaderFrameWriter writer) { + assert streamId >= 0; + assert base >= 0; + return new EncodingContext(streamId, base, writer); + } + + private ByteBuffer getByteBuffer(int size) { + ByteBuffer buf = ByteBuffer.allocate(size); + buf.limit(size); + return buf; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java new file mode 100644 index 00000000000..cbf2037af6a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack; + +public record FieldSectionPrefix(long requiredInsertCount, long base) { + + public static FieldSectionPrefix decode(long encodedRIC, long deltaBase, + int baseSign, DynamicTable dynamicTable) { + long decodedRIC = decodeRIC(encodedRIC, dynamicTable); + long decodedBase = decodeBase(decodedRIC, deltaBase, baseSign); + return new FieldSectionPrefix(decodedRIC, decodedBase); + } + + private static long decodeRIC(long encodedRIC, DynamicTable dynamicTable) { + if (encodedRIC == 0) { + return 0; + } + long maxEntries = dynamicTable.maxEntries(); + long insertCount = dynamicTable.insertCount(); + long fullRange = 2 * maxEntries; + if (encodedRIC > fullRange) { + throw decompressionFailed(); + } + long maxValue = insertCount + maxEntries; + long maxWrapped = (maxValue/fullRange) * fullRange; + long ric = maxWrapped + encodedRIC - 1; + if (ric > maxValue) { + if (ric <= fullRange) { + throw decompressionFailed(); + } + ric -= fullRange; + } + + if (ric == 0) { + throw decompressionFailed(); + } + return ric; + } + + private static long decodeBase(long decodedRic, long deltaBase, int signBit) { + if (signBit == 0) { + return decodedRic + deltaBase; + } else { + return decodedRic - deltaBase - 1; + } + } + + private static QPackException decompressionFailed() { + var decompressionFailed = new IllegalStateException("QPACK decompression failed"); + return QPackException.decompressionFailed(decompressionFailed, true); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java new file mode 100644 index 00000000000..83ee21eda30 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +public record HeaderField(String name, String value) { + + public HeaderField(String name) { + this(name, ""); + } + + @Override + public String toString() { + return value.isEmpty() ? name : name + ":" + value; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java new file mode 100644 index 00000000000..4b70777401d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack; + +public sealed interface HeadersTable permits StaticTable, DynamicTable { + + /** + * Add an entry to the table. + * + * @param name header name + * @param value header value + * @return unique index of entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + long insert(String name, String value); + + /** + * Get a table entry with specified unique index. + * + * @param index an entry unique index + * @return table entry + */ + HeaderField get(long index); + + /** + * Returns an index for name:value pair, or just name in a headers table. + * The contract for return values is the following: + * - a positive integer {@code i} where {@code i - 1} is an index of an + * entry with a header (n, v), where {@code n.equals(name) && v.equals(value)}. + *

      + * - a negative integer {@code j} where {@code -j - 1} is an index of an entry with + * a header (n, v), where {@code n.equals(name)}. + *

      + * - {@code 0} if there's no entry 'e' found such that {@code e.getName().equals(name)} + * + * @param name a name to search for + * @param value a value to search for + * @return a non-zero value if a matching entry is found, 0 otherwise + */ + long search(String name, String value); +} diff --git a/test/jdk/sun/tools/jrunscript/CheckEngine.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java similarity index 67% rename from test/jdk/sun/tools/jrunscript/CheckEngine.java rename to src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java index dd8f4cd81ae..1bdf84304ca 100644 --- a/test/jdk/sun/tools/jrunscript/CheckEngine.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -20,17 +22,8 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package jdk.internal.net.http.qpack; -import javax.script.*; - -/* - * If the JDK being tested is not a Sun product JDK and a js - * engine is not present, return an exit code of 2 to indicate that - * the jrunscript tests which assume a js engine can be vacuously - * passed. - */ -public class CheckEngine { - public static void main(String... args) { - System.exit(2); - } +public interface InsertionPolicy { + boolean shouldUpdateDynamicTable(TableEntry tableEntry); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java new file mode 100644 index 00000000000..44282e5ad53 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2017, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger.Level; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ALLOW_BLOCKING_ENCODING; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_BLOCKED_STREAMS; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_TABLE_CAPACITY; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_DRAINING_THRESHOLD; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_TABLE_CAPACITY_LIMIT; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NONE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Internal utilities and stuff. + */ +public final class QPACK { + + + // A dynamic table capacity that the encoder is allowed to set given that it doesn't + // exceed the max capacity value negotiated by the decoder. If the max capacity + // less than this limit the encoder's dynamic table capacity is set to the max capacity + // value. + public static final long ENCODER_TABLE_CAPACITY_LIMIT = QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_TABLE_CAPACITY = QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_FIELD_SECTION_SIZE = QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long DECODER_BLOCKED_STREAMS = QPACK_DECODER_BLOCKED_STREAMS; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean ALLOW_BLOCKING_ENCODING = QPACK_ALLOW_BLOCKING_ENCODING; + + // Threshold of available dynamic table space after which the draining + // index starts increasing. This index determines which entries are + // too close to eviction, and can be referenced by the encoder + public static final int ENCODER_DRAINING_THRESHOLD = QPACK_ENCODER_DRAINING_THRESHOLD; + + private static final RootLogger LOGGER; + private static final Map logLevels = + Map.of("NORMAL", NORMAL, "EXTRA", EXTRA); + + static { + String PROPERTY = "jdk.internal.httpclient.qpack.log.level"; + String value = Utils.getProperty(PROPERTY); + + if (value == null) { + LOGGER = new RootLogger(NONE); + } else { + String upperCasedValue = value.toUpperCase(); + Level l = logLevels.get(upperCasedValue); + if (l == null) { + LOGGER = new RootLogger(NONE); + LOGGER.log(System.Logger.Level.INFO, + () -> format("%s value '%s' not recognized (use %s); logging disabled", + PROPERTY, value, String.join(", ", logLevels.keySet()))); + } else { + LOGGER = new RootLogger(l); + LOGGER.log(System.Logger.Level.DEBUG, + () -> format("logging level %s", l)); + } + } + } + + public static Logger getLogger() { + return LOGGER; + } + + public static SettingsFrame updateDecoderSettings(SettingsFrame defaultSettingsFrame) { + SettingsFrame settingsFrame = defaultSettingsFrame; + settingsFrame.setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, DECODER_BLOCKED_STREAMS); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DECODER_MAX_TABLE_CAPACITY); + settingsFrame.setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, DECODER_MAX_FIELD_SECTION_SIZE); + return settingsFrame; + } + + private QPACK() { } + + /** + * The purpose of this logger is to provide means of diagnosing issues _in + * the QPACK implementation_. It's not a general purpose logger. + */ + // implements System.Logger to make it possible to skip this class + // when looking for the Caller. + public static class Logger implements System.Logger { + + /** + * Log detail level. + */ + public enum Level { + + NONE(0, System.Logger.Level.OFF), + NORMAL(1, System.Logger.Level.DEBUG), + EXTRA(2, System.Logger.Level.TRACE); + + private final int level; + final System.Logger.Level systemLevel; + + Level(int i, System.Logger.Level system) { + level = i; + systemLevel = system; + } + + public final boolean implies(Level other) { + return this.level >= other.level; + } + } + + private final String name; + private final Level level; + private final String path; + private final System.Logger logger; + + private Logger(String path, String name, Level level) { + this.path = path; + this.name = name; + this.level = level; + this.logger = Utils.getHpackLogger(path::toString, level.systemLevel); + } + + public final String getName() { + return name; + } + + @Override + public boolean isLoggable(System.Logger.Level level) { + return logger.isLoggable(level); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown) { + logger.log(level, bundle, msg,thrown); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params) { + logger.log(level, bundle, format, params); + } + + /* + * Usual performance trick for logging, reducing performance overhead in + * the case where logging with the specified level is a NOP. + */ + + public boolean isLoggable(Level level) { + return this.level.implies(level); + } + + public void log(Level level, Supplier s) { + if (this.level.implies(level)) { + logger.log(level.systemLevel, s); + } + } + + public Logger subLogger(String name) { + return new Logger(path + "/" + name, name, level); + } + + } + + private static final class RootLogger extends Logger { + + protected RootLogger(Level level) { + super("qpack", "qpack", level); + } + + } + + // -- low-level utilities -- + + /** + * An interface used to obtain the encoder or decoder stream pair + * from the enclosing HTTP/3 connection. + */ + @FunctionalInterface + public interface StreamPairSupplier { + QueuingStreamPair create(Consumer receiver); + } + + public interface QPACKErrorHandler { + void closeOnError(Throwable throwable, Http3Error error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java new file mode 100644 index 00000000000..6252a1b9549 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * Represents a QPack related failure as a failure cause and + * an HTTP/3 error code. + */ +public final class QPackException extends RuntimeException { + + @java.io.Serial + private static final long serialVersionUID = 8443631555257118370L; + + private final boolean isConnectionError; + private final Http3Error http3Error; + + public QPackException(Http3Error http3Error, Throwable cause, boolean isConnectionError) { + super(cause); + this.isConnectionError = isConnectionError; + this.http3Error = http3Error; + } + + public static QPackException encoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_ENCODER_STREAM_ERROR, cause, true); + } + + public static QPackException decoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_DECODER_STREAM_ERROR, cause, true); + } + + public static QPackException decompressionFailed(Throwable cause, boolean isConnectionError) { + throw new QPackException(Http3Error.QPACK_DECOMPRESSION_FAILED, cause, isConnectionError); + } + + + public Http3Error http3Error() { + return http3Error; + } + + public boolean isConnectionError() { + return isConnectionError; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java new file mode 100644 index 00000000000..52900a2d1c3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/* + * A header table with most common header fields. + * This table was generated by analyzing actual Internet traffic in 2018 + * and is a part of "QPACK: Header Compression for HTTP/3" RFC. + */ +public final class StaticTable implements HeadersTable { + + /* An immutable list of static header fields */ + public static final List HTTP3_HEADER_FIELDS = List.of( + new HeaderField(":authority"), + new HeaderField(":path", "/"), + new HeaderField("age", "0"), + new HeaderField("content-disposition"), + new HeaderField("content-length", "0"), + new HeaderField("cookie"), + new HeaderField("date"), + new HeaderField("etag"), + new HeaderField("if-modified-since"), + new HeaderField("if-none-match"), + new HeaderField("last-modified"), + new HeaderField("link"), + new HeaderField("location"), + new HeaderField("referer"), + new HeaderField("set-cookie"), + new HeaderField(":method", "CONNECT"), + new HeaderField(":method", "DELETE"), + new HeaderField(":method", "GET"), + new HeaderField(":method", "HEAD"), + new HeaderField(":method", "OPTIONS"), + new HeaderField(":method", "POST"), + new HeaderField(":method", "PUT"), + new HeaderField(":scheme", "http"), + new HeaderField(":scheme", "https"), + new HeaderField(":status", "103"), + new HeaderField(":status", "200"), + new HeaderField(":status", "304"), + new HeaderField(":status", "404"), + new HeaderField(":status", "503"), + new HeaderField("accept", "*/*"), + new HeaderField("accept", "application/dns-message"), + new HeaderField("accept-encoding", "gzip, deflate, br"), + new HeaderField("accept-ranges", "bytes"), + new HeaderField("access-control-allow-headers", "cache-control"), + new HeaderField("access-control-allow-headers", "content-type"), + new HeaderField("access-control-allow-origin", "*"), + new HeaderField("cache-control", "max-age=0"), + new HeaderField("cache-control", "max-age=2592000"), + new HeaderField("cache-control", "max-age=604800"), + new HeaderField("cache-control", "no-cache"), + new HeaderField("cache-control", "no-store"), + new HeaderField("cache-control", "public, max-age=31536000"), + new HeaderField("content-encoding", "br"), + new HeaderField("content-encoding", "gzip"), + new HeaderField("content-type", "application/dns-message"), + new HeaderField("content-type", "application/javascript"), + new HeaderField("content-type", "application/json"), + new HeaderField("content-type", "application/x-www-form-urlencoded"), + new HeaderField("content-type", "image/gif"), + new HeaderField("content-type", "image/jpeg"), + new HeaderField("content-type", "image/png"), + new HeaderField("content-type", "text/css"), + new HeaderField("content-type", "text/html; charset=utf-8"), + new HeaderField("content-type", "text/plain"), + new HeaderField("content-type", "text/plain;charset=utf-8"), + new HeaderField("range", "bytes=0-"), + new HeaderField("strict-transport-security", "max-age=31536000"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains; preload"), + new HeaderField("vary", "accept-encoding"), + new HeaderField("vary", "origin"), + new HeaderField("x-content-type-options", "nosniff"), + new HeaderField("x-xss-protection", "1; mode=block"), + new HeaderField(":status", "100"), + new HeaderField(":status", "204"), + new HeaderField(":status", "206"), + new HeaderField(":status", "302"), + new HeaderField(":status", "400"), + new HeaderField(":status", "403"), + new HeaderField(":status", "421"), + new HeaderField(":status", "425"), + new HeaderField(":status", "500"), + new HeaderField("accept-language"), + new HeaderField("access-control-allow-credentials", "FALSE"), + new HeaderField("access-control-allow-credentials", "TRUE"), + new HeaderField("access-control-allow-headers", "*"), + new HeaderField("access-control-allow-methods", "get"), + new HeaderField("access-control-allow-methods", "get, post, options"), + new HeaderField("access-control-allow-methods", "options"), + new HeaderField("access-control-expose-headers", "content-length"), + new HeaderField("access-control-request-headers", "content-type"), + new HeaderField("access-control-request-method", "get"), + new HeaderField("access-control-request-method", "post"), + new HeaderField("alt-svc", "clear"), + new HeaderField("authorization"), + new HeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), + new HeaderField("early-data", "1"), + new HeaderField("expect-ct"), + new HeaderField("forwarded"), + new HeaderField("if-range"), + new HeaderField("origin"), + new HeaderField("purpose", "prefetch"), + new HeaderField("server"), + new HeaderField("timing-allow-origin", "*"), + new HeaderField("upgrade-insecure-requests", "1"), + new HeaderField("user-agent"), + new HeaderField("x-forwarded-for"), + new HeaderField("x-frame-options", "deny"), + new HeaderField("x-frame-options", "sameorigin") + ); + + public static final StaticTable HTTP3 = new StaticTable(HTTP3_HEADER_FIELDS); + + private final List headerFields; + private final Map> indicesMap; + + private StaticTable(List headerFields) { + this.headerFields = headerFields; + this.indicesMap = buildIndicesMap(headerFields); + } + + @Override + public HeaderField get(long index) { + if (index >= headerFields.size()) { + throw new IllegalArgumentException("Invalid static table entry index"); + } + return headerFields.get((int)index); + } + + @Override + public long insert(String name, String value) { + throw new UnsupportedOperationException("Operation not supported by static tables"); + } + + @Override + public long search(String name, String value) { + Map values = indicesMap.get(name); + // 0 return value if no match is found in the static table + int searchResult = 0; + if (values != null) { + Integer idx = values.get(value); + if (idx != null) { + searchResult = idx + 1; + } else { + // Only name is found - return first id from indices for the name provided + searchResult = -values.values().iterator().next() - 1; + } + } + return searchResult; + } + + private static Map> buildIndicesMap(List fields) { + int numEntries = fields.size(); + Map> map = new HashMap<>(numEntries); + for (int i = 0; i < numEntries; i++) { + HeaderField f = fields.get(i); + Map values = map.computeIfAbsent(f.name(), _ -> new HashMap<>()); + values.put(f.value(), i); + } + return map; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java new file mode 100644 index 00000000000..6603c1d4c44 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.hpack.QuickHuffman; + +// Record containing table information for entry +public record TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, + EntryType type, boolean huffmanName, boolean huffmanValue) { + + public TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, EntryType type) { + this(isStaticTable, index, name, value, type, + isHuffmanBetterFor(name, true, type), + isHuffmanBetterFor(value, false, type)); + } + + public TableEntry toNewDynamicTableEntry(long index) { + return new TableEntry(false, index, name, value, EntryType.NAME_VALUE); + } + + public TableEntry relativizeDynamicTableEntry(long relativeIndex) { + assert !isStaticTable; + assert relativeIndex >= 0; + return new TableEntry(false, relativeIndex, name, value, type); + } + + public TableEntry(CharSequence name, CharSequence value) { + this(false, -1L, name, value, EntryType.NEITHER, + isHuffmanBetterFor(name, true, EntryType.NEITHER), + isHuffmanBetterFor(value, false, EntryType.NEITHER)); + } + + public TableEntry toLiteralsEntry() { + return new TableEntry(name, value); + } + + /** + * EntryType describes the type of TableEntry as either: + *

      + * - NAME_VALUE: a table entry where both name and value exist in table + * - NAME: a table entry where only name is present in table + * - NEITHER: a table entry where neither name nor value have been found + */ + public enum EntryType {NAME_VALUE, NAME, NEITHER} + + static boolean isHuffmanBetterFor(CharSequence str, boolean isName, EntryType type) { + return switch (type) { + case NEITHER -> QuickHuffman.isHuffmanBetterFor(str); + case NAME_VALUE -> false; + case NAME -> !isName && QuickHuffman.isHuffmanBetterFor(str); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java new file mode 100644 index 00000000000..5afe25d9781 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME_VALUE; + +/* + * Adds reverse lookup to dynamic and static tables. + * Decoder does not need this functionality. On the other hand, + * Encoder does. + */ +public final class TablesIndexer { + + private final DynamicTable dynamicTable; + private final StaticTable staticTable; + + public TablesIndexer(StaticTable staticTable, DynamicTable dynamicTable) { + this.dynamicTable = dynamicTable; + this.staticTable = staticTable; + } + + /** + * Searches in dynamic and static tables for an entry that has matching name + * or name:value. + * Found dynamic table entry ids are matched against provided + * known receive count value if it is non-negative. + * If known receive count value is negative the entry id check is + * not performed. + * + * @param name entry name to search + * @param value entry value to search + * @param knownReceivedCount known received count to match dynamic table + * entries, if negative - id check is not performed. + * @return a table entry that matches provided parameters + */ + public TableEntry entryOf(CharSequence name, CharSequence value, + long knownReceivedCount) { + // Invoking toString() will possibly allocate Strings for the sake of + // the searchDynamic, which doesn't feel right. + String n = name.toString(); + String v = value.toString(); + + // Tests can use -1 known receive count value to filter dynamic table + // entry ids. + boolean limitDynamicTableEntryIds = knownReceivedCount >= 0; + + // 1. Try exact match in the static table + var staticSearchResult = staticTable.search(n, v); + if (staticSearchResult > 0) { + // name:value pair is found in static table + return new TableEntry(true, staticSearchResult - 1, + name, value, NAME_VALUE); + } + // 2. Try exact match in the dynamic table + var dynamicSearchResult = dynamicTable.search(n, v); + if (dynamicSearchResult == 0 && staticSearchResult == 0) { + // dynamic and static tables do not contain name or name:value entries + // - use literal table entry + return new TableEntry(name, value); + } + long dtEntryId; + // name:value hit in dynamic table + if (dynamicSearchResult > 0) { + dtEntryId = dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, + NAME_VALUE); + } + } + // Name only hit in the static table + if (staticSearchResult < 0) { + return new TableEntry(true, -staticSearchResult - 1, name, + value, NAME); + } + + // Name only hit in the dynamic table + if (dynamicSearchResult < 0) { + dtEntryId = -dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, NAME); + } + } + + // No match found in the tables, or there is a dynamic table entry that has + // name or 'name:value' match but its index is greater than max allowed dynamic + // table index, ie the entry is not acknowledged by the decoder. + return new TableEntry(name, value); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java new file mode 100644 index 00000000000..d171e81dfbf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +/** + * QPACK (Header Compression for HTTP/3) implementation conforming to + * RFC 9204. + * + *

      Headers can be decoded and encoded by {@link jdk.internal.net.http.qpack.Decoder} + * and {@link jdk.internal.net.http.qpack.Encoder} respectively. + * + *

      Instances of these classes are not safe for use by multiple threads. + */ +package jdk.internal.net.http.qpack; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java new file mode 100644 index 00000000000..70ddace72ce --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPackException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECODER_STREAM_ERROR; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +/* + * Reader for decoder instructions described in RFC9204 + * "4.4 Encoder Instructions" section. + * Read instructions are passed to the consumer via the DecoderInstructionsReader.Callback + * instance supplied to the reader constructor. + */ +public class DecoderInstructionsReader { + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | Stream ID (7+) | + +---+---------------------------+ + */ + SECTION_ACKNOWLEDGMENT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | Stream ID (6+) | + +---+---+-----------------------+ + */ + STREAM_CANCELLATION, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | Increment (6+) | + +---+---+-----------------------+ + */ + INSERT_COUNT_INCREMENT + } + + private State state; + private final IntegerReader integerReader; + private final Callback callback; + private final Logger logger; + + public DecoderInstructionsReader(Callback callback, Logger logger) { + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECODER_STREAM_ERROR, true)); + this.callback = callback; + this.state = State.INIT; + this.logger = logger.subLogger("DecoderInstructionsReader"); + } + + public void read(ByteBuffer buffer) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + integerReader.reset(); + state = identifyDecoderInstruction(buffer); + break; + case INSERT_COUNT_INCREMENT, SECTION_ACKNOWLEDGMENT, STREAM_CANCELLATION: + // All decoder instructions consists of only one variable + // length integer field, therefore we fully read integer and + // then call the callback method depending on the state value + if (integerReader.read(buffer)) { + long value = integerReader.get(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Instruction: %s value: %s", + state.name(), value)); + } + // dispatch instruction to the consumer via the callback + dispatchParsedInstruction(value); + state = State.INIT; + } + break; + } + } + } + + private State identifyDecoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + integerReader.configure(7); + yield State.SECTION_ACKNOWLEDGMENT; + } + case 1 -> { + integerReader.configure(6); + yield State.STREAM_CANCELLATION; + } + default -> { + if ((b & 0b1100_0000) == 0) { + integerReader.configure(6); + yield State.INSERT_COUNT_INCREMENT; + } else { + throw QPackException.decoderStreamError( + new IOException("Unexpected decoder instruction: " + b)); + } + } + }; + } + + private void dispatchParsedInstruction(long value) { + switch (state) { + case INSERT_COUNT_INCREMENT: + callback.onInsertCountIncrement(value); + break; + case SECTION_ACKNOWLEDGMENT: + callback.onSectionAck(value); + break; + case STREAM_CANCELLATION: + callback.onStreamCancel(value); + break; + default: + throw QPackException.decoderStreamError( + new IOException("Unknown decoder instruction")); + } + } + + public interface Callback { + void onSectionAck(long streamId); + + void onStreamCancel(long streamId); + + void onInsertCountIncrement(long increment); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java new file mode 100644 index 00000000000..ff8b2868639 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.lang.System.Logger.Level.TRACE; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_ENCODER_STREAM_ERROR; + +/* + * Reader for encoder instructions defined in RFC9204 + * "4.3 Encoder Instructions" section. + * Read instruction is passed to the consumer via Callback + * interface supplied to the EncoderInstructionsReader constructor. + */ +public class EncoderInstructionsReader { + + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | Capacity (5+) | + +---+---+---+-------------------+ + */ + DT_CAPACITY, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Name Index (6+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_REF_NAME, + INSERT_NAME_REF_VALUE, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | H | Name Length (5+) | + +---+---+---+-------------------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_LIT_NAME, + INSERT_NAME_LIT_VALUE, + + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | Index (5+) | + +---+---+---+-------------------+ + */ + DUPLICATE + } + + private final QPACK.Logger logger; + private final Callback updateCallback; + private State state; + private final IntegerReader integerReader; + private final StringReader stringReader; + private int bitT = -1; + private long nameIndex = -1L; + private boolean huffmanValue; + private final StringBuilder valueString = new StringBuilder(); + + private boolean huffmanName; + private final StringBuilder nameString = new StringBuilder(); + + public EncoderInstructionsReader(Callback dtUpdateCallback, QPACK.Logger logger) { + this.logger = logger; + this.updateCallback = dtUpdateCallback; + this.state = State.INIT; + var errorToReport = new ReaderError(QPACK_ENCODER_STREAM_ERROR, true); + this.integerReader = new IntegerReader(errorToReport); + this.stringReader = new StringReader(errorToReport); + } + + public void read(ByteBuffer buffer, int maxStringLength) { + try { + read0(buffer, maxStringLength); + } catch (IllegalArgumentException | IllegalStateException exception) { + // "Duplicate" and "Insert With Name Reference" instructions can reference + // non-existing entries in the dynamic table. + // Such errors are treated as encoder stream errors. + throw QPackException.encoderStreamError(exception); + } + } + + private void read0(ByteBuffer buffer, int maxStringLength) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + state = identifyEncoderInstruction(buffer); + break; + case DT_CAPACITY: + if (integerReader.read(buffer)) { + long capacity = integerReader.get(); + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Dynamic Table Capacity update: %d", + capacity)); + } + updateCallback.onCapacityUpdate(integerReader.get()); + reset(); + } + break; + case INSERT_NAME_LIT_NAME: + if (stringReader.read(5, buffer, nameString, maxStringLength)) { + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + state = State.INSERT_NAME_LIT_VALUE; + } + break; + case INSERT_NAME_LIT_VALUE: + int stringReaderLimit = maxStringLength > 0 ? + Math.max(maxStringLength - nameString.length(), 0) : -1; + if (stringReader.read(buffer, valueString, stringReaderLimit)) { + huffmanValue = stringReader.isHuffmanEncoded(); + // Insert with literal name instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert with Literal Name ('%s','%s'," + + " huffmanName='%s', huffmanValue='%s')", nameString, + valueString, huffmanName, huffmanValue)); + } + updateCallback.onInsert(nameString.toString(), valueString.toString()); + reset(); + } + break; + case INSERT_NAME_REF_NAME: + if (integerReader.read(buffer)) { + nameIndex = integerReader.get(); + state = State.INSERT_NAME_REF_VALUE; + } + break; + case INSERT_NAME_REF_VALUE: + if (stringReader.read(buffer, valueString, maxStringLength)) { + // Insert with name reference instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert With Name Reference (T=%d, nameIdx=%d," + + " value='%s', valueHuffman='%s')", + bitT, nameIndex, valueString, stringReader.isHuffmanEncoded())); + } + updateCallback.onInsertIndexedName(bitT == 1, nameIndex, valueString.toString()); + reset(); + } + break; + case DUPLICATE: + if (integerReader.read(buffer)) { + updateCallback.onDuplicate(integerReader.get()); + reset(); + } + break; + } + } + } + + private State identifyEncoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + // Configure integer reader to read out name index and read the T bit + integerReader.configure(6); + bitT = (b & 0b0100_0000) == 0 ? 0 : 1; + yield State.INSERT_NAME_REF_NAME; + } + case 1 -> State.INSERT_NAME_LIT_NAME; + case 2 -> { + integerReader.configure(5); + yield State.DT_CAPACITY; + } + default -> { + boolean isDuplicateInstruction = (b & 0b1110_0000) == 0; + if (isDuplicateInstruction) { + integerReader.configure(5); + yield State.DUPLICATE; + } else { + throw QPackException.encoderStreamError( + new InternalError("Unexpected encoder instruction: " + b)); + } + } + }; + } + + public void reset() { + state = State.INIT; + bitT = -1; + nameIndex = -1L; + huffmanName = false; + huffmanValue = false; + resetBuffersAndReaders(); + } + + private void resetBuffersAndReaders() { + integerReader.reset(); + stringReader.reset(); + nameString.setLength(0); + valueString.setLength(0); + } + + public interface Callback { + void onCapacityUpdate(long capacity); + + void onInsert(String name, String value); + + void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString); + + void onDuplicate(long l); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java new file mode 100644 index 00000000000..ce2da3552fa --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedPostBaseReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + this.logger = logger; + } + + public void configure(int b) { + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long relativeIndex = integerReader.get(); + long absoluteIndex = prefix.base() + relativeIndex; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Post-Base Indexed Field Line: base=%s index=%s[%s]", + prefix.base(), relativeIndex, absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java new file mode 100644 index 00000000000..321a640bb5f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + integerReader = new IntegerReader( + new ReaderError(Http3Error.QPACK_DECOMPRESSION_FAILED, false)); + } + + public void configure(int b) { + integerReader.configure(6); + fromStaticTable = (b & 0b0100_0000) != 0; + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long intValue = integerReader.get(); + // "In a field line representation, a relative index of 0 refers to the + // entry with absolute index equal to Base - 1." + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("%s index %s", fromStaticTable ? "Static" : "Dynamic", + absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java new file mode 100644 index 00000000000..1dfa81b5631 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineLiteralsReader extends FieldLineReader { + private boolean hideIntermediary; + private boolean huffmanName, huffmanValue; + private final StringBuilder name, value; + private final StringReader stringReader; + private final QPACK.Logger logger; + private boolean firstValueRead = false; + + public FieldLineLiteralsReader(long maxSectionSize, AtomicLong sectionSizeTracker, + QPACK.Logger logger) { + // Dynamic table is not needed for literals reader + super(null, maxSectionSize, sectionSizeTracker); + this.logger = logger; + stringReader = new StringReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + name = new StringBuilder(512); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0001_0000) != 0; + } + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+-----------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + long readPart = ENTRY_SIZE + name.length() + value.length(); + checkPartialSize(readPart); + return false; + } + String n = name.toString(); + String v = value.toString(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with literal name ('%s', huffman=%b, '%s', huffman=%b)", + n, huffmanName, v, huffmanValue)); + } + checkSectionSize(DynamicTable.headerSize(n, v)); + action.onLiteralWithLiteralName(n, huffmanName, v, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!stringReader.read(3, input, name, getMaxFieldLineLimit(name.length()))) { + return false; + } + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + firstValueRead = true; + return false; + } else { + int maxLength = getMaxFieldLineLimit(name.length() + value.length()); + if (!stringReader.read(input, value, maxLength)) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + return true; + } + + public void reset() { + name.setLength(0); + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java new file mode 100644 index 00000000000..420844ad72d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameRefPostBaseReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameRefPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0000_1000) != 0; + integerReader.configure(3); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + + long absoluteIndex = prefix.base() + intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with post-base name reference (%s, %s, '%s', huffman=%b)", + absoluteIndex, prefix.base(), value, huffmanValue)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, + f.name(), valueStr, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java new file mode 100644 index 00000000000..f7e3d300628 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameReferenceReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameReferenceReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + fromStaticTable = (b & 0b0001_0000) != 0; + hideIntermediary = (b & 0b0010_0000) != 0; + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T | NameIndex (4+)| + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with name reference (%s, %s, '%s', huffman=%b)", + fromStaticTable ? "static" : "dynamic", intValue, value, huffmanValue)); + } + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, f.name(), valueStr, + huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java new file mode 100644 index 00000000000..da67f92bf19 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.StaticTable; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +sealed abstract class FieldLineReader permits FieldLineIndexedPostBaseReader, + FieldLineIndexedReader, FieldLineLiteralsReader, FieldLineNameRefPostBaseReader, + FieldLineNameReferenceReader { + + final long maxSectionSize; + boolean fromStaticTable; + private final AtomicLong sectionSizeTracker; + private final DynamicTable dynamicTable; + + FieldLineReader(DynamicTable dynamicTable, long maxSectionSize, AtomicLong sectionSizeTracker) { + this.maxSectionSize = maxSectionSize; + this.sectionSizeTracker = sectionSizeTracker; + this.dynamicTable = dynamicTable; + } + + abstract void reset(); + abstract void configure(int b); + abstract boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action); + + final void checkSectionSize(long fieldSize) { + long sectionSize = sectionSizeTracker.addAndGet(fieldSize); + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final void checkPartialSize(long partialFieldSize) { + long sectionSize = sectionSizeTracker.get() + partialFieldSize; + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final int getMaxFieldLineLimit(int partiallyRead) { + int maxLimit = -1; + if (maxSectionSize > 0) { + maxLimit = Math.clamp(maxSectionSize - partiallyRead - 32 - + sectionSizeTracker.get(), 0, Integer.MAX_VALUE); + } + return maxLimit; + } + + final int getMaxFieldLineLimit() { + return getMaxFieldLineLimit(0); + } + + private static QPackException maxFieldSectionExceeded(long sectionSize, long maxSize) { + throw QPackException.decompressionFailed( + new ProtocolException("Size exceeds MAX_FIELD_SECTION_SIZE: %s > %s" + .formatted(sectionSize, maxSize)), false); + } + + /** + * Checks if the decoder encounters a reference in a field line representation to + * a dynamic table entry that has already been evicted or that has an absolute index + * greater than or equal to the declared Required Insert Count (Section 4.5.1), + * it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + * @param absoluteIndex dynamic table absolute index + * @param prefix field line section prefix + */ + void checkEntryIndex(long absoluteIndex, FieldSectionPrefix prefix) { + if (!fromStaticTable && absoluteIndex >= prefix.requiredInsertCount()) { + throw QPackException.decompressionFailed( + new IOException("header index is greater than RIC"), true); + } + } + + /** + * Return a header field entry for the specified entry index. The table type + * is selected according to the {@code fromStaticTable} value. + * @param index absolute index of the table entry. + * @return a header field corresponding to the specified entry + */ + final HeaderField entryAtIndex(long index) { + HeaderField f; + try { + if (fromStaticTable) { + f = StaticTable.HTTP3.get(index); + } else { + assert dynamicTable != null; + f = dynamicTable.get(index); + } + } catch (IndexOutOfBoundsException | IllegalStateException | IllegalArgumentException e) { + throw QPackException.decompressionFailed( + new IOException("header fields table index", e), true); + } + return f; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java new file mode 100644 index 00000000000..a4ea55661fe --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +public class HeaderFrameReader { + + private enum State { + // Nothing has been read so-far, "Required Insert Count" (RIC) will be read next + INITIAL, + // "Required Insert Count" read is done, "S" and "Delta Base" are next + DELTA_BASE, + // Encoded Field Section Prefix read is done, ready to start reading header fields. + // In this state we only select a proper reader based on the field line encoding type, + // ie the first byte is analysed to select a proper reader. + SELECT_FIELD_READER, + INDEXED, + INDEX_WITH_POST_BASE, + LITERAL_WITH_LITERAL_NAME, + LITERAL_WITH_NAME_REF, + LITERAL_WITH_POST_BASE, + AWAITING_DT_INSERT_COUNT + } + + /* + 4.5.1. Encoded Field Section Prefix + Each encoded field section is prefixed with two integers. The Required Insert Count + is encoded as an integer with an 8-bit prefix using the encoding described in Section 4.5.1.1. + The Base is encoded as a Sign bit ('S') and a Delta Base value with a 7-bit prefix; + see Section 4.5.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | Required Insert Count (8+) | + +---+---------------------------+ + | S | Delta Base (7+) | + +---+---------------------------+ + */ + long requiredInsertCount; + long deltaBase; + int signBit; + volatile FieldSectionPrefix fieldSectionPrefix; + private final IntegerReader integerReader; + private FieldLineReader reader; + private final QPACK.Logger logger; + private final FieldLineIndexedReader indexedReader; + private final FieldLineIndexedPostBaseReader indexedPostBaseReader; + private final FieldLineNameReferenceReader literalWithNameReferenceReader; + private final FieldLineNameRefPostBaseReader literalWithNameRefPostBaseReader; + + private final FieldLineLiteralsReader literalWithLiteralNameReader; + // Need dynamic table reference for decoding field line section prefix + private final DynamicTable dynamicTable; + private final DecodingCallback decodingCallback; + + private volatile State state = State.INITIAL; + + private final SequentialScheduler headersScheduler = SequentialScheduler.lockingScheduler(this::readLoop); + private final ConcurrentLinkedQueue headersData = new ConcurrentLinkedQueue<>(); + + private final AtomicLong blockedStreamsCounter; + private final long maxBlockedStreams; + + // A tracker of header data received by the decoder, to check that the peer encoder + // honours the SETTINGS_MAX_FIELD_SECTION_SIZE value: + // RFC-9114: 4.2.2. Header Size Constraints + // "If an implementation wishes to advise its peer of this limit, it can + // be conveyed as a number of bytes in the SETTINGS_MAX_FIELD_SECTION_SIZE parameter. + // An implementation that has received this parameter SHOULD NOT send an HTTP message + // header that exceeds the indicated size" + // "A client can discard responses that it cannot process." + // + // Maximum allowed value is passed to FieldLineReader's implementations and not stored in + // HeaderFrameReader instance. + private final AtomicLong fieldSectionSizeTracker; + + private static final AtomicLong HEADER_FRAME_READER_IDS = new AtomicLong(); + + private void readLoop() { + try { + readLoop0(); + } catch (QPackException qPackException) { + Throwable cause = qPackException.getCause(); + if (qPackException.isConnectionError()) { + decodingCallback.onConnectionError(cause, qPackException.http3Error()); + } else { + decodingCallback.onStreamError(cause, qPackException.http3Error()); + } + } catch (Throwable throwable) { + decodingCallback.onConnectionError(throwable, H3_INTERNAL_ERROR); + } finally { + // Stop the scheduler, clear the reader's queue and + // remove all insert count notification events associated + // with current stream. + if (decodingCallback.hasError()) { + headersScheduler.stop(); + headersData.clear(); + dynamicTable.cleanupStreamInsertCountNotifications(decodingCallback.streamId()); + } + } + } + + private void readLoop0() { + ByteBuffer headerBlock; + OUTER: + while (!decodingCallback.hasError() && (headerBlock = headersData.peek()) != null) { + boolean endOfHeaderBlock = headerBlock == QuicStreamReader.EOF; + State state = this.state; + FieldSectionPrefix sectionPrefix = this.fieldSectionPrefix; + while (!decodingCallback.hasError() && headerBlock.hasRemaining()) { + if (state == State.SELECT_FIELD_READER) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + state = this.state = selectHeaderReaderState(b); + if (logger.isLoggable(EXTRA)) { + String message = format("next binary representation %s (first byte 0x%02x)", state, b); + logger.log(EXTRA, () -> message); + } + reader = switch (state) { + case INDEXED -> indexedReader; + case LITERAL_WITH_NAME_REF -> literalWithNameReferenceReader; + case LITERAL_WITH_LITERAL_NAME -> literalWithLiteralNameReader; + case INDEX_WITH_POST_BASE -> indexedPostBaseReader; + case LITERAL_WITH_POST_BASE -> literalWithNameRefPostBaseReader; + default -> throw QPackException.decompressionFailed( + new InternalError("Unexpected decoder state: " + state), false); + }; + reader.configure(b); + } else if (state == State.INITIAL) { + if (!integerReader.read(headerBlock)) { + continue; + } + // Required Insert Count was fully read + requiredInsertCount = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoded Required Insert Count = %d", requiredInsertCount)); + } + // Continue reading S and Delta Base values + state = this.state = State.DELTA_BASE; + // Reset integer reader + integerReader.reset(); + // Prepare it for reading S and Delta Base (7+) + integerReader.configure(7); + continue; + } else if (state == State.DELTA_BASE) { + if (signBit == -1) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + signBit = (b & 0b1000_0000) == 0b1000_0000 ? 1 : 0; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Base Sign = %d", signBit)); + } + } + if (!integerReader.read(headerBlock)) { + continue; + } + deltaBase = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Delta Base = %d", deltaBase)); + } + // Construct field section prefix from the parsed fields + sectionPrefix = this.fieldSectionPrefix = + FieldSectionPrefix.decode(requiredInsertCount, deltaBase, + signBit, dynamicTable); + + // Check if decoding of field section is blocked due to not yet received + // dynamic table entries + long insertCount = dynamicTable.insertCount(); + if (sectionPrefix.requiredInsertCount() > insertCount) { + long blocked = blockedStreamsCounter.incrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> "Blocked stream observed. Total blocked: " + blocked + + " Max allowed: " + maxBlockedStreams); + } + // System property value is checked here instead of the HTTP3 settings because decoder uses its + // value to update connection settings. HTTP client's encoder implementation won't block the streams - + // only acknowledged entry references is used, therefore this connection setting is not consulted + // on encoder side. + if (blocked > maxBlockedStreams) { + var ioException = new IOException(("too many blocked streams: current=%d; max=%d; " + + "prefixCount=%d; tableCount=%d").formatted(blocked, maxBlockedStreams, + sectionPrefix.requiredInsertCount(), insertCount)); + // If a decoder encounters more blocked streams than it promised to support, + // it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + throw QPackException.decompressionFailed(ioException, true); + } else { + CompletableFuture future = + dynamicTable.awaitFutureInsertCount(decodingCallback.streamId(), + sectionPrefix.requiredInsertCount()); + state = this.state = State.AWAITING_DT_INSERT_COUNT; + future.thenRun(this::onInsertCountUpdate); + } + break OUTER; + } + // The stream is unblocked - field lines can be decoded now + state = this.state = State.SELECT_FIELD_READER; + continue; + } else if (state == State.AWAITING_DT_INSERT_COUNT) { + // If we're waiting for a specific dynamic table update + return; + } + if (reader.read(headerBlock, sectionPrefix, decodingCallback)) { + // Finished reading of one header field line + state = this.state = State.SELECT_FIELD_READER; + } + } + if (!headerBlock.hasRemaining()) { + var head = headersData.poll(); + assert head == headerBlock; + } + if (endOfHeaderBlock) { + if (state == State.SELECT_FIELD_READER) { + decodingCallback.onComplete(); + } else { + logger.log(NORMAL, () -> "unexpected end of representation"); + throw QPackException.decompressionFailed( + new ProtocolException("Unexpected end of header block"), true); + } + } + } + } + + private void onInsertCountUpdate() { + long blocked = blockedStreamsCounter.decrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> "Stream Unblocked - number of blocked streams: " + blocked); + } + state = State.SELECT_FIELD_READER; + headersScheduler.runOrSchedule(); + } + + public HeaderFrameReader(DynamicTable dynamicTable, DecodingCallback callback, + AtomicLong blockedStreamsCounter, long maxBlockedStreams, + long maxFieldSectionSize, QPACK.Logger logger) { + this.blockedStreamsCounter = blockedStreamsCounter; + this.logger = logger.subLogger("HeaderFrameReader#" + + HEADER_FRAME_READER_IDS.incrementAndGet()); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("New HeaderFrameReader, dynamic table capacity = %s", + dynamicTable.capacity())); + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(NORMAL, () -> { + String hashCode = Integer.toHexString(System.identityHashCode(this)); + return format("toString='%s', identityHashCode=%s", this, hashCode); + }); + } + this.fieldSectionSizeTracker = new AtomicLong(); + indexedReader = new FieldLineIndexedReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedReader")); + indexedPostBaseReader = new FieldLineIndexedPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedPostBaseReader")); + literalWithNameReferenceReader = new FieldLineNameReferenceReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameReferenceReader")); + literalWithNameRefPostBaseReader = new FieldLineNameRefPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameRefPostBaseReader")); + literalWithLiteralNameReader = new FieldLineLiteralsReader( + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineLiteralsReader")); + integerReader = new IntegerReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + resetPrefixVars(); + // Since reader is constructed in Initial state - it means that the + // "Required Insert Count" will be read first. + integerReader.configure(8); + decodingCallback = callback; + this.dynamicTable = dynamicTable; + this.maxBlockedStreams = maxBlockedStreams; + } + + private void resetPrefixVars() { + requiredInsertCount = -1L; + deltaBase = -1L; + signBit = -1; + fieldSectionPrefix = null; + fieldSectionSizeTracker.set(0); + } + + public FieldSectionPrefix decodedSectionPrefix() { + if (deltaBase == -1L) { + throw new IllegalStateException("Field Section Prefix not parsed yet"); + } + return fieldSectionPrefix; + } + + public void read(ByteBuffer headerBlock, boolean endOfHeaderBlock) { + requireNonNull(headerBlock, "headerBlock"); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("reading %s, end of header block? %s", + headerBlock, endOfHeaderBlock)); + } + headersData.add(headerBlock); + if (endOfHeaderBlock) { + headersData.add(QuicStreamReader.EOF); + } + headersScheduler.runOrSchedule(); + } + + private State selectHeaderReaderState(int b) { + // First non-zero bit in lower 8 bits (see the caller) + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Index (6+) | + +---+---+-----------------------+ + */ + case 0 -> State.INDEXED; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | N | T |Name Index (4+)| + +---+---+---+---+---------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 1 -> State.LITERAL_WITH_NAME_REF; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | N | H |NameLen(3+)| + +---+---+---+---+---+-----------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 2 -> State.LITERAL_WITH_LITERAL_NAME; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | Index (4+) | + +---+---+---+---+---------------+ + */ + case 3 -> State.INDEX_WITH_POST_BASE; + // "Literal Field Line with Post-Base Name Reference": + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---------------+ + default -> { + if ((b & 0xF0) == 0) { + yield State.LITERAL_WITH_POST_BASE; + } + throw QPackException.decompressionFailed( + new IOException("Unknown frame reader line prefix: " + b), + false); + } + }; + } + + /** + * Reset the state of the HeaderFrameReader so that it's ready + * to parse a new HeaderFrame. + */ + public void reset() { + state = State.INITIAL; + reader = null; + resetPrefixVars(); + integerReader.reset(); + integerReader.configure(8); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java new file mode 100644 index 00000000000..35025c3f06a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static java.lang.String.format; + +/** + * This class able to decode integers up to and including 62 bits long values. + * https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + */ +public final class IntegerReader { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_READ = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int N; + private long maxValue; + private long value; + private long r; + private long b = 1; + private final ReaderError readError; + + public IntegerReader(ReaderError readError) { + this.readError = readError; + } + + public IntegerReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + // "QPACK implementations MUST be able to decode integers up to and including 62 bits long." + // https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + public static final long QPACK_MAX_INTEGER_VALUE = (1L << 62) - 1; + + public IntegerReader configure(int N) { + return configure(N, QPACK_MAX_INTEGER_VALUE); + } + + // + // Why is it important to configure 'maxValue' here. After all we can wait + // for the integer to be fully read and then check it. Can't we? + // + // Two reasons. + // + // 1. Value wraps around long won't be unnoticed. + // 2. It can spit out an exception as soon as it becomes clear there's + // an overflow. Therefore, no need to wait for the value to be fully read. + // + public IntegerReader configure(int N, long maxValue) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + checkPrefix(N); + if (maxValue < 0) { + throw new IllegalArgumentException( + "maxValue >= 0: maxValue=" + maxValue); + } + this.maxValue = maxValue; + this.N = N; + state = CONFIGURED; + return this; + } + + public boolean read(ByteBuffer input) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + int n = input.get() & max; + if (n != max) { + value = n; + state = DONE; + return true; + } else { + r = max; + } + state = FIRST_BYTE_READ; + } + if (state == FIRST_BYTE_READ) { + try { + // variable-length quantity (VLQ) + byte i; + boolean continuationFlag; + do { + if (!input.hasRemaining()) { + return false; + } + i = input.get(); + // RFC 7541: 5.1. Integer Representation + // "The most significant bit of each octet is used + // as a continuation flag: its value is set to 1 except + // for the last octet in the list" + continuationFlag = (i & 0b10000000) != 0; + long increment = Math.multiplyExact(b, i & 127); + if (continuationFlag) { + b = Math.multiplyExact(b, 128); + } + if (r > maxValue - increment) { + throw readError.toQPackException( + new IOException(format( + "Integer overflow: maxValue=%,d, value=%,d", + maxValue, r + increment))); + } + r += increment; + } while (continuationFlag); + value = r; + state = DONE; + return true; + } catch (ArithmeticException arithmeticException) { + // Sequence of bytes encodes value greater + // than QPACK_MAX_INTEGER_VALUE + throw readError.toQPackException(new IOException("Integer overflow", + arithmeticException)); + } + } + throw new InternalError(Arrays.toString( + new Object[]{state, N, maxValue, value, r, b})); + } + + public long get() throws IllegalStateException { + if (state != DONE) { + throw new IllegalStateException("Has not been fully read yet"); + } + return value; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerReader reset() { + b = 1; + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java new file mode 100644 index 00000000000..3be6033f827 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.QPackException; + +/** + * QPack readers configuration record to be used by the readers to + * report errors. + * @param http3Error corresponding HTTP/3 error code. + * @param isConnectionError if the reader error should be treated + * as connection error. + */ +record ReaderError(Http3Error http3Error, boolean isConnectionError) { + + /** + * Construct a {@link QPackException} from on {@code http3Error}, + * {@code isConnectionError} and provided {@code "cause"} values. + * @param cause cause of the constructed {@link QPackException} + * @return a {@code QPackException} instance. + */ + QPackException toQPackException(Throwable cause) { + return new QPackException(http3Error, cause, isConnectionError); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java new file mode 100644 index 00000000000..0cd0c92e91a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.readers; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.http3.Http3Error; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +public final class StringReader { + + private static final int NEW = 0; + private static final int FIRST_BYTE_READ = 1; + private static final int LENGTH_READ = 2; + private static final int DONE = 4; + + private final ReaderError readError; + private final IntegerReader intReader; + private final Huffman.Reader huffmanReader = new QuickHuffman.Reader(); + private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); + + private int state = NEW; + private boolean huffman; + private int remainingLength; + + public StringReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + public StringReader(ReaderError readError) { + this.readError = readError; + this.intReader = new IntegerReader(readError); + } + + public boolean read(ByteBuffer input, Appendable output, int maxLength) { + return read(7, input, output, maxLength); + } + + boolean read(int N, ByteBuffer input, Appendable output, int maxLength) { + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == NEW) { + int huffmanBit = switch (N) { + case 7 -> 0b1000_0000; // for all value strings + case 5 -> 0b0010_0000; // in name string for insert literal + case 3 -> 0b0000_1000; // in name string for literal + default -> throw new IllegalStateException("Unexpected value: " + N); + }; + int p = input.position(); + huffman = (input.get(p) & huffmanBit) != 0; + state = FIRST_BYTE_READ; + intReader.configure(N); + } + if (state == FIRST_BYTE_READ) { + boolean lengthRead = intReader.read(input); + if (!lengthRead) { + return false; + } + long remainingLengthLong = intReader.get(); + if (maxLength >= 0) { + long huffmanEstimate = huffman ? + remainingLengthLong / 4 : remainingLengthLong; + if (huffmanEstimate > maxLength) { + throw readError.toQPackException(new ProtocolException( + "Size exceeds MAX_FIELD_SECTION_SIZE or dynamic table capacity.")); + } + } + remainingLength = (int) remainingLengthLong; + state = LENGTH_READ; + } + if (state == LENGTH_READ) { + boolean isLast = input.remaining() >= remainingLength; + int oldLimit = input.limit(); + if (isLast) { + input.limit(input.position() + remainingLength); + } + remainingLength -= Math.min(input.remaining(), remainingLength); + try { + if (huffman) { + huffmanReader.read(input, output, isLast); + } else { + plainReader.read(input, output); + } + } catch (IOException ioe) { + throw readError.toQPackException(ioe); + } + if (isLast) { + input.limit(oldLimit); + state = DONE; + } + return isLast; + } + throw new InternalError(Arrays.toString( + new Object[]{state, huffman, remainingLength})); + } + + public boolean isHuffmanEncoded() { + if (state < FIRST_BYTE_READ) { + throw new IllegalStateException("Has not been fully read yet"); + } + return huffman; + } + + public void reset() { + if (huffman) { + huffmanReader.reset(); + } else { + plainReader.reset(); + } + intReader.reset(); + state = NEW; + } +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java similarity index 66% rename from test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java rename to src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java index 7e08fd24cf6..b028b994df2 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,16 +21,13 @@ * 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. - * */ +package jdk.internal.net.http.qpack.writers; -// Application which loads BoundMethodHandle species classes like the following: -// java/lang/invoke/BoundMethodHandle$Species_LLLL -import java.lang.management.ManagementFactory; +import java.nio.ByteBuffer; -public class AppWithBMH { - public static void main(String[] args) { - System.out.println("Hello world!"); - ManagementFactory.getGarbageCollectorMXBeans(); - } +interface BinaryRepresentationWriter { + boolean write(ByteBuffer destination); + + BinaryRepresentationWriter reset(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java new file mode 100644 index 00000000000..c27f46e2761 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class DecoderInstructionsWriter { + private final QPACK.Logger logger; + private boolean encoding; + private static final AtomicLong IDS = new AtomicLong(); + + private final IntegerWriter integerWriter = new IntegerWriter(); + + public DecoderInstructionsWriter() { + long id = IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("DecoderInstructionsWriter#" + id); + } + + /* + * Configure the writer for encoding "Section Acknowledgment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | Stream ID (7+) | + * +---+---------------------------+ + */ + public int configureForSectionAck(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 7, 0b1000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Section Acknowledgment for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(7, streamId); + } + + /* + * Configure the writer for encoding "Stream Cancellation" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | Stream ID (6+) | + * +---+---+-----------------------+ + */ + public int configureForStreamCancel(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 6, 0b0100_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Stream Cancellation for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(6, streamId); + } + + /* + * Configure the writer for encoding "Insert Count Increment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | Increment (6+) | + * +---+---+-----------------------+ + */ + public int configureForInsertCountInc(long increment) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(increment, 6, 0b0000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Insert Count Increment value=%s", + increment)); + } + return IntegerWriter.requiredBufferSize(6, increment); + } + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + boolean done = integerWriter.write(byteBuffer); + if (done) { + integerWriter.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java new file mode 100644 index 00000000000..29f6cac9ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDuplicateEntryWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDuplicateEntryWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDuplicateEntryWriter configure(long relativeId) { + // IntegerWriter.configure checks if the relative id value is not negative + intWriter.configure(relativeId, 5, 0b0000_0000); + // Need to store entry id for adding a duplicate to the dynamic table + // once write operation is completed + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java new file mode 100644 index 00000000000..1a920ff5b7b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDynamicTableCapacityWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDynamicTableCapacityWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDynamicTableCapacityWriter configure(long capacity) { + // IntegerWriter.configure checks if the capacity value is not negative + intWriter.configure(capacity, 5, 0b0010_0000); + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java new file mode 100644 index 00000000000..d1851a0c204 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class EncoderInsertIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public EncoderInsertIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Encoder Insert With %s Table Name Reference (%s, '%s', huffman=%b)", + e.isStaticTable() ? "Static" : "Dynamic", e.index(), e.value(), e.huffmanValue())); + } + return this.index(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertIndexedNameWriter index(TableEntry e) { + int N = 6; + int payload = 0b1000_0000; + long index = e.index(); + if (e.isStaticTable()) { + payload |= 0b0100_0000; + } + intWriter.configure(index, N, payload); + return this; + } + + private EncoderInsertIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java new file mode 100644 index 00000000000..1783f60b062 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class EncoderInsertLiteralNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + EncoderInsertLiteralNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Insert With Literal Name (%s, '%s', huffmanName=%b, huffmanValue=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue())); + } + return this.name(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertLiteralNameWriter name(TableEntry e) { + int N = 5; + int payload = 0b0100_0000; + if (e.huffmanName()) { + payload |= 0b0010_0000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private EncoderInsertLiteralNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java new file mode 100644 index 00000000000..c4a22436083 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class EncoderInstructionsWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final EncoderInsertIndexedNameWriter insertIndexedNameWriter; + private final EncoderInsertLiteralNameWriter insertLiteralNameWriter; + private final EncoderDuplicateEntryWriter duplicateWriter; + private final EncoderDynamicTableCapacityWriter capacityWriter; + private boolean encoding; + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + public EncoderInstructionsWriter() { + this(QPACK.getLogger()); + } + + public EncoderInstructionsWriter(QPACK.Logger parentLogger) { + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("EncoderInstructionsWriter#" + id); + // Writer for "Insert with Name Reference" encoder instruction + insertIndexedNameWriter = new EncoderInsertIndexedNameWriter( + logger.subLogger("EncoderInsertIndexedNameWriter")); + // Writer for "Insert with Literal Name" encoder instruction + insertLiteralNameWriter = new EncoderInsertLiteralNameWriter( + logger.subLogger("EncoderInsertLiteralNameWriter")); + // Writer for "Set Dynamic Table Capacity" encoder instruction + capacityWriter = new EncoderDynamicTableCapacityWriter(); + // Writer for "Duplicate" encoder instruction + duplicateWriter = new EncoderDuplicateEntryWriter(); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Insert with Name Reference" or "Insert with Literal Name" + * encoder instruction. The instruction is selected based on TableEntry.type() value: + * "Insert with Name Reference" is selected for TableEntry.EntryType.NAME: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | T | Name Index (6+) | + * +---+---+-----------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + * + * "Insert with Literal Name" is selected for TableEntry.EntryType.NEITHER: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | H | Name Length (5+) | + * +---+---+---+-------------------+ + * | Name String (Length bytes) | + * +---+---------------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + */ + public int configureForEntryInsertion(TableEntry e) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME -> insertIndexedNameWriter.configure(e); + case NEITHER -> insertLiteralNameWriter.configure(e); + default -> throw new IllegalArgumentException("Unsupported table entry insertion type: " + e.type()); + }; + return calculateEntryInsertionSize(e); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Duplicate" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 0 | Index (5+) | + * +---+---+---+-------------------+ + */ + public int configureForEntryDuplication(long entryIndexToDuplicate) { + checkIfEncodingInProgress(); + encoding = true; + duplicateWriter.configure(entryIndexToDuplicate); + writer = duplicateWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("duplicate entry with id=%s", entryIndexToDuplicate)); + } + return IntegerWriter.requiredBufferSize(5, entryIndexToDuplicate); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Set Dynamic Table Capacity" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 1 | Capacity (5+) | + * +---+---+---+-------------------+ + */ + public int configureForTableCapacityUpdate(long tableCapacity) { + checkIfEncodingInProgress(); + encoding = true; + capacityWriter.configure(tableCapacity); + writer = capacityWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("set dynamic table capacity to %s", tableCapacity)); + } + return IntegerWriter.requiredBufferSize(5, tableCapacity); + } + + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", byteBuffer)); + } + boolean done = writer.write(byteBuffer); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private int calculateEntryInsertionSize(TableEntry e) { + int vlen = Math.min(QuickHuffman.lengthOf(e.value()), e.value().length()); + int integerValuesSize; + return switch (e.type()) { + case NAME -> { + // Calculate how many bytes are needed to encode the index part: + // | 1 | T | Name Index (6+) | + integerValuesSize = IntegerWriter.requiredBufferSize(6, e.index()); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need vlen bytes for the value string content + yield integerValuesSize + vlen; + } + case NEITHER -> { + int nlen = Math.min(QuickHuffman.lengthOf(e.name()), e.name().length()); + // Calculate how many bytes are needed to encode the name length part: + // | 0 | 1 | H | Name Length (5+) | + integerValuesSize = IntegerWriter.requiredBufferSize(5, nlen); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need nlen + vlen bytes for the name and the value strings + // content + yield integerValuesSize + nlen + vlen; + } + default -> throw new IllegalArgumentException("Unsupported table entry type: " + e.type()); + }; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java new file mode 100644 index 00000000000..6e268f32acc --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + FieldLineIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + return e.isStaticTable() ? configureStatic(e, hideIntermediary) : + configureDynamic(e, hideIntermediary, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e, boolean hideIntermediary) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Static Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + e.index(), e.value(), e.huffmanValue(), hideIntermediary)); + } + return this.staticIndex(e.index(), hideIntermediary).value(e); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With %s Dynamic Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + usePostBase ? "Post-Base" : "", index, e.value(), e.huffmanValue(), + hideIntermediary)); + } + if (usePostBase) { + return this.dynamicPostBaseIndex(index, hideIntermediary).value(e); + } else { + return this.dynamicIndex(index, hideIntermediary).value(e); + } + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public FieldLineIndexedNameWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineIndexedNameWriter staticIndex(long absoluteIndex, boolean hideIntermediary) { + int payload = 0b0101_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(absoluteIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0100_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(relativeIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicPostBaseIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0000_0000; + if (hideIntermediary) { + payload |= 0b0000_1000; + } + intWriter.configure(relativeIndex, 3, payload); + return this; + } + + private FieldLineIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java new file mode 100644 index 00000000000..c829966980a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedWriter implements BinaryRepresentationWriter { + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + + public FieldLineIndexedWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, long base) { + return e.isStaticTable() ? configureStatic(e) : configureDynamic(e, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e) { + assert e.isStaticTable(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Static Table reference" + + " (%s, '%s', '%s')", e.index(), e.name(), e.value())); + } + return this.staticIndex(e.index()); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, long base) { + assert !e.isStaticTable(); + // RFC-9204: 3.2.6. Post-Base Indexing + // Post-Base indices are used in field line representations for entries with absolute + // indices greater than or equal to Base, starting at 0 for the entry with absolute index + // equal to Base and increasing in the same direction as the absolute index. + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Dynamic Table reference %s (%s[%s], '%s', '%s')", + usePostBase ? "with Post-Base Index" : "", index, e.index(), e.name(), e.value())); + } + if (usePostBase) { + return dynamicPostBaseIndex(index); + } else { + return dynamicIndex(index); + } + } + + @Override + public boolean write(ByteBuffer destination) { + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } + + private FieldLineIndexedWriter staticIndex(long absoluteIndex) { + int N = 6; + intWriter.configure(absoluteIndex, N, 0b1100_0000); + return this; + } + + private FieldLineIndexedWriter dynamicIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 6; + intWriter.configure(relativeIndex, N, 0b1000_0000); + return this; + } + + private FieldLineIndexedWriter dynamicPostBaseIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 4; + intWriter.configure(relativeIndex, N, 0b0001_0000); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java new file mode 100644 index 00000000000..42187c256d7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class FieldLineLiteralsWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public FieldLineLiteralsWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Name and Value Literals ('%s', '%s', huffmanName=%b, huffmanValue=%b, hideIntermediary=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue(), hideIntermediary)); + } + return this.name(e, hideIntermediary).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineLiteralsWriter name(TableEntry e, boolean hideIntermediary) { + int N = 3; + int payload = 0b0010_0000; + if (hideIntermediary) { + payload |= 0b0001_0000; + } + if (e.huffmanName()) { + payload |= 0b0000_1000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private FieldLineLiteralsWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java new file mode 100644 index 00000000000..18e1d1e2676 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.FieldSectionPrefix; + +import java.nio.ByteBuffer; + +public class FieldLineSectionPrefixWriter { + enum State {NEW, CONFIGURED, RIC_WRITTEN, DONE} + + private final IntegerWriter intWriter; + private State state = State.NEW; + private long encodedRic; + private int signBit; + private long deltaBase; + + public FieldLineSectionPrefixWriter() { + this.intWriter = new IntegerWriter(); + } + + private void encodeFieldSectionPrefixFields(FieldSectionPrefix fsp, long maxEntries) { + // Required Insert Count encoded according to RFC-9204 "4.5.1.1: Required Insert Count" + // Base and Sign encoded according to RFC-9204: "4.5.1.2. Base" + long ric = fsp.requiredInsertCount(); + long base = fsp.base(); + + if (ric == 0) { + encodedRic = 0; + deltaBase = 0; + signBit = 0; + } else { + encodedRic = (ric % (2 * maxEntries)) + 1; + signBit = base >= ric ? 0 : 1; + deltaBase = base >= ric ? base - ric : ric - base - 1; + } + } + + public int configure(FieldSectionPrefix sectionPrefix, long maxEntries) { + intWriter.reset(); + encodeFieldSectionPrefixFields(sectionPrefix, maxEntries); + intWriter.configure(encodedRic, 8, 0); + state = State.CONFIGURED; + return IntegerWriter.requiredBufferSize(8, encodedRic) + + IntegerWriter.requiredBufferSize(7, deltaBase); + } + + public boolean write(ByteBuffer destination) { + if (state == State.NEW) { + throw new IllegalStateException("Configure first"); + } + + if (state == State.CONFIGURED) { + if (!intWriter.write(destination)) { + return false; + } + // Required Insert Count part is written, + // prepare integer writer for delta base and + // base sign write + intWriter.reset(); + int signPayload = signBit == 1 ? 0b1000_0000 : 0b0000_0000; + intWriter.configure(deltaBase, 7, signPayload); + state = State.RIC_WRITTEN; + } + + if (state == State.RIC_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = State.DONE; + } + return state == State.DONE; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java new file mode 100644 index 00000000000..3841ca7c5ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class HeaderFrameWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final FieldLineIndexedWriter indexedWriter; + private final FieldLineIndexedNameWriter literalWithNameReferenceWriter; + private final FieldLineLiteralsWriter literalWithLiteralNameWriter; + private boolean encoding; + private static final AtomicLong HEADER_FRAME_WRITER_IDS = new AtomicLong(); + + public HeaderFrameWriter() { + this(QPACK.getLogger()); + } + + public HeaderFrameWriter(QPACK.Logger parentLogger) { + long id = HEADER_FRAME_WRITER_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("HeaderFrameWriter#" + id); + + indexedWriter = new FieldLineIndexedWriter(logger.subLogger("FieldLineIndexedWriter")); + literalWithNameReferenceWriter = new FieldLineIndexedNameWriter( + logger.subLogger("FieldLineIndexedNameWriter")); + literalWithLiteralNameWriter = new FieldLineLiteralsWriter( + logger.subLogger("FieldLineLiteralsWriter")); + } + + public void configure(TableEntry e, boolean sensitive, long base) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME_VALUE -> indexedWriter.configure(e, base); + case NAME -> literalWithNameReferenceWriter.configure(e, sensitive, base); + case NEITHER -> literalWithLiteralNameWriter.configure(e, sensitive); + }; + } + + /** + * Writes the {@linkplain #configure(TableEntry, boolean, long) + * set up} header into the given buffer. + * + *

      The method writes as much as possible of the header's binary + * representation into the given buffer, starting at the buffer's position, + * and increments its position to reflect the bytes written. The buffer's + * mark and limit will not be modified. + * + *

      Once the method has returned {@code true}, the configured header is + * deemed encoded. A new header may be set up. + * + * @param headerFrame the buffer to encode the header into, may be empty + * @return {@code true} if the current header has been fully encoded, + * {@code false} otherwise + * @throws NullPointerException if the buffer is {@code null} + * @throws IllegalStateException if there is no set up header + */ + public boolean write(ByteBuffer headerFrame) { + if (!encoding) { + throw new IllegalStateException("A header hasn't been set up"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", headerFrame)); + } + boolean done = writer.write(headerFrame); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException("Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java new file mode 100644 index 00000000000..f042135b4a1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public final class IntegerWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_WRITTEN = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int payload; + private int N; + private long value; + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | | | | | | | | | + // +---+---+---+-------------------+ + // |<--------->|<----------------->| + // payload N=5 + // + // payload is the contents of the left-hand side part of the first octet; + // it is truncated to fit into 8-N bits, where 1 <= N <= 8; + // + public IntegerWriter configure(long value, int N, int payload) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + if (value < 0) { + throw new IllegalArgumentException("value >= 0: value=" + value); + } + checkPrefix(N); + this.value = value; + this.N = N; + this.payload = payload & 0xFF & (0xFFFFFFFF << N); + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + if (value < max) { + output.put((byte) (payload | value)); + state = DONE; + return true; + } + output.put((byte) (payload | max)); + value -= max; + state = FIRST_BYTE_WRITTEN; + } + if (state == FIRST_BYTE_WRITTEN) { + while (value >= 128 && output.hasRemaining()) { + output.put((byte) ((value & 127) + 128)); + value /= 128; + } + if (!output.hasRemaining()) { + return false; + } + output.put((byte) value); + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, payload, N, value})); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public static int requiredBufferSize(int N, long value) { + checkPrefix(N); + int size = 1; + int max = (2 << (N - 1)) - 1; + if (value < max) { + return size; + } + size++; + value -= max; + while (value >= 128) { + value /= 128; + size++; + } + return size; + } + + public IntegerWriter reset() { + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java new file mode 100644 index 00000000000..298c3d8f9c1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +// StringWriter does not require a notion of endOfInput (isLast) in 'write' +// methods due to the nature of string representation in HPACK. Namely, the +// length of the string is put before string's contents. Therefore the length is +// always known beforehand. +// +// Expected use: +// +// configure write* (reset configure write*)* +// +public final class StringWriter { + private static final int DEFAULT_PREFIX = 7; + private static final int DEFAULT_PAYLOAD = 0b0000_0000; + private static final int HUFFMAN_PAYLOAD = 0b1000_0000; + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int LENGTH_WRITTEN = 2; + private static final int DONE = 4; + + private final IntegerWriter intWriter = new IntegerWriter(); + private final Huffman.Writer huffmanWriter = new QuickHuffman.Writer(); + private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); + + private int state = NEW; + private boolean huffman; + + public StringWriter configure(CharSequence input, boolean huffman) { + return configure(input, 0, input.length(), DEFAULT_PREFIX, huffman ? HUFFMAN_PAYLOAD : DEFAULT_PAYLOAD, huffman); + } + + public StringWriter configure(CharSequence input, int N, int payload, boolean huffman) { + return configure(input, 0, input.length(), N, payload, huffman); + } + + StringWriter configure(CharSequence input, + int start, + int end, + int N, + int payload, + boolean huffman) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + if (!huffman) { + plainWriter.configure(input, start, end); + intWriter.configure(end - start, N, payload); + } else { + huffmanWriter.from(input, start, end); + intWriter.configure(huffmanWriter.lengthOf(input, start, end), N, payload); + } + + this.huffman = huffman; + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == DONE) { + return true; + } + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + if (intWriter.write(output)) { + state = LENGTH_WRITTEN; + } else { + return false; + } + } + if (state == LENGTH_WRITTEN) { + boolean written = huffman + ? huffmanWriter.write(output) + : plainWriter.write(output); + if (written) { + state = DONE; + return true; + } else { + return false; + } + } + throw new InternalError(Arrays.toString(new Object[]{state, huffman})); + } + + public void reset() { + intWriter.reset(); + if (huffman) { + huffmanWriter.reset(); + } else { + plainWriter.reset(); + } + state = NEW; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java new file mode 100644 index 00000000000..9f2ebf17264 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A class that allows to read data from an aggregation of {@code ByteBuffer}. + * This is mostly geared to reading Quic or HTTP/3 frames that are composed + * of an aggregation of {@linkplain VariableLengthEncoder Variable Length Integers}. + * This class is not multi-thread safe. + *

      + * The {@code BuffersReader} class is an abstract class with two concrete + * implementations: {@link SingleBufferReader} and {@link ListBuffersReader}. + *

      + * The {@link SingleBufferReader} presents a simple lightweight view of a single + * {@link ByteBuffer}. Instances of {@code SingleBufferReader} can be created by + * calling {@link BuffersReader#single(ByteBuffer) BuffersReader.single(buffer)}; + *

      + * The {@link ListBuffersReader} view can be created from a (possibly empty) + * list of byte buffers. New byte buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} to the {@link ListBuffersReader} instance + * as they become available. Once a frame has been fully received, + * {@link BuffersReader#release()} or {@link BuffersReader#getAndRelease(long)} should + * be called to forget and relinquish all bytes buffers up to the current + * {@linkplain #position() position} of the {@code BuffersReader}. + * Released buffers are removed from {@code BuffersReader} list, and the position + * of the reader is reset to 0, allowing to read the next frame from the remaining + * data. + */ +public abstract sealed class BuffersReader { + + /** + * Release all buffers held by this {@code BuffersReader}, whether + * consumed or unconsumed. Released buffer are all set to their + * limit. + */ + public abstract void clear(); + + // Used to store the original position and limit of a + // buffer at the time it's added to the reader's list + // It is not possible to beyond that position or limit when + // using the reader + private record Buffer(ByteBuffer buffer, int offset, int limit) { + Buffer { + assert offset <= limit; + assert offset >= 0; + assert limit == buffer.limit(); + } + Buffer(ByteBuffer buffer) { + this(buffer, buffer.position(), buffer.limit()); + } + } + + /** + * {@return the current position of the reader} + * The semantic is similar to {@link ByteBuffer#position()}. + */ + public abstract long position(); + + /** + * {@return the limit of the reader} + * The semantic is similar to {@link ByteBuffer#limit()}. + */ + public abstract long limit(); + + /** + * Reads one byte from the reader. This method increase + * the position by one. The semantic is similar to + * {@link ByteBuffer#get()}. + * @return the byte at the current position + * @throws BufferUnderflowException if trying to read past + * the limit. + */ + public abstract byte get(); + + /** + * Reads the byte located at the given position in the + * reader. The semantic is similar to {@link ByteBuffer#get(int)}. + * This method doesn't change the position of the reader. + * + * @param position the position of the byte + * @return the byte at the given position in the reader + * + * @throws IndexOutOfBoundsException if trying to read before + * the reader's position or after the reader's limit + */ + public abstract byte get(long position); + + /** + * Sets the position of the reader. + * The semantic is similar to {@link ByteBuffer#position(int)}. + * + * @param newPosition the new position + * + * @throws IllegalArgumentException if trying to set + * the position to a negative value, or to a value + * past the limit + */ + public abstract void position(long newPosition); + + /** + * Releases all the data that has been read, sets the + * reader's position to 0 and its limit to the amount + * of data remaining. + */ + public abstract void release(); + + /** + * Returns a list of {@code ByteBuffer} containing the + * requested amount of bytes, starting at the current + * position, then release all the data up to the new + * position, and reset the reader's position to 0 and + * the reader's limit to the amount of remaining data. + * . + * @param bytes the amount of bytes to read and move + * to the returned list. + * + * @return a list of {@code ByteBuffer} containing the next + * {@code bytes} of data, starting at the current position. + * + * @throws BufferUnderflowException if attempting to read past + * the limit + */ + public abstract List getAndRelease(long bytes); + + /** + * {@return true if the reader has remaining bytes to the read} + * The semantic is similar to {@link ByteBuffer#hasRemaining()}. + */ + public boolean hasRemaining() { + return position() < limit(); + } + + /** + * {@return the number of bytes that remain to read} + * The semantic is similar to {@link ByteBuffer#remaining()}. + */ + public long remaining() { + long rem = limit() - position(); + return rem > 0 ? rem : 0; + } + + /** + * {@return the cumulated amount of data that has been read in this + * {@code BuffersReader} since its creation} + * This number is not reset when calling {@link #release()}. + */ + public abstract long read(); + + /** + * {@return The offset of this {@code BuffersReader}} + * This is the position in the first {@code ByteBuffer} that + * was set on the reader. The {@code BuffersReader} will not + * allow to get or set a position lower than the offset. + */ + public abstract long offset(); + + /** + * {@return true if this {@code BuffersReader} is empty} + * A {@code BuffersReader} is empty if it has been {@linkplain + * #list() created empty, or if it has been {@linkplain #release() + * released} after all data has been read. + */ + public abstract boolean isEmpty(); + + /** + * A lightweight view allowing to see a {@link ByteBuffer} as a + * {@link BuffersReader}. This class wrap a single {@link ByteBuffer} + * and cannot be reused after {@link #release()}. + */ + public static final class SingleBufferReader extends BuffersReader { + ByteBuffer single; + long read = 0; + long start; + SingleBufferReader(ByteBuffer single) { + this.single = single; + start = single.position(); + } + + @Override + public void release() { + single = null; + } + + @Override + public List getAndRelease(long bytes) { + return List.of(getAndReleaseBuffer(bytes)); + } + + @Override + public byte get() { + if (single == null) throw new BufferUnderflowException(); + return single.get(); + } + + @Override + public byte get(long position) { + if (single == null || position < start || position >= single.limit()) + throw new IndexOutOfBoundsException(); + return single.get((int) position); + } + + @Override + public long limit() { + return single == null ? 0 : single.limit(); + } + + @Override + public long position() { + return single == null ? 0 : single.position(); + } + + @Override + public boolean hasRemaining() { + return single != null && single.hasRemaining(); + } + + @Override + public void position(long pos) { + if (single == null || pos < start || pos > single.limit()) + throw new BufferUnderflowException(); + single.position((int) pos); + } + + /** + * This method has the same semantics than {@link #getAndRelease(long)} + * except that it avoids creating a list. + * @return a buffer containing the next {@code bytes}. + */ + public ByteBuffer getAndReleaseBuffer(long bytes) { + var released = single; + int remaining = released.remaining(); + if (bytes > remaining) + throw new BufferUnderflowException(); + if (bytes == remaining) { + read = single.limit() - start; + single = null; + } else { + read = single.position() - start; + single = released.slice(released.position() + (int)bytes, released.limit()); + start = 0; + released = released.slice(released.position(), (int) bytes); + } + return released; + } + + @Override + public long read() { + return single == null ? read : (read + single.position() - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public boolean isEmpty() { + return single == null; + } + + @Override + public void clear() { + if (single == null) return; + single.position(single.limit()); + single = null; + } + } + + /** + * A {@code BuffersReader} that iterates over a list of {@code ByteBuffers}. + * New {@code ByteBuffers} can be added at the end of list by calling + * {@link #add(ByteBuffer)} or {@link #addAll(List)}, which increases + * the {@linkplain #limit() limit} accordingly. + *

      + * When {@link #release() released}, the data prior to the current + * {@linkplain #position()} is discarded, the {@linkplain #position() position} + * and {@linkplain #offset() offset} are reset to {@code 0}, and the + * {@linkplain #limit() limit} is set to the amount of remaining data. + *

      + * A {@code ListBuffersReader} can be reused after being released. + * If it still contains data, the {@linkplain #offset() offset} will + * be {@code 0}. Otherwise, the offset will be set to the position + * of the first buffer {@linkplain #add(ByteBuffer) added} to the + * {@code ListBuffersReader}. + */ + public static final class ListBuffersReader extends BuffersReader { + private final List buffers = new ArrayList<>(); + private Buffer current; + private int nextIndex; + private long currentOffset; + private long position; + private long limit; + private long start; + private long readAndReleased = 0; + + ListBuffersReader() { + } + + /** + * Adds a new {@code ByteBuffer} to this {@code BuffersReader}. + * If the reader is {@linkplain #isEmpty() empty}, the reader's + * {@linkplain #offset() offset} and {@linkplain #position() position} + * is set to the buffer's position, and the reader {@linkplain #limit() + * limit} is set to the buffer's limit. + * Otherwise, the reader's limit is simply increased by the buffer's + * remaining bytes. The reader will only allow to read those bytes + * between the current position and limit of the buffer. + * + * @apiNote + * This class doesn't make defensive copies of the provided buffers, + * so the caller must not modify the buffer's position or limit + * after it's been added to the reader. + * + * @param buffer a byte buffer + * @return this reader + */ + public ListBuffersReader add(ByteBuffer buffer) { + if (buffers.isEmpty()) { + int lim = buffer.limit(); + buffers.add(new Buffer(buffer, 0, lim)); + start = buffer.position(); + position = limit = start; + currentOffset = 0; + } else { + buffers.add(new Buffer(buffer)); + } + limit += buffer.remaining(); + return this; + } + + /** + * Adds a list of byte buffers to this reader. + * This is equivalent to calling: + * {@snippet : + * ListBuffersReader reader = ...; + * for (var buffer : buffers) { + * reader.add(buffer); // @link substring="add" target="#add(ByteBuffer)" + * } + * } + * @param buffers a list of {@link ByteBuffer ByteBuffers} + * @return this reader + */ + public ListBuffersReader addAll(List buffers) { + for (var buffer : buffers) { + if (isEmpty()) { + add(buffer); + continue; + } + this.buffers.add(new Buffer(buffer)); + limit += buffer.remaining(); + } + return this; + } + + @Override + public boolean isEmpty() { + return buffers.isEmpty(); + } + + @Override + public byte get() { + ByteBuffer buffer = current(true); + byte res = buffer.get(); + position++; + return res; + } + + @Override + public byte get(long pos) { + if (pos >= limit || pos < start) + throw new IndexOutOfBoundsException(); + ByteBuffer buffer = current(false); + if (position == limit && current != null) { + // let the current buffer throw + buffer = current.buffer; + } + assert buffer != null : "limit check failed"; + if (pos == position) { + return buffer.get(buffer.position()); + } + long offset = currentOffset; + int index = nextIndex; + Buffer cur = current; + while (pos >= offset) { + int bpos = buffer.position(); + int boffset = cur.offset; + int blimit = buffer.limit(); + assert index == nextIndex || bpos == boffset; + if (pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + if (index >= buffers.size()) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + int skipped = cur.limit - cur.offset; + offset += skipped; + cur = buffers.get(index++); + buffer = cur.buffer; + } + assert pos <= offset; + int blimit = cur.offset; + int boffset = cur.offset; + while (pos < offset) { + assert blimit == cur.limit || index == nextIndex && blimit == boffset; + if (index <= 1) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + cur = buffers.get(--index - 1); + buffer = cur.buffer; + int bpos = buffer.position(); + blimit = buffer.limit(); + boffset = cur.offset; + int skipped = blimit - boffset; + offset -= skipped; + assert index == nextIndex || bpos == blimit; + if (pos - offset >= 0 && pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + } + assert false : "buffer not found"; + throw new IndexOutOfBoundsException(); // should not reach here + } + + /** + * {@return the current {@code ByteBuffer} in which to find + * the byte at the current {@link #position()}} + * + * @param throwIfUnderflow if true, calling this method + * will throw {@link BufferUnderflowException} if + * the position is past the limit. + * + * @throws BufferUnderflowException if attempting to read past + * the limit and {@code throwIfUnderflow == true} + */ + private ByteBuffer current(boolean throwIfUnderflow) { + while (current == null || !current.buffer.hasRemaining()) { + if (buffers.size() > nextIndex) { + if (nextIndex != 0) { + currentOffset = position; + } else { + currentOffset = 0; + } + current = buffers.get(nextIndex++); + } else if (throwIfUnderflow) { + throw new BufferUnderflowException(); + } else { + return null; + } + } + return current.buffer; + } + + @Override + public List getAndRelease(long bytes) { + release(); + if (bytes > limit - position) { + throw new BufferUnderflowException(); + } + ByteBuffer buf = current(false); + if (buf == null || bytes == 0) return List.of(); + List list = null; + assert position == 0; + assert currentOffset == 0; + while (bytes > 0) { + buf = current(false); + assert nextIndex == 1; + assert buf != null; + assert buf.position() == current.offset; + int remaining = buf.remaining(); + if (remaining <= bytes) { + var b = buffers.remove(--nextIndex); + assert b == current; + long relased = buf.remaining(); + assert b.buffer.limit() == b.limit; + bytes -= relased; + limit -= relased; + readAndReleased += relased; + current = null; + + // if a buffer has no remaining bytes it + // may be EOF. Let's not skip it here + // if (!buf.hasRemaining()) continue; + + if (bytes == 0 && list == null) { + list = List.of(buf); + } else { + if (list == null) { + list = new ArrayList<>(); + } + list.add(buf); + } + } else { + var b = current; + long relased = bytes; + bytes = 0; + limit -= relased; + var pos = buf.position(); + assert b.limit == buf.limit(); + assert pos == b.offset; + var slice = buf.slice(pos, (int)relased); + buf.position(pos + (int) relased); + buffers.set(nextIndex - 1, current = new Buffer(buf)); + readAndReleased += relased; + if (list != null) { + list.add(slice); + } else { + list = List.of(slice); + } + assert bytes == 0; + } + } + return list; + } + + @Override + public long position() { + return position; + } + + @Override + public long limit() { + return limit; + } + + @Override + public void release() { + long released = - start; + for (var it = buffers.listIterator(); it.hasNext(); ) { + var b = it.next(); + var buf = b.buffer; + released += (buf.position() - b.offset); + if (buf.hasRemaining()) { + it.set(new Buffer(buf)); + break; + } + it.remove(); + } + assert released == position - start + : "start=%s, position=%s, released=%s" + .formatted(start, position, released); + readAndReleased += released; + limit -= position; + current = null; + position = 0; + currentOffset = 0; + nextIndex = 0; + start = 0; + } + + @Override + public void position(long pos) { + if (pos > limit) throw new IllegalArgumentException(pos + " > " + limit); + if (pos < start) throw new IllegalArgumentException(pos + " < " + start); + if (pos == position) return; // happy case! + // look forward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + if (pos > position) { + long skip = pos - position; + assert skip > 0; + while (skip > 0) { + var buffer = current(true); + int remaining = buffer.remaining(); + if (remaining == 0) continue; + if (skip > remaining) { + // somewhere after the current buffer + buffer.position(buffer.limit()); + position += remaining; + skip -= remaining; + } else { + // somewhere in the current buffer + buffer.position(buffer.position() + (int) skip); + position += skip; + skip = 0; + } + } + } else { + // look backward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + long skip = pos - position; + assert skip < 0; + if (current == null) { + current(false); + if (current == null) + throw new IllegalArgumentException(); + } + while (skip < 0) { + var buffer = current.buffer; + assert buffer.limit() == current.limit; + var remaining = buffer.position() - current.offset; + var rest = skip + remaining; + if (rest >= 0) { + // somewhere in this byte buffer, between the + // buffer offset and the buffer position + buffer.position(buffer.position() + (int)skip); + position += skip; + assert position >= start; + skip = 0; + } else { + // in some buffer prior to the current byte buffer + buffer.position(current.offset); + skip += remaining; + position -= remaining; + assert skip < 0; + assert position >= start; + assert nextIndex > 1; + current = buffers.get(--nextIndex - 1); + currentOffset -= current.limit - current.offset; + assert currentOffset >= 0; + assert current.buffer.position() == current.limit; + } + } + } + } + + @Override + public long read() { + return readAndReleased + (position - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public void clear() { + release(); + position(limit()); + release(); + } + } + + /** + * Creates a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer}. + * @param buffer a byte buffer + * @return a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer} + */ + public static SingleBufferReader single(ByteBuffer buffer) { + return new SingleBufferReader(Objects.requireNonNull(buffer)); + } + + /** + * Creates an {@linkplain #isEmpty() empty} {@link ListBuffersReader}. + * @return an empty {@code ListBuffersReader} + */ + public static ListBuffersReader list() { + return new ListBuffersReader(); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffer}. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(ByteBuffer buffer) { + return new ListBuffersReader().add(buffer); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffers} list. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(List buffers) { + return new ListBuffersReader().addAll(buffers); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java new file mode 100644 index 00000000000..d2daa6fadaf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public interface CodingContext { + + /** + * {@return the largest incoming packet number successfully processed + * in the given packet number space} + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetSpace the packet number space + */ + long largestProcessedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the given packet number space} + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetSpace the packet number space + */ + long largestAckedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the length of the local connection ids expected + * to be found in incoming short header packets} + */ + int connectionIdLength(); + + /** + * {@return the largest incoming packet number successfully processed + * in the packet number space corresponding to the given packet type} + *

      + * This is equivalent to calling:

      +     *     {@code largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType));}
      +     * 
      + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetType the packet type + */ + default long largestProcessedPN(QuicPacket.PacketType packetType) { + return largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the packet number space corresponding to the given packet type} + *

      + * This is equivalent to calling:

      +     *     {@code largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType));}
      +     * 
      + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetType the packet type + */ + default long largestAckedPN(QuicPacket.PacketType packetType) { + return largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * Writes the given outgoing packet in the given byte buffer. + * This method moves the position of the byte buffer. + * @param packet the outgoing packet to write + * @param buffer the byte buffer to write the packet into + * @return the number of bytes written + * @throws java.nio.BufferOverflowException if the buffer doesn't have + * enough space to write the packet + */ + int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Reads an encrypted packet from the given byte buffer. + * This method moves the position of the byte buffer. + * @param src a byte buffer containing a non encrypted packet + * @return the packet read + * @throws IOException if the packet couldn't be read + * @throws QuicTransportException if packet is correctly signed but malformed + */ + QuicPacket parsePacket(ByteBuffer src) throws IOException, QuicKeyUnavailableException, QuicTransportException; + + /** + * Returns the original destination connection id, required for + * calculating the retry integrity tag. + *

      + * This is only of interest when protecting/unprotecting a {@linkplain + * QuicPacket.PacketType#RETRY Retry Packet}. + * + * @return the original destination connection id, required for calculating + * the retry integrity tag + */ + QuicConnectionId originalServerConnId(); + + /** + * Returns the TLS engine associated with this context + * @return the TLS engine associated with this context + */ + QuicTLSEngine getTLSEngine(); + + /** + * Checks if the provided token is valid for the given context and connection ID. + * @param destinationID destination connection ID found in the packet + * @param token token to verify + * @return true if token is valid, false otherwise + */ + boolean verifyToken(QuicConnectionId destinationID, byte[] token); + + /** + * {@return The minimum payload size for short packet payloads}. + * Padding will be added to match that size if needed. + * @param destConnectionIdLength the length of the destination + * connectionId included in the packet + */ + default int minShortPacketPayloadSize(int destConnectionIdLength) { + // See RFC 9000, Section 10.3 + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3 + // [..] the endpoint SHOULD ensure that all packets it sends + // are at least 22 bytes longer than the minimum connection + // ID length that it requests the peer to include in its + // packets [...] + // + // A 1-RTT packet contains the peer connection id + // (whose length is destConnectionIdLength), therefore the + // payload should be at least 5 - (destConnectionIdLength + // - connectionIdLength()) - where connectionIdLength is the + // length of the local connection ID. + return 5 - (destConnectionIdLength - connectionIdLength()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java new file mode 100644 index 00000000000..24230a0883d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +// responsible for managing the connection termination of a QUIC connection +public sealed interface ConnectionTerminator permits ConnectionTerminatorImpl { + + // lets the terminator know that the connection is still alive and should not be + // idle timed out + void keepAlive(); + + void terminate(TerminationCause cause); + + boolean tryReserveForUse(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java new file mode 100644 index 00000000000..150d6233953 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl.HandshakeFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl.ProtectionRecord; +import jdk.internal.net.http.quic.TerminationCause.AppLayerClose; +import jdk.internal.net.http.quic.TerminationCause.SilentTermination; +import jdk.internal.net.http.quic.TerminationCause.TransportError; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSED; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSING; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.DRAINING; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.INTERNAL_ERROR; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +final class ConnectionTerminatorImpl implements ConnectionTerminator { + + private final QuicConnectionImpl connection; + private final Logger debug; + private final String logTag; + private final AtomicReference terminationCause = new AtomicReference<>(); + private final CompletableFuture futureTC = new MinimalFuture<>(); + + ConnectionTerminatorImpl(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + this.logTag = connection.logTag(); + } + + @Override + public void keepAlive() { + this.connection.idleTimeoutManager.keepAlive(); + } + + @Override + public boolean tryReserveForUse() { + return this.connection.idleTimeoutManager.tryReserveForUse(); + } + + @Override + public void terminate(final TerminationCause cause) { + Objects.requireNonNull(cause); + try { + doTerminate(cause); + } catch (Throwable t) { + // make sure we do fail the handshake CompletableFuture(s) + // even when the connection termination itself failed. that way + // the dependent CompletableFuture(s) tasks don't keep waiting forever + failHandshakeCFs(t); + } + } + + TerminationCause getTerminationCause() { + return this.terminationCause.get(); + } + + private void doTerminate(final TerminationCause cause) { + final ConnectionCloseFrame frame; + KeySpace keySpace; + switch (cause) { + case SilentTermination st -> { + silentTerminate(st); + return; + } + case TransportError te -> { + frame = new ConnectionCloseFrame(te.getCloseCode(), te.frameType, + te.getPeerVisibleReason()); // 0x1c + keySpace = te.keySpace; + } + case TerminationCause.InternalError ie -> { + frame = new ConnectionCloseFrame(ie.getCloseCode(), 0, + ie.getPeerVisibleReason()); // 0x1c + keySpace = null; + } + case AppLayerClose alc -> { + // application layer triggered connection close + frame = new ConnectionCloseFrame(alc.getCloseCode(), + alc.getPeerVisibleReason()); // 0x1d + keySpace = null; + } + } + if (keySpace == null) { + // TODO: review this + keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + } + immediateClose(frame, keySpace, cause); + } + + void incomingConnectionCloseFrame(final ConnectionCloseFrame frame) { + Objects.requireNonNull(frame); + if (debug.on()) { + debug.log("Received close frame: %s", frame); + } + drain(frame); + } + + void incomingStatelessReset() { + // if local endpoint is a client, then our peer is a server + final boolean peerIsServer = connection.isClientConnection(); + if (Log.errors()) { + Log.logError("{0}: stateless reset from peer ({1})", connection.logTag(), + (peerIsServer ? "server" : "client")); + } + final SilentTermination st = forSilentTermination("stateless reset from peer (" + + (peerIsServer ? "server" : "client") + ")"); + terminate(st); + } + + /** + * Called only when the connection is expected to be discarded without being required + * to inform the peer. + * Discards all state, no CONNECTION_CLOSE is sent, nor does the connection enter closing + * or discarding state. + */ + private void silentTerminate(final SilentTermination terminationCause) { + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for this connection + connection.idleTimeoutManager.shutdown(); + // mark the connection state as closed (we don't enter closing or draining state + // during silent termination) + if (!markClosed(terminationCause)) { + // previously already closed + return; + } + if (Log.quic()) { + Log.logQuic("{0} silently terminating connection due to: {1}", + logTag, terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("silently terminating connection due to: " + terminationCause.getLogMsg()); + } + if (debug.on() || Log.quic()) { + String message = connection.loggableState(); + if (message != null) { + Log.logQuic("{0} connection state: {1}", logTag, message); + debug.log("connection state: %s", message); + } + } + failHandshakeCFs(); + // remove from the endpoint + unregisterConnFromEndpoint(); + discardConnectionState(); + // terminate the streams + connection.streams.terminate(terminationCause); + } + + CompletableFuture futureTerminationCause() { + return this.futureTC; + } + + private void unregisterConnFromEndpoint() { + final QuicEndpoint endpoint = this.connection.endpoint(); + if (endpoint == null) { + // this can happen if the connection is being terminated before + // an endpoint has been established (which is OK) + return; + } + endpoint.removeConnection(this.connection); + } + + private void immediateClose(final ConnectionCloseFrame closeFrame, + final KeySpace keySpace, + final TerminationCause terminationCause) { + assert closeFrame != null : "connection close frame is null"; + assert keySpace != null : "keyspace is null"; + final String logMsg = terminationCause.getLogMsg(); + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + // switch to closing state + if (!markClosing(terminationCause)) { + // has previously already gone into closing state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (connection.stateHandle().draining()) { + if (Log.quic()) { + Log.logQuic("{0} skipping immediate close, since connection is already" + + " in draining state", logTag, logMsg); + } else if (debug.on()) { + debug.log("skipping immediate close, since connection is already" + + " in draining state"); + } + // we are already (in the subsequent) draining state, no need to anything more + return; + } + try { + final String closeCodeHex = (terminationCause.isAppLayer() ? "(app layer) " : "") + + "0x" + Long.toHexString(closeFrame.errorCode()); + if (Log.quic()) { + Log.logQuic("{0} entering closing state, code {1} - {2}", logTag, closeCodeHex, logMsg); + } else if (debug.on()) { + debug.log("entering closing state, code " + closeCodeHex + " - " + logMsg); + } + pushConnectionCloseFrame(keySpace, closeFrame); + } catch (Exception e) { + if (Log.errors()) { + Log.logError("{0} removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION: {1}", logTag, e); + } else if (debug.on()) { + debug.log("removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION"); + } + // we failed to send a CONNECTION_CLOSE frame. this implies that the QuicEndpoint + // won't detect that the QuicConnectionImpl has transitioned to closing connection + // and thus won't remap it to closing. we thus discard such connection from the + // endpoint. + unregisterConnFromEndpoint(); + } + failHandshakeCFs(); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to closing state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to closing state"); + } + } + + private void drain(final ConnectionCloseFrame incomingFrame) { + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + final boolean isAppLayerClose = incomingFrame.variant(); + final String closeCodeString = isAppLayerClose ? + "[app]" + connection.quicInstance().appErrorToString(incomingFrame.errorCode()) : + QuicTransportErrors.toString(incomingFrame.errorCode()); + final String reason = incomingFrame.reasonString(); + final String peer = connection.isClientConnection() ? "server" : "client"; + final String msg = "Connection closed by " + peer + " peer: " + + closeCodeString + + (reason == null || reason.isEmpty() ? "" : (" " + reason)); + final TerminationCause terminationCause; + if (isAppLayerClose) { + terminationCause = appLayerClose(incomingFrame.errorCode(), msg) + .peerVisibleReason(reason); + } else { + terminationCause = forTransportError(incomingFrame.errorCode(), msg, + incomingFrame.errorFrameType()) + .peerVisibleReason(reason); + } + // switch to draining state + if (!markDraining(terminationCause)) { + // has previously already gone into draining state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (Log.quic()) { + Log.logQuic("{0} entering draining state, {1}", logTag, + terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("entering draining state, " + + terminationCause.getLogMsg()); + } + // RFC-9000, section 10.2.2: + // An endpoint that receives a CONNECTION_CLOSE frame MAY send a single packet containing + // a CONNECTION_CLOSE frame before entering the draining state, using a NO_ERROR code if + // appropriate. An endpoint MUST NOT send further packets. + // if we had previously marked our state as closing, then that implies + // we would have already sent a connection close frame. we won't send + // another when draining in such a case. + if (markClosing(terminationCause)) { + try { + if (Log.quic()) { + Log.logQuic("{0} sending CONNECTION_CLOSE frame before entering draining state", + logTag); + } else if (debug.on()) { + debug.log("sending CONNECTION_CLOSE frame before entering draining state"); + } + final ConnectionCloseFrame outgoingFrame = + new ConnectionCloseFrame(NO_ERROR.code(), incomingFrame.getTypeField(), null); + final KeySpace currentKeySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + pushConnectionCloseFrame(currentKeySpace, outgoingFrame); + } catch (Exception e) { + // just log and ignore, since sending the CONNECTION_CLOSE when entering + // draining state is optional + if (Log.errors()) { + Log.logError(logTag + " Failed to send CONNECTION_CLOSE frame," + + " when entering draining state: {0}", e); + } else if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE frame, when entering" + + " draining state: " + e); + } + } + } + failHandshakeCFs(); + // remap the connection to a draining connection + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + endpoint.draining(connection); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to draining state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to draining state"); + } + } + + private void discardConnectionState() { + // close packet spaces + connection.packetNumberSpaces().close(); + // close the incoming packets buffered queue + connection.closeIncoming(); + } + + private void failHandshakeCFs() { + final TerminationCause tc = this.terminationCause.get(); + assert tc != null : "termination cause is null"; + failHandshakeCFs(tc.getCloseCause()); + } + + private void failHandshakeCFs(final Throwable cause) { + final HandshakeFlow handshakeFlow = connection.handshakeFlow(); + handshakeFlow.failHandshakeCFs(cause); + } + + private boolean markClosing(final TerminationCause terminationCause) { + return mark(CLOSING, terminationCause); + } + + private boolean markDraining(final TerminationCause terminationCause) { + return mark(DRAINING, terminationCause); + } + + private boolean markClosed(final TerminationCause terminationCause) { + return mark(CLOSED, terminationCause); + } + + private boolean mark(final int mask, final TerminationCause cause) { + assert cause != null : "termination cause is null"; + final boolean causeSet = this.terminationCause.compareAndSet(null, cause); + // first mark the state appropriately, before completing the futureTerminationCause + // completable future, so that any dependent actions on the completable future + // will see the right state + final boolean marked = this.connection.stateHandle().mark(mask); + if (causeSet) { + this.futureTC.completeAsync(() -> cause, connection.quicInstance().executor()); + } + return marked; + } + + /** + * CONNECTION_CLOSE frame is not congestion controlled (RFC-9002 section 3 + * and RFC-9000 section 12.4, table 3), nor is it queued or scheduled for sending. + * This method constructs a {@link QuicPacket} containing the {@code frame} and immediately + * {@link QuicConnectionImpl#pushDatagram(ProtectionRecord) pushes the datagram} through + * the connection. + * + * @param keySpace the KeySpace to use for sending the packet + * @param frame the CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException if the keys for the KeySpace aren't available + * @throws QuicTransportException for any QUIC transport exception when sending the packet + */ + private void pushConnectionCloseFrame(final KeySpace keySpace, + final ConnectionCloseFrame frame) + throws QuicKeyUnavailableException, QuicTransportException { + // ConnectionClose frame is allowed in Initial, Handshake, 0-RTT, 1-RTT spaces. + // for Initial and Handshake space, the frame is expected to be of type 0x1c. + // see RFC-9000, section 12.4, Table 3 for additional details + final ConnectionCloseFrame toSend = switch (keySpace) { + case ONE_RTT, ZERO_RTT -> frame; + case INITIAL, HANDSHAKE -> { + // RFC 9000 - section 10.2.3: + // A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + // of type 0x1c when sending the frame in Initial or Handshake packets. + // Otherwise, information about the application state might be revealed. + // Endpoints MUST clear the value of the Reason Phrase field and SHOULD + // use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + // of type 0x1c. + yield frame.clearApplicationState(); + } + default -> { + throw new IllegalStateException("cannot send a connection close frame" + + " in keyspace: " + keySpace); + } + }; + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(toSend)); + final ProtectionRecord protectionRecord = ProtectionRecord.single(packet, + connection::allocateDatagramForEncryption); + // while sending the packet containing the CONNECTION_CLOSE frame, the pushDatagram will + // remap (or remove) the QuicConnectionImpl in QuicEndpoint. + connection.pushDatagram(protectionRecord); + } + + /** + * Returns a {@link ByteBuffer} which contains an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

      + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException + * @throws QuicTransportException + */ + ByteBuffer makeConnectionCloseDatagram() + throws QuicKeyUnavailableException, QuicTransportException { + // in theory we don't need this assert, but given the knowledge that this method + // should only be invoked by a closing QuicEndpoint, we have this assert here to + // prevent misuse of this makeConnectionCloseDatagram() method + assert connection.endpoint().isClosed() : "QUIC endpoint isn't closed"; + final ConnectionCloseFrame connCloseFrame = new ConnectionCloseFrame(NO_ERROR.code(), + QuicFrame.CONNECTION_CLOSE, null); + final KeySpace keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + // we don't want the connection's ByteBuffer pooling infrastructure + // (through the QuicConnectionImpl::allocateDatagramForEncryption) for + // this packet, so we use a simple custom allocator. + final Function allocator = (pkt) -> ByteBuffer.allocate(pkt.size()); + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(connCloseFrame)); + final ProtectionRecord encrypted = ProtectionRecord.single(packet, allocator) + .encrypt(connection.codingContext()); + final ByteBuffer datagram = encrypted.datagram(); + final int firstPacketOffset = encrypted.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + return datagram; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java new file mode 100644 index 00000000000..a7469f18ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicTLSEngine; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + +/** + * Keeps track of activity on a {@code QuicConnectionImpl} and manages + * the idle timeout of the QUIC connection + */ +final class IdleTimeoutManager { + + private static final long NO_IDLE_TIMEOUT = 0; + + private final QuicConnectionImpl connection; + private final Logger debug; + private final AtomicBoolean shutdown = new AtomicBoolean(); + private final AtomicLong idleTimeoutDurationMs = new AtomicLong(); + private final ReentrantLock stateLock = new ReentrantLock(); + // must be accessed only when holding stateLock + private IdleTimeoutEvent idleTimeoutEvent; + // must be accessed only when holding stateLock + private StreamDataBlockedEvent streamDataBlockedEvent; + // the time at which the last outgoing packet was sent or an + // incoming packet processed on the connection + private volatile long lastPacketActivityAt; + + private final ReentrantLock idleTerminationLock = new ReentrantLock(); + // true if it has been decided to terminate the connection due to being idle, + // false otherwise. should be accessed only when holding the idleTerminationLock + private boolean chosenForIdleTermination; + // the time at which the connection was last reserved for use. + // should be accessed only when holding the idleTerminationLock + private long lastUsageReservationAt; + + IdleTimeoutManager(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + } + + /** + * Starts the idle timeout management for the connection. This should be called + * after the handshake is complete for the connection. + * + * @throw IllegalStateException if handshake hasn't yet completed or if the handshake + * has failed for the connection + */ + void start() { + final CompletableFuture handshakeCF = + this.connection.handshakeFlow().handshakeCF(); + // start idle management only for successfully completed handshake + if (!handshakeCF.isDone()) { + throw new IllegalStateException("handshake isn't yet complete," + + " cannot start idle connection management"); + } + if (handshakeCF.isCompletedExceptionally()) { + throw new IllegalStateException("cannot start idle connection management for a failed" + + " connection"); + } + startTimers(); + } + + /** + * Starts the idle timeout timer of the QUIC connection, if not already started. + */ + private void startTimers() { + if (shutdown.get()) { + return; + } + this.stateLock.lock(); + try { + if (shutdown.get()) { + return; + } + startIdleTerminationTimer(); + startStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + } + + private void startIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + final Optional idleTimeoutMillis = getIdleTimeout(); + if (idleTimeoutMillis.isEmpty()) { + if (debug.on()) { + debug.log("idle connection management disabled for connection"); + } else { + Log.logQuic("{0} idle connection management disabled for connection", + connection.logTag()); + } + return; + } + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(idleTimeoutMillis.get()); + // we don't expect idle timeout management to be started more than once + assert this.idleTimeoutEvent == null : "idle timeout management" + + " already started for connection"; + // create the idle timeout event and register with the QuicTimerQueue. + this.idleTimeoutEvent = new IdleTimeoutEvent(deadline); + timerQueue.offer(this.idleTimeoutEvent); + if (debug.on()) { + debug.log("started QUIC idle timeout management for connection," + + " idle timeout event: " + this.idleTimeoutEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started QUIC idle timeout management for connection," + + " idle timeout event: {1} deadline: {2}", + connection.logTag(), this.idleTimeoutEvent, deadline); + } + } + + private void stopIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.idleTimeoutEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of IdleTimeoutEvent will return Deadline.MAX) + final Deadline nextDeadline = this.idleTimeoutEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.idleTimeoutEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.idleTimeoutEvent, Deadline.MIN); + } + this.idleTimeoutEvent = null; + } + + private void startStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + // 75% of idle timeout or if idle timeout is not configured, then 30 seconds + final long timeoutMillis = getIdleTimeout() + .map((v) -> (long) (0.75 * v)) + .orElse(30000L); + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(timeoutMillis); + // we don't expect the timer to be started more than once + assert this.streamDataBlockedEvent == null : "STREAM_DATA_BLOCKED timer already started"; + // create the timeout event and register with the QuicTimerQueue. + this.streamDataBlockedEvent = new StreamDataBlockedEvent(deadline, timeoutMillis); + timerQueue.offer(this.streamDataBlockedEvent); + if (debug.on()) { + debug.log("started STREAM_DATA_BLOCKED timer for connection," + + " event: " + this.streamDataBlockedEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started STREAM_DATA_BLOCKED timer for connection," + + " event: {1} deadline: {2}", + connection.logTag(), this.streamDataBlockedEvent, deadline); + } + } + + private void stopStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.streamDataBlockedEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of StreamDataBlockedEvent will return Deadline.MAX) + final Deadline nextDeadline = this.streamDataBlockedEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.streamDataBlockedEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.streamDataBlockedEvent, Deadline.MIN); + } + this.streamDataBlockedEvent = null; + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForUse() { + this.idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // idle termination has been decided for this connection, don't use it + return false; + } + // if the connection is nearing idle timeout due to lack of traffic then + // don't use it + final long lastPktActivity = lastPacketActivityAt; + final long currentNanos = System.nanoTime(); + final long inactivityMs = MILLISECONDS.convert((currentNanos - lastPktActivity), + NANOSECONDS); + final boolean nearingIdleTimeout = getIdleTimeout() + .map((timeoutMillis) -> inactivityMs >= (0.8 * timeoutMillis)) // 80% of idle timeout + .orElse(false); + if (nearingIdleTimeout) { + return false; + } + // express interest in using the connection + this.lastUsageReservationAt = System.nanoTime(); + return true; + } finally { + this.idleTerminationLock.unlock(); + } + } + + + /** + * Returns the idle timeout duration, in milliseconds, negotiated for the connection represented + * by this {@code IdleTimeoutManager}. The negotiated idle timeout of a connection + * is the minimum of the idle connection timeout that is advertised by the + * endpoint represented by this {@code IdleTimeoutManager} and the idle + * connection timeout advertised by the peer. If neither endpoints have advertised + * any idle connection timeout then this method returns an + * {@linkplain Optional#empty() empty} value. + * + * @return the idle timeout in milliseconds or {@linkplain Optional#empty() empty} + */ + Optional getIdleTimeout() { + final long val = this.idleTimeoutDurationMs.get(); + return val == NO_IDLE_TIMEOUT ? Optional.empty() : Optional.of(val); + } + + void keepAlive() { + lastPacketActivityAt = System.nanoTime(); // TODO: timeline().instant()? + } + + void shutdown() { + if (!shutdown.compareAndSet(false, true)) { + // already shutdown + return; + } + this.stateLock.lock(); + try { + // unregister the timeout events from the QuicTimerQueue + stopIdleTerminationTimer(); + stopStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + if (debug.on()) { + debug.log("idle timeout manager shutdown"); + } + } + + void localIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + void peerIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + private void checkUpdateIdleTimeout(final long newIdleTimeoutMillis) { + if (newIdleTimeoutMillis <= 0) { + // idle timeout should be non-zero value, we disregard other values + return; + } + long current; + boolean updated = false; + // update the idle timeout if the new timeout is lesser + // than the previously set value + while ((current = this.idleTimeoutDurationMs.get()) == NO_IDLE_TIMEOUT + || current > newIdleTimeoutMillis) { + updated = this.idleTimeoutDurationMs.compareAndSet(current, newIdleTimeoutMillis); + if (updated) { + break; + } + } + if (!updated) { + return; + } + if (debug.on()) { + debug.log("idle connection timeout updated to " + + newIdleTimeoutMillis + " milli seconds"); + } else { + Log.logQuic("{0} idle connection timeout updated to {1} milli seconds", + connection.logTag(), newIdleTimeoutMillis); + } + } + + private TimeLine timeLine() { + return this.connection.endpoint().timeSource(); + } + + // called when the connection has been idle past its idle timeout duration + private void idleTimedOut() { + if (shutdown.get()) { + return; // nothing to do - the idle timeout manager has been shutdown + } + final Optional timeoutVal = getIdleTimeout(); + assert timeoutVal.isPresent() : "unexpectedly idle timing" + + " out connection, when no idle timeout is configured"; + final long timeoutMillis = timeoutVal.get(); + if (Log.quic() || debug.on()) { + // log idle timeout, with packet space statistics + final String msg = "silently terminating connection due to idle timeout (" + + timeoutMillis + " milli seconds)"; + StringBuilder sb = new StringBuilder(); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (connection.packetNumberSpaces().get(sp) instanceof PacketSpaceManager m) { + sb.append("\n PacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + if (Log.quic()) { + Log.logQuic("{0} {1}: {2}", connection.logTag(), msg, sb.toString()); + } else if (debug.on()) { + debug.log("%s: %s", msg, sb); + } + } + // silently close the connection and discard all its state + final TerminationCause cause = forSilentTermination("connection idle timed out (" + + timeoutMillis + " milli seconds)"); + connection.terminator.terminate(cause); + } + + private long computeInactivityMillis() { + final long currentNanos = System.nanoTime(); + final long lastActiveNanos = Math.max(lastPacketActivityAt, lastUsageReservationAt); + return MILLISECONDS.convert((currentNanos - lastActiveNanos), NANOSECONDS); + } + + final class IdleTimeoutEvent implements QuicTimedEvent { + private final long eventId; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private IdleTimeoutEvent(final Deadline deadline) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + final Optional idleTimeout = getIdleTimeout(); + if (idleTimeout.isEmpty()) { + // nothing to do, don't reschedule + return Deadline.MAX; + } + final long idleTimeoutMillis = idleTimeout.get(); + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + Deadline postponed = maybePostponeDeadline(idleTimeoutMillis); + if (postponed != null) { + // not idle long enough, reschedule + this.nextDeadline = postponed; + return postponed; + } + chosenForIdleTermination = true; + } finally { + idleTerminationLock.unlock(); + } + // the connection has been idle for the idle timeout duration, go + // ahead and terminate it. + terminateNow(); + assert shutdown.get() : "idle timeout manager was expected to be shutdown"; + this.nextDeadline = Deadline.MAX; + return Deadline.MAX; + } + + private Deadline maybePostponeDeadline(final long expectedIdleDurationMs) { + assert idleTerminationLock.isHeldByCurrentThread() : "not holding idle termination lock"; + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= expectedIdleDurationMs) { + // the connection has been idle long enough, don't postpone the timeout. + return null; + } + // not idle long enough, compute the deadline when it's expected to reach + // idle timeout + final long remainingMs = expectedIdleDurationMs - inactivityMs; + final Deadline next = timeLine().instant().plusMillis(remainingMs); + if (debug.on()) { + debug.log("postponing timeout event: " + this + " to fire" + + " in " + remainingMs + " milli seconds, deadline: " + next); + } + return next; + } + + private void terminateNow() { + try { + idleTimedOut(); + } finally { + shutdown(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "QuicIdleTimeoutEvent-" + this.eventId; + } + } + + final class StreamDataBlockedEvent implements QuicTimedEvent { + private final long eventId; + private final long timeoutMillis; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private StreamDataBlockedEvent(final Deadline deadline, final long timeoutMillis) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.timeoutMillis = timeoutMillis; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // connection is already chosen for termination, no need to send + // a STREAM_DATA_BLOCKED + this.nextDeadline = Deadline.MAX; + return this.nextDeadline; + } + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= timeoutMillis && connection.streams.hasBlockedStreams()) { + // has been idle long enough, but there are streams that are blocked due to + // flow control limits and that could have lead to the idleness. + // trigger sending a STREAM_DATA_BLOCKED frame for the streams + // to try and have their limits increased by the peer. + connection.streams.enqueueStreamDataBlocked(); + if (debug.on()) { + debug.log("enqueued a STREAM_DATA_BLOCKED frame since connection" + + " has been idle due to blocked stream(s)"); + } else { + Log.logQuic("{0} enqueued a STREAM_DATA_BLOCKED frame" + + " since connection has been idle due to" + + " blocked stream(s)", connection.logTag()); + } + } + this.nextDeadline = timeLine().instant().plusMillis(timeoutMillis); + return this.nextDeadline; + } finally { + idleTerminationLock.unlock(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "StreamDataBlockedEvent-" + this.eventId; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java new file mode 100644 index 00000000000..76a1f251e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantLock; + +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by the local endpoint of a connection. + * - Produces outgoing NEW_CONNECTION_ID frames, + * - handles incoming RETIRE_CONNECTION_ID frames, + * - registers produced connection IDs with the QuicEndpoint + * Handshake connection ID is created and registered by QuicConnection. + */ +final class LocalConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private long nextConnectionIdSequence; + private final ReentrantLock lock = new ReentrantLock(); + private boolean closed; // when true, no more connection IDs are registered + + // the connection ids (there can be more than one) with which the endpoint identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap localConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + + LocalConnIdManager(final QuicConnectionImpl connection, final String dbTag, + QuicConnectionId handshakeConnectionId) { + this.debug = Utils.getDebugLogger(() -> dbTag); + this.connection = connection; + this.localConnectionIds.put(nextConnectionIdSequence++, handshakeConnectionId); + } + + private QuicConnectionId newConnectionId() { + return connection.endpoint().idFactory().newConnectionId(); + + } + + private byte[] statelessTokenFor(QuicConnectionId cid) { + return connection.endpoint().idFactory().statelessTokenFor(cid); + } + + void handleRetireConnectionIdFrame(final QuicConnectionId incomingPacketDestConnId, + final QuicPacket.PacketType packetType, + final RetireConnectionIDFrame retireFrame) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received RETIRE_CONNECTION_ID frame: %s", retireFrame); + } + final QuicConnectionId toRetire; + lock.lock(); + try { + final long seqNumber = retireFrame.sequenceNumber(); + if (seqNumber >= nextConnectionIdSequence) { + // RFC-9000, section 19.16: Receipt of a RETIRE_CONNECTION_ID frame containing a + // sequence number greater than any previously sent to the peer MUST be treated + // as a connection error of type PROTOCOL_VIOLATION + throw new QuicTransportException("Invalid sequence number " + seqNumber + + " in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + toRetire = this.localConnectionIds.get(seqNumber); + if (toRetire == null) { + return; + } + if (toRetire.equals(incomingPacketDestConnId)) { + // RFC-9000, section 19.16: The sequence number specified in a RETIRE_CONNECTION_ID + // frame MUST NOT refer to the Destination Connection ID field of the packet in which + // the frame is contained. The peer MAY treat this as a connection error of type + // PROTOCOL_VIOLATION. + throw new QuicTransportException("Invalid connection id in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + // forget this id from our local store + this.localConnectionIds.remove(seqNumber); + this.connection.endpoint().removeConnectionId(toRetire, connection); + } finally { + lock.unlock(); + } + if (debug.on()) { + debug.log("retired connection id " + toRetire); + } + } + + public QuicFrame nextFrame(int remaining) { + if (localConnectionIds.size() >= 2) { + return null; + } + int cidlen = connection.endpoint().idFactory().connectionIdLength(); + if (cidlen == 0) { + return null; + } + // frame: + // type - 1 byte + // sequence number - var int + // retire prior to - 1 byte (always zero) + // connection id: + 1 byte + // stateless reset token - 16 bytes + int len = 19 + cidlen + VariableLengthEncoder.getEncodedSize(nextConnectionIdSequence); + if (len > remaining) { + return null; + } + NewConnectionIDFrame newCidFrame; + QuicConnectionId cid = newConnectionId(); + byte[] token = statelessTokenFor(cid); + lock.lock(); + try { + if (closed) return null; + newCidFrame = new NewConnectionIDFrame(nextConnectionIdSequence++, 0, + cid.asReadOnlyBuffer(), ByteBuffer.wrap(token)); + this.localConnectionIds.put(newCidFrame.sequenceNumber(), cid); + this.connection.endpoint().addConnectionId(cid, connection); + if (debug.on()) { + debug.log("Sending NEW_CONNECTION_ID frame"); + } + return newCidFrame; + } finally { + lock.unlock(); + } + } + + public List connectionIds() { + lock.lock(); + try { + // copy to avoid ConcurrentModificationException + return List.copyOf(localConnectionIds.values()); + } finally { + lock.unlock(); + } + } + + public void close() { + lock.lock(); + closed = true; + lock.unlock(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java new file mode 100644 index 00000000000..52821a0fac2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; + +/** + * A class to take care of frames reordering in an ordered flow. + * + * Frames that are {@linkplain #receive(QuicFrame) received} out of order + * will be either buffered or dropped, depending on their {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) position} + * with respect to the current ordered flow {@linkplain #offset() offset}. + * The buffered frames are returned by later calls to {@linkplain #poll()} + * when the flow offset matches the frame offset. + * + * Frames that are {@linkplain #receive(QuicFrame) received} in order + * are immediately returned. + * + * This class is not thread-safe and concurrent access needs to be synchronized + * externally. + * @param A frame type that defines an offset and a {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * length}. The offset of the frame + * indicates its {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * position} in the ordered flow. + */ +public sealed abstract class OrderedFlow { + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link CryptoFrame}. + */ + public static final class CryptoDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code CryptoDataFlow} to reorder + * a flow of {@code CryptoFrame} instances. + */ + public CryptoDataFlow() { + super(CryptoFrame::compareOffsets, + CryptoFrame::offset, + CryptoFrame::length); + } + + @Override + protected CryptoFrame slice(CryptoFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link StreamFrame}. + */ + public static final class StreamDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code StreamDataFlow} to reorder + * a flow of {@code StreamFrame} instances. + */ + public StreamDataFlow() { + super(StreamFrame::compareOffsets, + StreamFrame::offset, + StreamFrame::dataLength); + } + + @Override + protected StreamFrame slice(StreamFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + private final ConcurrentSkipListSet queue; + private final ToLongFunction position; + private final ToIntFunction length; + long offset; + long buffered; + + /** + * Constructs a new instance of ordered flow to reorder frames in a given + * flow. + * @param comparator A comparator to order the frames according to their position in + * the ordered flow. Typically, this will compare the + * frame's offset: the frame with the smaller offset will be sorted + * before the frame with the greater offset + * @param position A method reference that returns the position of the frame in the + * flow. For instance, this would be {@link CryptoFrame#offset() + * CryptoFrame::offset} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#offset() StreamFrame::offset} if {@code } + * is {@code StreamFrame} + * @param length A method reference that returns the number of bytes in the frame data. + * This is used to compute the expected position of the next + * frame in the flow. For instance, this would be {@link CryptoFrame#length() + * CryptoFrame::length} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#dataLength() StreamFrame::dataLength} if {@code } + * is {@code StreamFrame} + */ + public OrderedFlow(Comparator comparator, ToLongFunction position, + ToIntFunction length) { + queue = new ConcurrentSkipListSet<>(comparator); + this.position = position; + this.length = length; + } + + /** + * {@return a slice of the given frame} + * @param frame the frame to slice + * @param offset the new frame offset + * @param length the new frame length + * @throws IndexOutOfBoundsException if the new offset or length + * fall outside of the frame's bounds + */ + protected abstract T slice(T frame, long offset, int length); + + /** + * Receives a new frame. If the frame is below the current + * offset the frame is dropped. If it is above the current offset, + * it is queued. + * If the frame is exactly at the current offset, it is + * returned. + * + * @param frame a frame that was received + * @return the next frame in the flow, or {@code null} if it is not + * available yet. + */ + public T receive(T frame) { + if (frame == null) return null; + + long start = this.position.applyAsLong(frame); + int length = this.length.applyAsInt(frame); + long end = start + length; + assert length >= 0; + assert start >= 0; + long offset = this.offset; + if (end <= offset || length == 0) { + // late arrival or empty frame. Just drop it; No overlap + // if we reach here! + return null; + } else if (start > offset) { + // the frame is after the offset. + // insert or slice it, depending on what we + // have already received. + enqueue(frame, start, length, offset); + return null; + } else { + // case where the frame is either at offset, or is below + // offset but has a length that provides bytes that + // overlap with the current offset. In the later case + // we will return a slice. + int todeliver = (int)(end - offset); + + assert end == offset + todeliver; + // update the offset with the new position + this.offset = end; + // cleanup the queue + dropuntil(end); + if (start == offset) return frame; + return slice(frame, offset, todeliver); + } + } + + private T peekFirst() { + if (queue.isEmpty()) return null; + // why is there no peekFirst? + try { + return queue.first(); + } catch (NoSuchElementException nse) { + return null; + } + } + + private void enqueue(T frame, long pos, int length, long after) { + assert pos == position.applyAsLong(frame); + assert length == this.length.applyAsInt(frame); + assert pos > after; + long offset = this.offset; + assert offset >= after; + long newpos = pos; + int newlen = length; + long limit = Math.addExact(pos, length); + + // look at the closest frame, if any, whose offset is <= to + // the new frame offset. Try to see if the new frame overlaps + // with that frame, and if so, drops the part that overlaps + // in the new frame. + T floor = queue.floor(frame); + if (floor != null) { + long foffset = position.applyAsLong(floor); + long flen = this.length.applyAsInt(floor); + if (limit <= foffset + flen) { + // bytes already all buffered! + // just drop the frame + return; + } + assert foffset <= pos; + // foffset == pos case handled as ceiling below + if (foffset < pos && pos - foffset < flen) { + // reduce the frame if it overlaps with the + // one that sits just before in the queue + newpos = foffset + flen; + newlen = length - (int) (newpos - pos); + } + } + assert limit == newpos + newlen; + + // Look at the frames that have an offset higher or equal to + // the new frame offset, and see if any overlap with the new + // frame. Remove frames that are entirely contained in the new one, + // slice the current frame if the frames overlap. + while (true) { + T ceil = queue.ceiling(frame); + if (ceil != null) { + long coffset = position.applyAsLong(ceil); + assert coffset >= newpos : "overlapping frames in queue"; + if (coffset < limit) { + long clen = this.length.applyAsInt(ceil); + if (clen <= limit - coffset) { + // ceiling frame completely contained in the new frame: + // remove the ceiling frame + queue.remove(ceil); + buffered -= clen; + continue; + } + // safe cast, since newlen <= len + newlen = (int) (coffset - newpos); + } + } + break; + } + assert newlen >= 0; + if (newlen == length) { + assert newpos == pos; + queue.add(frame); + } else if (newlen > 0) { + queue.add(slice(frame, newpos, newlen)); + } + buffered += newlen; + } + + /** + * Removes and return the head of the queue if it is at the + * current offset. Otherwise, returns null. + * @return the head of the queue if it is at the current offset, + * or {@code null} + */ + public T poll() { + return poll(offset); + } + + /** + * {@return the number of buffered frames} + */ + public int size() { + return queue.size(); + } + + /** + * {@return the number of bytes buffered} + */ + public long buffered() { + return buffered; + } + + /** + * {@return true if there are no buffered frames} + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * {@return the current offset of this buffer} + */ + public long offset() { + return offset; + } + + /** + * Drops all buffered frames + */ + public void clear() { + queue.clear(); + } + + /** + * Drop all frames in the buffer whose position is strictly + * below offset. + * + * @param offset the offset below which frames should be dropped + * @return the amount of dropped data + */ + private long dropuntil(long offset) { + T head; + long pos; + long dropped = 0; + do { + head = peekFirst(); + if (head == null) break; + pos = position.applyAsLong(head); + if (pos < offset) { + var length = this.length.applyAsInt(head); + var consumed = offset - pos; + if (length <= consumed) { + // drop it + if (head == queue.pollFirst()) { + buffered -= length; + dropped += length; + } else { + throw new AssertionError("Concurrent modification"); + } + } else { + // safe cast: consumed < length if we reach here + int newlen = length - (int)consumed; + var newhead = slice(head, offset, newlen); + if (head == queue.pollFirst()) { + queue.add(newhead); + buffered -= consumed; + dropped += consumed; + } else { + throw new AssertionError("Concurrent modification"); + } + } + } + } while (pos < offset); + return dropped; + } + + /** + * Pretends to {@linkplain #receive(QuicFrame) receive} the head of the queue, + * if it is at the provided offset + * + * @param offset the minimal offset + * + * @return a received frame at the current flow offset, or {@code null} + */ + private T poll(long offset) { + long current = this.offset; + assert offset <= current; + dropuntil(offset); + T head = peekFirst(); + if (head != null) { + long pos = position.applyAsLong(head); + if (pos == offset) { + // the frame we wanted was in the queue! + // well, let's handle it... + if (head == queue.pollFirst()) { + long length = this.length.applyAsInt(head); + buffered -= length; + } else { + throw new AssertionError("Concurrent modification"); + } + return receive(head); + } + } + return null; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java new file mode 100644 index 00000000000..35b42333a1e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.util.concurrent.Executor; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportException; + +/** + * This interface is a useful abstraction used to tie + * {@link PacketSpaceManager} and {@link QuicConnectionImpl}. + * The {@link PacketSpaceManager} uses functionalities provided + * by a {@link PacketEmitter} when it deems that a packet needs + * to be retransmitted, or that an acknowledgement is due. + * It also uses the emitter's {@linkplain #timer() timer facility} + * when it needs to register a {@link QuicTimedEvent}. + * + * @apiNote + * All these methods are actually implemented by {@link QuicConnectionImpl} + * but the {@code PacketEmitter} interface makes it possible to write + * unit tests against a {@link PacketSpaceManager} without involving + * any {@code QuicConnection} instance. + * + */ +public interface PacketEmitter { + /** + * {@return the timer queue used by this packet emitter} + */ + QuicTimerQueue timer(); + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + * @param attempts the number of previous retransmission of this packet. + * A value of 0 indicates the first retransmission. + */ + void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + long emitAckPacket(PacketSpace packetSpaceManager, AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Called when a packet has been acknowledged. + * @param packet the acknowledged packet + */ + void acknowledged(QuicPacket packet); + + /** + * Called when congestion controller allows sending one packet + * @param packetNumberSpace current packet number space + * @return true if a packet was sent, false otherwise + */ + boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * {@return an executor to use when {@linkplain + * jdk.internal.net.http.common.SequentialScheduler#runOrSchedule(Executor) + * offloading loops to another thread} is required} + */ + Executor executor(); + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event) { + timer().reschedule(event); + } + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event, Deadline deadline) { + timer().reschedule(event, deadline); + } + + /** + * Abort the connection if needed, for example if the peer is not responding + * or max idle time was reached + */ + void checkAbort(PacketNumberSpace packetNumberSpace); + + /** + * {@return true if this emitter is open for transmitting packets, else returns false} + */ + boolean isOpen(); + + default void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { }; + + default String logTag() { return toString(); } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java new file mode 100644 index 00000000000..494af85447e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java @@ -0,0 +1,2370 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckFrameBuilder; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A {@code PacketSpaceManager} takes care of acknowledgement and + * retransmission of packets for a given {@link PacketNumberSpace}. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ + +// See also: RFC 9000, https://www.rfc-editor.org/rfc/rfc9000#name-sending-ack-frames +// Every packet SHOULD be acknowledged at least once, and +// ack-eliciting packets MUST be acknowledged at least once within +// the maximum delay an endpoint communicated using the max_ack_delay +// transport parameter [...]; +// [...] +// In order to assist loss detection at the sender, an endpoint +// SHOULD generate and send an ACK frame without delay when it +// receives an ack-eliciting packet either: +// - when the received packet has a packet number less +// than another ack-eliciting packet that has been received, or +// - when the packet has a packet number larger than the +// highest-numbered ack-eliciting packet that has been received +// and there are missing packets between that packet and this +// packet. [...] +public sealed class PacketSpaceManager implements PacketSpace + permits PacketSpaceManager.OneRttPacketSpaceManager, + PacketSpaceManager.HandshakePacketSpaceManager { + + private final QuicCongestionController congestionController; + private volatile boolean blockedByCC; + // packet threshold for loss detection; RFC 9002 suggests 3 + private static final long kPacketThreshold = 3; + // Multiplier for persistent congestion; RFC 9002 suggests 3 + private static final int kPersistentCongestionThreshold = 3; + + /** + * A record that stores the next AckFrame that should be sent + * within this packet number space. + * + * @param ackFrame the ACK frame to send. + * @param deadline the deadline by which to send this ACK frame. + * @param lastUpdated the time at which the {@link AckFrame}'s + * {@link AckFrame#largestAcknowledged()} was + * last updated. Used for calculating ack delay. + * @param sent the time at which the {@link AckFrame} was sent, + * or {@code null} if it has not been sent yet. + */ + record NextAckFrame(AckFrame ackFrame, + Deadline deadline, + Deadline lastUpdated, + Deadline sent) { + /** + * {@return an identical {@code NextAckFrame} record, with an updated + * {@code deadline}} + * @param deadline the new deadline + * @param sent the point in time at which the ack frame was sent, or null. + */ + public NextAckFrame withDeadline(Deadline deadline, Deadline sent) { + return new NextAckFrame(ackFrame, deadline, lastUpdated, sent); + } + } + + // true if transmit timer should fire now + private volatile boolean transmitNow; + + // These two numbers control whether an PING frame will be + // sent with the next ACK frame, to turn the packet that + // contains the ACK frame into an ACK-eliciting packet. + // These numbers are *not* defined in RFC 9000, but are used + // to implement a strategy for sending occasional PING frames + // in order to prevent ACK frames from growing too big. + // See RFC 9000 section 13.2.4 + // https://www.rfc-editor.org/rfc/rfc9000#name-limiting-ranges-by-tracking + public static final int MAX_ACKRANGE_COUNT_BEFORE_PING = 10; + + protected final Logger debug; + private final Supplier debugStrSupplier; + private final PacketNumberSpace packetNumberSpace; + private final PacketEmitter packetEmitter; + private final ReentrantLock transferLock = new ReentrantLock(); + // The next packet number to use in this space + private final AtomicLong nextPN = new AtomicLong(); + private final TimeLine instantSource; + private final QuicRttEstimator rttEstimator; + // first packet number sent after handshake confirmed + private long handshakeConfirmedPN; + + // A priority queue containing a record for each unacknowledged PingRequest. + // PingRequest are removed from this queue when they are acknowledged, that + // is when any packet whose number is greater than the request packet + // is acknowledged. + // Note: this is used to implement {@link #requestSendPing()} which is + // used to implement out of band ping requests triggered by the + // application. + private final ConcurrentLinkedQueue pendingPingRequests = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet. + // Packets are removed from this queue when they are acknowledged, or when they + // are being retransmitted. In which case, they will be in the pendingRetransmission + // queue + private final ConcurrentLinkedQueue pendingAcknowledgements = + new ConcurrentLinkedQueue<>(); + + // A map containing send times of ack-eliciting packets. + // Packets are removed from this map when they can't contribute to RTT sample, + // i.e. when they are acknowledged, or when a higher-numbered packet is acknowledged. + private final ConcurrentSkipListMap sendTimes = + new ConcurrentSkipListMap<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which is currently being retransmitted. + // Packets are removed from this queue when they have been scheduled for retransmission + // with the quic endpoint + private final ConcurrentLinkedQueue pendingRetransmission = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which should be retransmitted. + // Packets are removed from this queue when they have been scheduled for encryption. + private final ConcurrentLinkedQueue triggeredForRetransmission = + new ConcurrentLinkedQueue<>(); + + // lost packets + private final ConcurrentLinkedQueue lostPackets = + new ConcurrentLinkedQueue<>(); + + // A task invoked by the QuicTimerQueue when some packet retransmission are + // due. This task will move packets from the pendingAcknowledgement queue + // into the triggeredForRetransmission queue (and pendingRetransmission queue) + private final PacketTransmissionTask packetTransmissionTask; + // Used to synchronize transmission with handshake restarts + private final ReentrantLock transmitLock = new ReentrantLock(); + private volatile boolean fastRetransmitDone; + private volatile boolean fastRetransmit; + + /** + * A record to store previous numbers with which a packet has been + * retransmitted. If such a packet is acknowledged, we can stop + * retransmission. + * + * @param number A packet number with which the content of this + * packet was previously sent. + * @param largestAcknowledged the largest packet number acknowledged by this + * previous packet, or {@code -1L} if no packet was + * acknowledged by this packet. + * @param previous Further previous packet numbers, or {@code null}. + */ + private record PreviousNumbers(long number, + long largestAcknowledged, PreviousNumbers previous) {} + + /** + * A record used to implement {@link #requestSendPing()}. + * @param sent when the ping frame was sent + * @param packetNumber the packet number of the packet containing the pingframe + * @param response the response, which will be complete as soon as a packet whose number is + * >= to {@code packetNumber} is received. + */ + private record PingRequest(Deadline sent, long packetNumber, CompletableFuture response) {} + + /** + * A record to store a packet that hasn't been acknowledged, and should + * be scheduled for retransmission if not acknowledged when the deadline + * is reached. + * + * @param packet the unacknowledged quic packet + * @param sent the instant when the packet was sent. + * @param packetNumber the packet number of the {@code packet} + * @param largestAcknowledged the largest packet number acknowledged by this + * packet, or {@code -1L} if no packet is acknowledged + * by this packet. + * @param previousNumbers previous packet numbers with which the packet was + * transmitted, if any, {@code null} otherwise. + */ + private record PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, long largestAcknowledged, + PreviousNumbers previousNumbers) { + + PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, PreviousNumbers previousNumbers) { + this(packet, sent, packetNumber, + AckFrame.largestAcknowledgedInPacket(packet), previousNumbers); + } + + boolean hasPreviousNumber(long packetNumber) { + if (this.packetNumber <= packetNumber) return false; + var pn = previousNumbers; + while (pn != null) { + if (pn.number == packetNumber) { + return true; + } + pn = pn.previous; + } + return false; + } + boolean hasExactNumber(long packetNumber) { + return this.packetNumber == packetNumber; + } + boolean hasNumber(long packetNumber) { + return this.packetNumber == packetNumber || hasPreviousNumber(packetNumber); + } + PreviousNumbers findPreviousAcknowledged(AckFrame frame) { + var pn = previousNumbers; + while (pn != null) { + if (frame.isAcknowledging(pn.number)) return pn; + pn = pn.previous; + } + return null; + } + boolean isAcknowledgedBy(AckFrame frame) { + if (frame.isAcknowledging(packetNumber)) return true; + else return findPreviousAcknowledged(frame) != null; + } + + public int attempts() { + var pn = previousNumbers; + int count = 0; + while (pn != null) { + count++; + pn = pn.previous; + } + return count; + } + + String prettyPrint() { + StringBuilder b = new StringBuilder(); + b.append("pn:").append(packetNumber); + var ppn = previousNumbers; + if (ppn != null) { + var sep = " ["; + while (ppn != null) { + b.append(sep).append(ppn.number); + ppn = ppn.previous; + sep = ", "; + } + b.append("]"); + } + return b.toString(); + } + } + + /** + * A task that sends packets to the peer. + * + * Packets are sent after a delay when: + * - ack delay timer expires + * - PTO timer expires + * They can also be sent without delay when: + * - we are unblocked by the peer + * - new data is available for sending, and we are not blocked + * - need to send ack without delay + */ + final class PacketTransmissionTask implements QuicTimedEvent { + private final SequentialScheduler handleScheduler = + SequentialScheduler.lockingScheduler(this::handleLoop); + private final long id = QuicTimerQueue.newEventId(); + private volatile Deadline nextDeadline; // updated through VarHandle + private PacketTransmissionTask() { + nextDeadline = Deadline.MAX; + } + + @Override + public long eventId() { return id; } + + @Override + public Deadline deadline() { + return nextDeadline; + } + + @Override + public Deadline handle() { + if (closed) { + if (debug.on()) { + debug.log("packet space already closed, PacketTransmissionTask will" + + " no longer be scheduled"); + } + return Deadline.MAX; + } + handleScheduler.runOrSchedule(packetEmitter.executor()); + return Deadline.MAX; + } + + /** + * The handle loop takes care of sending ACKs, packaging stream data + * (if applicable), and retransmitting on PTO. It is never invoked + * directly - but can be triggered by {@link #handle()} or {@link + * #runTransmitter()} + */ + private void handleLoop() { + transmitLock.lock(); + try { + handleLoop0(); + } catch (Throwable t) { + if (Log.errors()) { + Log.logError("{0}: {1} handleLoop failed: {2}", + packetEmitter.logTag(), packetNumberSpace, t); + Log.logError(t); + } else if (debug.on()) { + debug.log("handleLoop failed", t); + } + } finally { + transmitLock.unlock(); + } + } + + private void handleLoop0() throws IOException, QuicTransportException { + // while congestion control allows, or if PTO expired: + // - send lost packet or new packet + // if PTO still expired (== nothing was sent) + // - resend oldest packet, if available + // - otherwise send ping (+ack, if available) + // if ACK still not sent, send ack + if (debug.on()) { + debug.log("PacketTransmissionTask::handle"); + } + packetEmitter.checkAbort(PacketSpaceManager.this.packetNumberSpace); + // Handle is called from within the executor + var nextDeadline = this.nextDeadline; + Deadline now = now(); + do { + transmitNow = false; + var closed = !isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("PacketTransmissionTask::handle: %s closed", + PacketSpaceManager.this.packetNumberSpace); + } + return; + } + if (debug.on()) debug.log("PacketTransmissionTask::handle"); + // this may update congestion controller + int lost = detectAndRemoveLostPackets(now); + if (lost > 0 && debug.on()) debug.log("handle: found %s lost packets", lost); + // if we're sending on PTO, we need to double backoff afterwards + boolean needBackoff = isPTO(now); + int packetsSent = 0; + boolean cwndAvailable; + while ((cwndAvailable = congestionController.canSendPacket()) || + (needBackoff && packetsSent < 2)) { // if PTO, try to send 2 packets + if (!isOpenForTransmission()) { + break; + } + final boolean retransmitted; + try { + retransmitted = retransmit(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + clearAll(); + return; + } + throw new IOException("failed to retransmit data, reason: " + + qkue.getMessage()); + } + if (retransmitted) { + packetsSent++; + continue; + } + final boolean sentNew; + // nothing was retransmitted - check for new data + try { + sentNew = sendNewData(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not transmitting any more data"); + } + return; + } + throw new IOException("failed to send new data, reason: " + + qkue.getMessage()); + } + if (!sentNew) { + break; + } else { + if (needBackoff && packetsSent == 0 && Log.quicRetransmit()) { + Log.logQuic("%s OUT: transmitted new packet on PTO".formatted( + packetEmitter.logTag())); + } + } + packetsSent++; + } + blockedByCC = !cwndAvailable; + if (!cwndAvailable && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: blocked by CC"); + // CC might be available already + if (congestionController.canSendPacket()) { + if (debug.on()) debug.log("handle: unblocked immediately"); + transmitNow = true; + } + } + try { + if (isPTO(now) && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: retransmit on PTO"); + // nothing was sent by the above loop - try to resend the oldest packet + retransmitPTO(); + } else if (fastRetransmit) { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + fastRetransmitDone = true; + fastRetransmit = false; + if (debug.on()) debug.log("handle: fast retransmit"); + // try to resend the oldest packet + retransmitPTO(); + } + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + return; + } + throw new IOException("failed to retransmitPTO data, key space, reason: " + + qkue.getMessage()); + } + boolean stillPTO = isPTO(now); + // if the ack frame is not sent yet, send it now + var ackFrame = getNextAckFrame(!stillPTO); + var pingRequested = PacketSpaceManager.this.pingRequested; + boolean sendPing = pingRequested != null || stillPTO + || shouldSendPing(now, ackFrame); + if (sendPing || ackFrame != null) { + if (debug.on()) debug.log("handle: generate ACK packet or PING ack:%s ping:%s", + ackFrame != null, sendPing); + final long emitted; + try { + emitted = emitAckPacket(ackFrame, sendPing); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not sending ack/ping packet"); + } + return; + } + throw new IOException("failed to send ack/ping data, reason: " + + qkue.getMessage()); + } + if (sendPing && pingRequested != null) { + if (emitted < 0) pingRequested.complete(-1L); + else registerPingRequest(new PingRequest(now, emitted, pingRequested)); + synchronized (PacketSpaceManager.this) { + PacketSpaceManager.this.pingRequested = null; + } + } + } + if (needBackoff) { + long backoff = rttEstimator.increasePtoBackoff(); + if (debug.on()) { + debug.log("handle: %s increase backoff to %s", + PacketSpaceManager.this.packetNumberSpace, + backoff); + } + packetEmitter.ptoBackoffIncreased(PacketSpaceManager.this, backoff); + } + + // if nextDeadline is not Deadline.MAX the task will be + // automatically rescheduled. + if (debug.on()) debug.log("handle: refreshing deadline"); + nextDeadline = computeNextDeadline(); + } while(!nextDeadline.isAfter(now)); + + logNoDeadline(nextDeadline, true); + if (Deadline.MAX.equals(nextDeadline)) return; + // we have a new deadline + packetEmitter.reschedule(this, nextDeadline); + } + + /** + * Create and send a new packet + * @return true if packet was sent, false if there is no more data to send + */ + private boolean sendNewData() throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) debug.log("handle: sending data..."); + boolean sent = packetEmitter.sendData(packetNumberSpace); + if (!sent) { + if (debug.on()) debug.log("handle: no more data to send"); + } + return sent; + } + + @Override + public Deadline refreshDeadline() { + Deadline previousDeadline, newDeadline; + do { + previousDeadline = this.nextDeadline; + newDeadline = computeNextDeadline(); + } while (!Handles.DEADLINE.compareAndSet(this, previousDeadline, newDeadline)); + + if (!newDeadline.equals(previousDeadline)) { + if (debug.on()) { + var now = now(); + if (newDeadline.equals(Deadline.MAX)) { + debug.log("Deadline refreshed: no new deadline"); + } else if (newDeadline.equals(Deadline.MIN)) { + debug.log("Deadline refreshed: run immediately"); + } else if (previousDeadline.equals(Deadline.MAX) || previousDeadline.equals(Deadline.MIN)) { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms", delay); + } else { + debug.log("Deadline refreshed: new deadline in %dms", delay); + } + } else { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms (diff: %dms)", + delay, previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } else { + debug.log("Deadline refreshed: new deadline in %dms (diff: %dms)", + instantSource.instant().until(newDeadline, ChronoUnit.MILLIS), + previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } + } + } + } else { + debug.log("Deadline not refreshed: no change"); + } + logNoDeadline(newDeadline, false); + return newDeadline; + } + + void logNoDeadline(Deadline newDeadline, boolean onlyNoDeadline) { + if (Log.quicRetransmit()) { + if (Deadline.MAX.equals(newDeadline)) { + if (shouldLogWhenNoDeadline()) { + Log.logQuic("{0}: {1} no deadline, task unscheduled", + packetEmitter.logTag(), packetNumberSpace); + } // else: no changes... + } else if (!onlyNoDeadline && shouldLogWhenNewDeadline()) { + if (Deadline.MIN.equals(newDeadline)) { + Log.logQuic("{0}: {1} Deadline.MIN, task will be rescheduled immediately", + packetEmitter.logTag(), packetNumberSpace); + } else { + try { + Log.logQuic("{0}: {1} new deadline computed, deadline in {2}ms", + packetEmitter.logTag(), packetNumberSpace, + Long.toString(now().until(newDeadline, ChronoUnit.MILLIS))); + } catch (ArithmeticException ae) { + Log.logError("Unexpected exception while logging deadline " + + newDeadline + ": " + ae); + Log.logError(ae); + assert false : "Unexpected ArithmeticException: " + ae; + } + } + } + } + } + + private boolean hadNoDeadline; + private synchronized boolean shouldLogWhenNoDeadline() { + if (!hadNoDeadline) { + hadNoDeadline = true; + return true; + } + return false; + } + + private synchronized boolean shouldLogWhenNewDeadline() { + if (hadNoDeadline) { + hadNoDeadline = false; + return true; + } + return false; + } + + boolean hasNoDeadline() { + return Deadline.MAX.equals(nextDeadline); + } + + // reschedule this task + void reschedule() { + Deadline deadline = computeNextDeadline(); + Deadline nextDeadline = this.nextDeadline; + if (Deadline.MAX.equals(deadline)) { + debug.log("no deadline, don't reschedule"); + } else if (deadline.equals(nextDeadline)) { + debug.log("deadline unchanged, don't reschedule"); + } else { + packetEmitter.reschedule(this, deadline); + debug.log("retransmission task: rescheduled"); + } + } + + @Override + public String toString() { + return "PacketTransmissionTask(" + debugStrSupplier.get() + ")"; + } + } + + Deadline deadline() { + return packetTransmissionTask.deadline(); + } + + Deadline prospectiveDeadline() { + return computeNextDeadline(false); + } + + // remove all pending acknowledgements and retransmissions. + private void clearAll() { + transferLock.lock(); + try { + pendingAcknowledgements.forEach(ack -> congestionController.packetDiscarded(List.of(ack.packet))); + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingAcknowledgements.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending acks: " + sb); + } + } + pendingAcknowledgements.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending retransmissions: " + sb); + } + } + pendingRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + triggeredForRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting triggered-for-retransmissions: " + sb.toString()); + } + } + triggeredForRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + lostPackets.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting lost-packets: " + sb.toString()); + } + } + lostPackets.clear(); + } finally { + transferLock.unlock(); + } + } + + private void retransmitPTO() throws QuicKeyUnavailableException, QuicTransportException { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; retransmission on PTO dropped", packetNumberSpace); + } + clearAll(); + return; + } + + PendingAcknowledgement pending; + transferLock.lock(); + try { + if ((pending = pendingAcknowledgements.poll()) != null) { + if (debug.on()) debug.log("Retransmit on PTO: looking for candidate"); + // TODO should keep this packet on the list until it's either acked or lost + congestionController.packetDiscarded(List.of(pending.packet)); + pendingRetransmission.add(pending); + } + } finally { + transferLock.unlock(); + } + if (pending != null) { + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + } + } + + /** + * {@return true if this packet space isn't closed and if the underlying packet emitter + * is open, else returns false} + */ + private boolean isOpenForTransmission() { + return !this.closed && this.packetEmitter.isOpen(); + } + + /** + * A class to keep track of the largest packet that was acknowledged by + * a packet that is being acknowledged. + * This information is used to implement the algorithm described in + * RFC 9000 13.2.4. Limiting Ranges by Tracking ACK Frames + */ + private final class EmittedAckTracker { + volatile long ignoreAllPacketsBefore = -1; + /** + * Record the {@link AckFrame#largestAcknowledged() + * largest acknowledged} packet that was sent in an + * {@link AckFrame} that the peer has acknowledged. + * @param largestAcknowledged the packet number to record + * @return the largest {@code largestAcknowledged} + * packet number that was recorded. + * This is necessarily smaller than (or equal to) the + * {@link #getLargestSentAckedPN()}. + */ + private long record(long largestAcknowledged) { + long witness; + long largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + do { + witness = largestAckedPNReceivedByPeer; + if (witness >= largestAcknowledged) { + largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + assert witness <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAckedPNReceivedByPeer: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(witness, ignoreAllPacketsBefore, largestSentAckedPN); + return witness; + } + } while (!Handles.LARGEST_ACK_ACKED_PN.compareAndSet( + PacketSpaceManager.this, witness, largestAcknowledged)); + assert witness <= largestAcknowledged; + assert largestAcknowledged <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAcknowledged: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(largestSentAckedPN, ignoreAllPacketsBefore, largestSentAckedPN); + return largestAcknowledged; + } + + private boolean ignoreAllPacketsBefore(long packetNumber) { + long ignoreAllPacketsBefore; + do { + ignoreAllPacketsBefore = this.ignoreAllPacketsBefore; + if (packetNumber <= ignoreAllPacketsBefore) return false; + } while (!Handles.IGNORE_ALL_PN_BEFORE.compareAndSet( + this, ignoreAllPacketsBefore, packetNumber)); + return true; + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + public boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + if (frame.isAcknowledging(pending.packetNumber)) { + record(pending.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + record(previous.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + return false; + } + + public long largestAckAcked() { + return largestAckedPNReceivedByPeer; + } + + public void dropPacketNumbersSmallerThan(long newLargestIgnored) { + // this method is called after arbitrarily reducing the ack range + // to this value; This mean we will drop packets whose packet + // number is smaller than the given packet number. + if (ignoreAllPacketsBefore(newLargestIgnored)) { + record(newLargestIgnored); + } + } + } + + private final QuicTLSEngine quicTLSEngine; + private final EmittedAckTracker emittedAckTracker; + private volatile NextAckFrame nextAckFrame; // assigned through VarHandle + // exponent for outgoing packets; defaults to 3 + public static final int ACK_DELAY_EXPONENT = 3; + // max ack delay sent in quic transport parameters, in millis + public static final int ADVERTISED_MAX_ACK_DELAY = 25; + // max timer delay, i.e. how late selector.select returns; 15.6 millis on Windows + public static final int TIMER_DELAY = 16; + // effective max ack delay for outgoing application packets + public static final int MAX_ACK_DELAY = ADVERTISED_MAX_ACK_DELAY - TIMER_DELAY; + + // exponent for incoming packets + private volatile long peerAckDelayExponent; + // max peer ack delay; zero on initial and handshake, + // initialized from transport parameters on application + private volatile long peerMaxAckDelayMillis; // ms + // max ack delay; zero on initial and handshake, MAX_ACK_DELAY on application + private final long maxAckDelay; // ms + volatile boolean closed; + + // The last time an ACK eliciting packet was sent. + // May be null before any such packet is sent... + private volatile Deadline lastAckElicitingTime; + + // not null if sending ping has been requested. + private volatile CompletableFuture pingRequested; + + // The largest packet number successfully processed in this space. + // Needed to decode received packet numbers, see RFC 9000 appendix A.3 + private volatile long largestProcessedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number received in this space. + // Needed to determine if we should send ACK without delay, see RFC 9000 section 13.2.1 + private volatile long largestAckElicitingReceivedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number sent in this space. + // Needed to determine if we should arm PTO timer + private volatile long largestAckElicitingSentPN; + + // The largest packet number acknowledged by peer. + // Needed to determine packet number length, see RFC 9000 appendix A.2 + private volatile long largestReceivedAckedPN; // assigned through VarHandle + + // The largest packet number acknowledged in this space + // This is the largest packet number we have acknowledged. + // This should be less or equal to the largestProcessedPN always. + // Not used. + private volatile long largestSentAckedPN; // assigned through VarHandle + + // The largest packet number that this instance has included + // in an AckFrame sent to the peer, and of which the peer has + // acknowledged reception. + // Used to limit ack ranges, see RFC 9000 section 13.2.4 + private volatile long largestAckedPNReceivedByPeer; // assigned through VarHandle + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * @param connection The connection for which this manager + * is created. + * @param packetNumberSpace The packet number space. + */ + public PacketSpaceManager(final QuicConnectionImpl connection, + final PacketNumberSpace packetNumberSpace) { + this(packetNumberSpace, connection.emitter(), TimeSource.source(), + connection.rttEstimator, connection.congestionController, connection.getTLSEngine(), + () -> connection.dbgTag() + "[" + packetNumberSpace.name() + "]"); + } + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * + * @param packetNumberSpace the packet number space. + * @param packetEmitter the packet emitter + * @param congestionController the congestion controller + * @param debugStrSupplier a supplier for a debug tag to use for logging purposes + */ + public PacketSpaceManager(PacketNumberSpace packetNumberSpace, + PacketEmitter packetEmitter, + TimeLine instantSource, + QuicRttEstimator rttEstimator, + QuicCongestionController congestionController, + QuicTLSEngine quicTLSEngine, + Supplier debugStrSupplier) { + largestProcessedPN = largestReceivedAckedPN = largestAckElicitingReceivedPN + = largestAckElicitingSentPN = largestSentAckedPN = largestAckedPNReceivedByPeer = -1L; + this.debugStrSupplier = debugStrSupplier; + this.debug = Utils.getDebugLogger(debugStrSupplier); + this.instantSource = instantSource; + this.rttEstimator = rttEstimator; + this.congestionController = congestionController; + this.packetNumberSpace = packetNumberSpace; + this.packetEmitter = packetEmitter; + this.emittedAckTracker = new EmittedAckTracker(); + this.packetTransmissionTask = new PacketTransmissionTask(); + this.quicTLSEngine = quicTLSEngine; + maxAckDelay = (packetNumberSpace == PacketNumberSpace.APPLICATION) + ? MAX_ACK_DELAY : 0; + } + + /** + * {@return the max delay before emitting a non ACK-eliciting packet to + * acknowledge a received ACK-eliciting packet, in milliseconds} + */ + public long getMaxAckDelay() { + return maxAckDelay; + } + + /** + * {@return the max ACK delay of the peer, in milliseconds} + */ + public long getPeerMaxAckDelayMillis() { + return peerMaxAckDelayMillis; + } + + /** + * Changes the value of the {@linkplain #getPeerMaxAckDelayMillis() + * peer max ACK delay} and ack delay exponent + * + * @param peerDelay the new delay, in milliseconds + * @param ackDelayExponent the new ack delay exponent + */ + @Override + public void updatePeerTransportParameters(long peerDelay, long ackDelayExponent) { + this.peerAckDelayExponent = ackDelayExponent; + this.peerMaxAckDelayMillis = peerDelay; + } + + @Override + public PacketNumberSpace packetNumberSpace() { + return packetNumberSpace; + } + + @Override + public long allocateNextPN() { + return nextPN.getAndIncrement(); + } + + @Override + public long getLargestPeerAckedPN() { + return largestReceivedAckedPN; + } + + @Override + public long getLargestProcessedPN() { + return largestProcessedPN; + } + + @Override + public long getMinPNThreshold() { + return largestAckedPNReceivedByPeer; + } + + @Override + public long getLargestSentAckedPN() { + return largestSentAckedPN; + } + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + public void versionChanged() { + // don't retransmit packet with "bad" version + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("version changed - clearing pending acks"); + } + clearAll(); + } + + public void retry() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("retry received - clearing pending acks"); + } + clearAll(); + } + + @Override + public ReentrantLock getTransmitLock() { + return transmitLock; + } + + // adds the PingRequest to the pendingPingRequests queue so + // that it can be completed when the packet is ACK'ed. + private void registerPingRequest(PingRequest pingRequest) { + if (closed) { + pingRequest.response().completeExceptionally(new IOException("closed")); + return; + } + pendingPingRequests.add(pingRequest); + // could be acknowledged already! + processPingResponses(largestReceivedAckedPN); + } + + @Override + public void close() { + if (closed) { + return; + } + synchronized (this) { + if (closed) return; + closed = true; + } + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("{0} closing packet space {1}", + packetEmitter.logTag(), packetNumberSpace); + } + if (debug.on()) { + debug.log("closing packet space"); + } + // stop the internal scheduler + packetTransmissionTask.handleScheduler.stop(); + // make sure the task gets eventually removed from the timer + packetEmitter.reschedule(packetTransmissionTask); + // clear pending acks, retransmissions + transferLock.lock(); + try { + clearAll(); + // discard the (TLS) keys + if (debug.on()) { + debug.log("discarding TLS keys"); + } + this.quicTLSEngine.discardKeys(tlsEncryptionLevel()); + } finally { + transferLock.unlock(); + } + rttEstimator.resetPtoBackoff(); + // complete any ping request that hasn't been completed + IOException io = null; + try { + for (var pr : pendingPingRequests) { + if (io == null) { + io = new IOException("Not sending ping because " + + this.packetNumberSpace + " packet space is being closed"); + } + // TODO: is it necessary for this to be an exceptional completion? + pr.response().completeExceptionally(io); + } + } finally { + pendingPingRequests.clear(); + } + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void runTransmitter() { + transmitNow = true; + // run the handle loop + packetTransmissionTask.handle(); + } + + @Override + public void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting) { + assert PacketNumberSpace.of(packet) == packetNumberSpace; + assert packetNumber > largestAckedPNReceivedByPeer; + + if (closed) { + if (debug.on()) { + debug.log("%s closed, ignoring %s(pn: %s)", packetNumberSpace, packet, packetNumber); + } + return; + } + + if (debug.on()) { + debug.log("packetReceived %s(pn:%d, needsAck:%s)", + packet, packetNumber, isAckEliciting); + } + + // whether the packet is ack eliciting or not, we need to add its packet + // number to the ack frame. + packetProcessed(packetNumber); + addToAckFrame(packetNumber, isAckEliciting); + } + + // used in tests + public T triggeredForRetransmission(Function walker) { + return walker.apply(triggeredForRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + public T pendingRetransmission(Function walker) { + return walker.apply(pendingRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public T pendingAcknowledgements(Function walker) { + return walker.apply(pendingAcknowledgements.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public AtomicLong getNextPN() { return nextPN; } + + // Called by the retransmitLoop scheduler. + // Retransmit one packet for which retransmission has been triggered by + // the PacketTransmissionTask. + // return true if something was retransmitted, or false if there was nothing to retransmit + private boolean retransmit() throws QuicKeyUnavailableException, QuicTransportException { + PendingAcknowledgement pending; + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("already closed; retransmission dropped"); + } + clearAll(); + return false; + } + transferLock.lock(); + try { + pending = triggeredForRetransmission.poll(); + } finally { + transferLock.unlock(); + } + + if (pending != null) { + // allocate new packet number + // create new packet + // encrypt packet + // send packet + if (debug.on()) debug.log("handle: retransmitting..."); + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + return true; + } + return false; + } + + /** + * Called by the {@link PacketTransmissionTask} to + * generate a non ACK eliciting packet containing only the given + * ACK frame. + * + *

      If a received packet is ACK-eliciting, then it will be either + * directly acknowledged by {@link QuicConnectionImpl} - which will + * call {@link #getNextAckFrame(boolean)} to embed the {@link AckFrame} + * in a packet, or by a non-eliciting ACK packet which will be + * triggered {@link #getMaxAckDelay() maxAckDelay} after the reception + * of the ACK-eliciting packet (this method, triggered by the {@link + * PacketTransmissionTask}). + * + *

      This method doesn't reset the {@linkplain #getNextAckFrame(boolean) + * next ack frame} to be sent, but reset its delay so that only + * one non ACK-eliciting packet is emitted to acknowledge a given + * packet. + * + * @param ackFrame The ACK frame to send. + * @return the packet number of the emitted packet + */ + private long emitAckPacket(AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + final boolean closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("Packet space closed, ack/ping won't be sent" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + try { + return packetEmitter.emitAckPacket(this, ackFrame, sendPing); + } catch (QuicKeyUnavailableException | QuicTransportException e) { + if (!this.isOpenForTransmission()) { + // possible race condition where the packet space was closed (and keys discarded) + // while there was an attempt to send an ACK/PING frame. + // Ignore such cases, since it's OK to not send those frames when the packet space + // is already closed + if (debug.on()) { + debug.log("ack/ping wasn't sent since packet space was closed" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + throw e; + } + } + + boolean isClosing(QuicPacket packet) { + var frames = packet.frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream() + .anyMatch(ConnectionCloseFrame.class::isInstance); + } + + private synchronized void lastAckElicitingSent(long packetNumber) { + if (largestAckElicitingSentPN < packetNumber) { + largestAckElicitingSentPN = packetNumber; + } + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + if (packetNumber < 0) { + throw new IllegalArgumentException("Invalid packet number: " + packetNumber); + } + largestAckSent(AckFrame.largestAcknowledgedInPacket(packet)); + if (previousPacketNumber >= 0) { + if (debug.on()) { + debug.log("retransmitted packet %s(%d) as %d", + packet.packetType(), previousPacketNumber, packetNumber); + } + + boolean found = false; + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + // TODO: should use a tail set here to skip all pending acks + // whose packet number is < previousPacketNumber? + var iterator = pendingRetransmission.iterator(); + PendingAcknowledgement replacement; + while (iterator.hasNext()) { + PendingAcknowledgement pending = iterator.next(); + if (pending.hasPreviousNumber(previousPacketNumber)) { + // no need to retransmit twice, but can this happen? + iterator.remove(); + } else if (!found && pending.hasExactNumber(previousPacketNumber)) { + PreviousNumbers previous = new PreviousNumbers( + previousPacketNumber, + pending.largestAcknowledged, pending.previousNumbers); + replacement = + new PendingAcknowledgement(packet, now(), packetNumber, previous); + if (debug.on()) { + debug.log("Packet %s(pn:%s) previous %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber, packet.packetType(), previousPacketNumber); + } + var rep = replacement; + if (lostPackets.removeIf(p -> rep.hasPreviousNumber(p.packetNumber))) { + lostPackets.add(rep); + } + addAcknowledgement(replacement); + iterator.remove(); + found = true; + } + } + } finally { + transferLock.unlock(); + } + if (found && packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + if (!found) { + if (debug.on()) { + debug.log("packetRetransmitted: packet not found - previous: %s for %s(%s)", + previousPacketNumber, packet.packetType(), packetNumber); + } + } + } else { + if (packet.isAckEliciting()) { + // This method works with the following assumption: + // - Non ACK eliciting packet do not need to be retransmitted because: + // - they only contain ack frames - which may/will we be retransmitted + // anyway with the next ack eliciting packet + // - they will not be acknowledged directly - we don't want to + // resend them constantly + if (debug.on()) { + debug.log("Packet %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber); + } + PendingAcknowledgement pending = new PendingAcknowledgement(packet, + now(), packetNumber, null); + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + addAcknowledgement(pending); + if (packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + } finally { + transferLock.unlock(); + } + } + } + + + } + + private void addAcknowledgement(PendingAcknowledgement ack) { + lastAckElicitingSent(ack.sent); + lastAckElicitingSent(ack.packetNumber); + pendingAcknowledgements.add(ack); + sendTimes.put(ack.packetNumber, ack.sent); + congestionController.packetSent(ack.packet().size()); + } + + /** + * Computes the next deadline for generating a non ACK eliciting + * packet containing the next ACK frame, or for retransmitting + * unacknowledged packets for which retransmission is due. + * This may be different to the {@link #nextScheduledDeadline()} + * if newer changes have not been taken into account yet. + * @return the deadline at which the scheduler's task for this packet + * space should be scheduled to wake up + */ + public Deadline computeNextDeadline() { + return computeNextDeadline(true); + } + + public Deadline computeNextDeadline(boolean verbose) { + + if (closed) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] closed - no deadline", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MAX; + } + if (transmitNow) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] transmit now", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + if (pingRequested != null) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] ping requested", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + var ack = nextAckFrame; + + Deadline ackDeadline = (ack == null || ack.sent() != null) + ? Deadline.MAX // if the ack frame has already been sent, getNextAck() returns null + : ack.deadline(); + Deadline lossDeadline = getLossTimer(); + // TODO: consider removing the debug traces in this method when integrating + // if both loss deadline and PTO timer are set, loss deadline is always earlier + if (verbose && debug.on() && lossDeadline != Deadline.MIN) debug.log("lossDeadline is: " + lossDeadline); + if (lossDeadline != null) { + if (verbose && debug.on()) { + if (lossDeadline == Deadline.MIN) { + debug.log("lossDeadline is immediate"); + } else if (!ackDeadline.isBefore(lossDeadline)) { + debug.log("lossDeadline in %s ms", + Deadline.between(now(), lossDeadline).toMillis()); + } else { + debug.log("ackDeadline before lossDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] loss deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, lossDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, lossDeadline)))); + } + return min(ackDeadline, lossDeadline); + } + Deadline ptoDeadline = getPtoDeadline(); + if (verbose && debug.on()) debug.log("ptoDeadline is: " + ptoDeadline); + if (ptoDeadline != null) { + if (verbose && debug.on()) { + if (!ackDeadline.isBefore(ptoDeadline)) { + debug.log("ptoDeadline in %s ms", + Deadline.between(now(), ptoDeadline).toMillis()); + } else { + debug.log("ackDeadline before ptoDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] PTO deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, ptoDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, ptoDeadline)))); + } + return min(ackDeadline, ptoDeadline); + } + if (verbose && debug.on()) { + if (ackDeadline == Deadline.MAX) { + debug.log("ackDeadline is: Deadline.MAX"); + } else { + debug.log("ackDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (ackDeadline.equals(Deadline.MAX)) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] no deadline: " + + "pendingAcks: %s, triggered: %s, pendingRetransmit: %s", + packetEmitter.logTag(), packetNumberSpace, pendingAcknowledgements.size(), + triggeredForRetransmission.size(), pendingRetransmission.size())); + } + } else { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] deadline is %s", + packetEmitter.logTag(), packetNumberSpace(), + Utils.debugDeadline(now(), ackDeadline))); + } + } + return ackDeadline; + } + + /** + * {@return the next deadline at which the scheduler's task for this packet + * space is currently scheduled to wake up} + */ + public Deadline nextScheduledDeadline() { + return packetTransmissionTask.nextDeadline; + } + + private Deadline now() { + return instantSource.instant(); + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + private boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + return emittedAckTracker.trackAcknowlegment(pending, frame); + } + + private boolean isAcknowledgingLostPacket(PendingAcknowledgement pending, AckFrame frame, + List[] recovered) { + if (frame.isAcknowledging(pending.packetNumber)) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + return false; + + } + + @Override + public void processAckFrame(AckFrame frame) throws QuicTransportException { + // for each acknowledged packet, remove it from the + // list of packets pending acknowledgement, or from the + // list of packets pending retransmission + long largestAckAckedBefore = emittedAckTracker.largestAckAcked(); + long largestAcknowledged = frame.largestAcknowledged(); + Deadline now = now(); + if (largestAcknowledged >= nextPN.get()) { + throw new QuicTransportException("Acknowledgement for a nonexistent packet", + null, frame.getTypeField(), QuicTransportErrors.PROTOCOL_VIOLATION); + } + + int lostCount; + transferLock.lock(); + try { + if (largestAckReceived(largestAcknowledged)) { + // if the largest acknowledged PN is newly acknowledged + // and at least one of the newly acked packets is ack-eliciting + // -> use the new RTT sample + // the below code only checks if largest acknowledged is ack-eliciting + Deadline sentTime = sendTimes.get(largestAcknowledged); + if (sentTime != null) { + long ackDelayMicros; + if (isApplicationSpace()) { + confirmHandshake(); + long baseAckDelay = peerAckDelayToMicros(frame.ackDelay()); + // if packet was sent after handshake confirmed, use max ack delay + if (largestAcknowledged >= handshakeConfirmedPN) { + ackDelayMicros = Math.min( + baseAckDelay, + TimeUnit.MILLISECONDS.toMicros(peerMaxAckDelayMillis)); + } else { + ackDelayMicros = baseAckDelay; + } + } else { + // acks are not delayed during handshake + ackDelayMicros = 0; + } + long rttSample = sentTime.until(now, ChronoUnit.MICROS); + if (debug.on()) { + debug.log("New RTT sample on packet %s: %s us (delay %s us)", + largestAcknowledged, rttSample, + ackDelayMicros); + } + rttEstimator.consumeRttSample( + rttSample, + ackDelayMicros, + now + ); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not ack eliciting", + largestAcknowledged); + } + } + if (packetNumberSpace != PacketNumberSpace.INITIAL) { + rttEstimator.resetPtoBackoff(); + } + purgeSendTimes(largestAcknowledged); + // complete PingRequests if needed + processPingResponses(largestAcknowledged); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not largest", + largestAcknowledged); + } + } + + pendingRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + triggeredForRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + for (Iterator iterator = pendingAcknowledgements.iterator(); iterator.hasNext(); ) { + PendingAcknowledgement p = iterator.next(); + if (trackAcknowlegment(p, frame)) { + iterator.remove(); + congestionController.packetAcked(p.packet.size(), p.sent); + } + } + lostCount = detectAndRemoveLostPackets(now); + @SuppressWarnings({"unchecked","rawtypes"}) + List[] recovered= Log.quicRetransmit() ? new List[1] : null; + lostPackets.removeIf((p) -> isAcknowledgingLostPacket(p, frame, recovered)); + if (recovered != null && recovered[0] != null) { + Log.logQuic("{0} lost packets recovered: {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), packetType(), + recovered[0].stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + + long largestAckAcked = emittedAckTracker.largestAckAcked(); + if (largestAckAcked > largestAckAckedBefore) { + if (debug.on()) { + debug.log("%s: largestAckAcked=%d - cleaning up AckFrame", + packetNumberSpace, largestAckAcked); + } + // remove ack ranges that we no longer need to acknowledge. + // this implements the algorithm described in RFC 9000, + // 13.2.4. Limiting Ranges by Tracking ACK Frames + cleanupAcks(); + } + + if (lostCount > 0) { + if (debug.on()) + debug.log("Found %s lost packets", lostCount); + // retransmit if possible + runTransmitter(); + } else if (blockedByCC && congestionController.canSendPacket()) { + // CC just got unblocked... send more data + blockedByCC = false; + runTransmitter(); + } else { + // RTT was updated, some packets might be lost, recompute timers + packetTransmissionTask.reschedule(); + } + } + + @Override + public void confirmHandshake() { + assert isApplicationSpace(); + if (handshakeConfirmedPN == 0) { + handshakeConfirmedPN = nextPN.get(); + } + } + + private void purgeSendTimes(long largestAcknowledged) { + sendTimes.headMap(largestAcknowledged, true).clear(); + } + + private long peerAckDelayToMicros(long ackDelay) { + return ackDelay << peerAckDelayExponent; + } + + private NextAckFrame getNextAck(boolean onlyOverdue, int maxSize) { + Deadline now = now(); + // This method is called to retrieve the AckFrame that will + // be embedded in the next packet sent to the peer. + // We therefore need to disarm the timer that will send a + // non-ACK eliciting packet with that AckFrame (if any) before + // returning the AckFrame. This is the purpose of the loop + // below... + while (true) { + NextAckFrame ack = nextAckFrame; + if (ack == null + || ack.deadline() == Deadline.MAX + || (onlyOverdue && ack.deadline().isAfter(now)) + || ack.sent() != null) { + return null; + } + // also reserve 3 bytes for the ack delay + if (ack.ackFrame().size() > maxSize - 3) return null; + NextAckFrame newAck = ack.withDeadline(Deadline.MAX, now); + boolean respin = !Handles.NEXTACK.compareAndSet(this, ack, newAck); + if (!respin) { + return ack; + } + } + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue) { + return getNextAckFrame(onlyOverdue, Integer.MAX_VALUE); + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize) { + if (closed) { + return null; + } + NextAckFrame ack = getNextAck(onlyOverdue, maxSize); + if (ack == null) { + return null; + } + long delay = ack.lastUpdated() + .until(now(), ChronoUnit.MICROS) >> ACK_DELAY_EXPONENT; + return ack.ackFrame().withAckDelay(delay); + } + + /** + * Returns the count of unacknowledged packets that were declared lost. + * The lost packets are moved from the pendingAcknowledgements + * into the pendingRetransmission. + * + * @param now current time, used for time-based loss detection. + */ + private int detectAndRemoveLostPackets(Deadline now) { + Deadline lossSendTime = now.minus(rttEstimator.getLossThreshold()); + int count = 0; + // debug.log("preparing for retransmission"); + transferLock.lock(); + try { + List lost = Log.quicRetransmit() ? new ArrayList<>() : null; + List packets = new ArrayList<>(); + Deadline firstSendTime = null, lastSendTime = null; + for (PendingAcknowledgement head = pendingAcknowledgements.peek(); + head != null && head.packetNumber < largestReceivedAckedPN; + head = pendingAcknowledgements.peek()) { + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold || + !lossSendTime.isBefore(head.sent)) { + if (debug.on()) { + debug.log("retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + if (pendingAcknowledgements.remove(head)) { + pendingRetransmission.add(head); + triggeredForRetransmission.add(head); + packets.add(head.packet); + if (firstSendTime == null) { + firstSendTime = head.sent; + } + lastSendTime = head.sent; + var lp = head; + lostPackets.removeIf(p -> lp.hasPreviousNumber(p.packetNumber)); + lostPackets.add(head); + count++; + if (lost != null) lost.add(head); + } + } else { + if (debug.on()) { + debug.log("no retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + break; + } + } + if (!packets.isEmpty()) { + // Persistent congestion is detected more aggressively than mandated by RFC 9002: + // - may be reported even if there's no prior RTT sample + // - may be reported even if there are acknowledged packets between the lost ones + boolean persistent = Deadline.between(firstSendTime, lastSendTime) + .compareTo(getPersistentCongestionDuration()) > 0; + congestionController.packetLost(packets, lastSendTime, persistent); + } + if (lost != null && !lost.isEmpty()) { + Log.logQuic("{0} lost packet {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), + packetType(), lost.stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + return count; + } + + PacketType packetType() { + return switch (packetNumberSpace) { + case INITIAL -> PacketType.INITIAL; + case HANDSHAKE -> PacketType.HANDSHAKE; + case APPLICATION -> PacketType.ONERTT; + case NONE -> PacketType.NONE; + }; + } + + /** + * {@return true if PTO timer expired, false otherwise} + */ + private boolean isPTO(Deadline now) { + Deadline ptoDeadline = getPtoDeadline(); + return ptoDeadline != null && !ptoDeadline.isAfter(now); + } + + // returns true if this space is the APPLICATION space + private boolean isApplicationSpace() { + return packetNumberSpace == PacketNumberSpace.APPLICATION; + } + + // returns the PTO duration + Duration getPtoDuration() { + var pto = rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(rttEstimator.getPtoBackoff()); + var max = QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; + // don't allow PTO > 240s + return pto.compareTo(max) > 0 ? max : pto; + } + + // returns the persistent congestion duration + Duration getPersistentCongestionDuration() { + return rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(kPersistentCongestionThreshold); + } + + private Deadline getPtoDeadline() { + if (packetNumberSpace == PacketNumberSpace.INITIAL && lastAckElicitingTime != null) { + if (!quicTLSEngine.keysAvailable(QuicTLSEngine.KeySpace.HANDSHAKE)) { + // if handshake keys are not available, initial PTO must be set + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + // set anti-deadlock timer + if (lastAckElicitingTime == null) { + lastAckElicitingTime = now(); + } + if (largestAckElicitingSentPN == -1) { + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (largestAckElicitingSentPN <= largestReceivedAckedPN) { + return null; + } + // Application space deadline can only be set when handshake is confirmed + if (isApplicationSpace() && quicTLSEngine.getHandshakeState() != QuicTLSEngine.HandshakeState.HANDSHAKE_CONFIRMED) { + return null; + } + return lastAckElicitingTime.plus(getPtoDuration()); + } + + private Deadline getLossTimer() { + PendingAcknowledgement head = pendingAcknowledgements.peek(); + if (head == null || head.packetNumber >= largestReceivedAckedPN) { + return null; + } + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold) { + return Deadline.MIN; + } + return head.sent.plus(rttEstimator.getLossThreshold()); + } + + // Compute the new deadline when adding an ack-eliciting packet number + // to an ack frame which is not empty. + private Deadline computeNewDeadlineFor(AckFrame frame, Deadline now, Deadline deadline, + long packetNumber, long previousLargest, + long ackDelay) { + + boolean previousEliciting = !deadline.equals(Deadline.MAX); + + if (closed) return Deadline.MAX; + + if (previousEliciting) { + // RFC 9000 #13.2.2: + // We should send an ACK immediately after receiving two + // ACK-eliciting packets + if (debug.on()) { + debug.log("two ACK-Eliciting packets received: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber < previousLargest) { + // RFC 9000 #13.2.1: + // if the packet has PN less than another ack-eliciting packet, + // send ACK frame as soon as possible + if (debug.on()) { + debug.log("ACK-Eliciting packet received out of order: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber - 1 > previousLargest && previousLargest > -1) { + // RFC 9000 #13.2.1: + // Check whether there are gaps between this packet and the + // previous ACK-eliciting packet that was received: + // if we find any gap we should send an ACK frame as soon + // as possible + if (!frame.isRangeAcknowledged(previousLargest + 1, packetNumber)) { + if (debug.on()) { + debug.log("gaps detected between this packet" + + " and the previous ACK eliciting packet: " + + "next ack deadline now"); + } + return now; + } + } + // send ACK within max delay + return now.plusMillis(ackDelay); + } + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + */ + @Override + public CompletableFuture requestSendPing() { + CompletableFuture pingRequested; + synchronized (this) { + if ((pingRequested = this.pingRequested) == null) { + pingRequested = this.pingRequested = new MinimalFuture<>(); + } + } + runTransmitter(); + return pingRequested; + } + + // Look at whether a ping frame should be sent with the + // next ACK frame... + // If a PING frame should be sent, return the new deadline (now) + // Otherwise, return Deadline.MAX; + // A PING frame will be sent if: + // - the AckFrame contains more than (10) ACK Ranges + // - and no ACK eliciting packet was sent, or the last ACK-eliciting was + // sent long enough ago - typically 1 PTO delay + // These numbers are implementation dependent and not defined in the RFC, but + // help implement a strategy that sends occasional PING frames to limit the size + // of the ACK frames - as described in RFC 9000. + // + // See RFC 9000 Section 13.2.4 + private boolean shouldSendPing(Deadline now, AckFrame frame) { + Deadline last = lastAckElicitingTime; + if (frame != null && + (last == null || + last.isBefore(now.minus(rttEstimator.getBasePtoDuration()))) + && frame.ackRanges().size() > MAX_ACKRANGE_COUNT_BEFORE_PING) { + return true; + } + return false; + } + + // TODO: store the builder instead of storing the AckFrame? + // storing a builder would mean locking - so it might not be a good + // idea. But creating a new builder and AckFrame each time means + // producing more garbage for the GC to collect. + // This method is called when a new packet is received, and it adds the + // received packet number to the next ACK frame to send out. + // If the packet is ACK eliciting it also arms a timeout (if needed) + // to make sure the packet will be acknowledged within the committed + // time frame. + private void addToAckFrame(long packetNumber, boolean isAckEliciting) { + + long largestAckEliciting = largestAckElicitingReceivedPN; + if (isAckEliciting) ackElicitingPacketProcessed(packetNumber); + + if (debug.on()) { + if (packetNumber < largestAckEliciting) { + debug.log("already received a larger ACK eliciting packet"); + } + } + + // compute a new AckFrame that includes the + // provided packet number + NextAckFrame nextAckFrame, ack = null; + boolean reschedule; + long largestAckAcked; + long newLargestAckAcked = -1; + do { + Deadline now = now(); + nextAckFrame = this.nextAckFrame; + var frame = nextAckFrame == null ? null : nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = (frame == null || !frame.isAcknowledging(packetNumber)) + && packetNumber > largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Adding %s(%d) to ackFrame %s (ackEliciting %s)", + packetNumberSpace, packetNumber, nextAckFrame, isAckEliciting); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked) + .addAck(packetNumber); + assert !builder.isEmpty(); + frame = builder.build(); + + // Note: we could optimize this if needed by simply using a max number of + // ranges: we could pre-compute the approximate size of a frame that has N ranges + // and use that. + final int maxFrameSize = QuicConnectionImpl.SMALLEST_MAXIMUM_DATAGRAM_SIZE - 100; + if (frame.size() > maxFrameSize) { + // frame is too big. We will drop some ranges + int ranges = frame.ackRanges().size(); + int index = ranges/3; + builder.dropAckRangesAfter(index); + newLargestAckAcked = builder.getLargestAckAcked(); + var newFrame = builder.build(); + if (Log.quicCC() || Log.quicRetransmit()) { + Log.logQuic("{0}: frame too big ({1} bytes) dropping ack ranges after {2}, " + + "will ignore packets smaller than {3} (new frame: {4} bytes)", + debugStrSupplier.get(), Integer.toString(frame.size()), + Integer.toString(index), Long.toString(newLargestAckAcked), + Integer.toString(newFrame.size())); + } + frame = newFrame; + assert frame.size() <= maxFrameSize; + } + assert frame.isAcknowledging(packetNumber); + if (nextAckFrame == null) { + if (debug.on()) debug.log("no previous ackframe"); + Deadline deadline = isAckEliciting + ? now.plusMillis(maxAckDelay) + : Deadline.MAX; + ack = new NextAckFrame(frame, deadline, now, null); + reschedule = isAckEliciting; + if (debug.on()) debug.log("next deadline: " + maxAckDelay); + } else { + Deadline deadline = nextAckFrame.deadline(); + Deadline nextDeadline = deadline; + boolean deadlineNotExpired = now.isBefore(deadline); + if (isAckEliciting && deadlineNotExpired) { + if (debug.on()) debug.log("computing new deadline for ackframe"); + nextDeadline = computeNewDeadlineFor(frame, now, deadline, + packetNumber, largestAckEliciting, maxAckDelay); + } + long millisToNext = nextDeadline.equals(Deadline.MAX) + ? Long.MAX_VALUE + : now.until(nextDeadline, ChronoUnit.MILLIS); + if (debug.on()) { + if (nextDeadline == Deadline.MAX) { + debug.log("next deadline is: Deadline.MAX"); + } else { + debug.log("next deadline is: " + millisToNext); + } + } + ack = new NextAckFrame(frame, nextDeadline, now, null); + reschedule = !nextDeadline.equals(deadline) + || millisToNext <= 0; + } + if (debug.on()) { + String delay = reschedule ? Utils.millis(now(), ack.deadline()) + : "not rescheduled"; + debug.log("%s: new ackFrame composed: %s - reschedule=%s", + packetNumberSpace, ack.ackFrame(), delay); + } + } else { + reschedule = false; + if (debug.on()) { + debug.log("packet %s(%d) is already in ackFrame %s", + packetNumberSpace, packetNumber, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + if (newLargestAckAcked >= 0) { + // we reduced the frame because it was too big: we need to ignore + // packets that are larger than the new largest ignored packet. + // this is now our new de-facto 'largestAckAcked' even if it wasn't + // really acked by the peer + emittedAckTracker.dropPacketNumbersSmallerThan(newLargestAckAcked); + } + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert packetNumber <= largestAckAcked + || ackFrame != null && ackFrame.isAcknowledging(packetNumber) + || nextAckFrame != null && nextAckFrame.ackFrame() != null + && nextAckFrame.ackFrame.isAcknowledging(packetNumber) + : "packet %s(%s) should be in ackFrame" + .formatted(packetNumberSpace, packetNumber); + + if (reschedule) { + runTransmitter(); + } + } + + void debugState() { + if (debug.on()) { + debug.log("state: %s", isClosed() ? "closed" : "opened" ); + debug.log("AckFrame: " + nextAckFrame); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + debug.log("Pending acks: %s", pendingAcks); + debug.log("Pending retransmit: %s", pendingRetransmit); + } + } + + void debugState(String prefix, StringBuilder sb) { + String state = isClosed() ? "closed" : "opened"; + sb.append(prefix).append("State: ").append(state).append('\n'); + sb.append(prefix).append("AckFrame: ").append(nextAckFrame).append('\n'); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + sb.append(prefix).append("Pending acks: ").append(pendingAcks).append('\n'); + sb.append(prefix).append("Pending retransmit: ").append(pendingRetransmit); + } + + @Override + public boolean isAcknowledged(long packetNumber) { + var ack = nextAckFrame; + var ackFrame = ack == null ? null : ack.ackFrame(); + var largestProcessed = largestProcessedPN; + // if ackFrame is null it means all packets <= largestProcessedPN + // have been acked. + if (ackFrame == null) return packetNumber <= largestProcessed; + if (packetNumber > largestProcessed) return false; + var largestAckedPNReceivedByPeer = this.largestAckedPNReceivedByPeer; + if (packetNumber <= largestAckedPNReceivedByPeer) return true; + return ackFrame.isAcknowledging(packetNumber); + } + + @Override + public void fastRetransmit() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (closed || fastRetransmitDone) { + return; + } + fastRetransmit = true; + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("Scheduling fast retransmit"); + } else if (debug.on()) { + debug.log("Scheduling fast retransmit"); + } + runTransmitter(); + + } + + private static Deadline min(Deadline one, Deadline two) { + return two.isAfter(one) ? one : two; + } + + // This implements the algorithm described in RFC 9000: + // 13.2.4. Limiting Ranges by Tracking ACK Frames + private void cleanupAcks() { + // clean up the next ACK frame, removing all packets <= largestAckAcked + NextAckFrame nextAckFrame, ack = null; + long largestAckAcked; + do { + nextAckFrame = this.nextAckFrame; + if (nextAckFrame == null) return; // nothing to do! + var frame = nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = frame != null + && frame.smallestAcknowledged() <= largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Dropping all acks below %s(%d) in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked); + frame = builder.isEmpty() ? null : builder.build(); + if (frame == null) { + ack = null; + if (debug.on()) { + debug.log("%s: ackFrame cleared - nothing to acknowledge", + packetNumberSpace); + } + } else { + Deadline deadline = nextAckFrame.deadline(); + ack = new NextAckFrame(frame, deadline, + nextAckFrame.lastUpdated(), nextAckFrame.sent()); + if (debug.on()) { + debug.log("%s: ackFrame cleaned up: %s", + packetNumberSpace, ack.ackFrame()); + } + } + } else { + if (debug.on()) { + debug.log("%s: no packet smaller than %d in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert ackFrame == null || ackFrame.smallestAcknowledged() > largestAckAcked + : "%s(pn > %s) should not acknowledge packet <= %s" + .formatted(packetNumberSpace, ackFrame.smallestAcknowledged(), largestAckAcked); + } + + private long ackElicitingPacketProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestAckElicitingReceivedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_ACK_ELICITING_RECEIVED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private long packetProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestProcessedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_PROCESSED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + /** + * Theoretically we should wait for the packet that contains the + * ping frame to be acknowledged, but if we receive the ack of a + * packet with a larger number, we can assume that the connection + * is still alive, and therefore complete the ping response. + * @param packetNumber the acknowledged packet number + */ + private void processPingResponses(long packetNumber) { + if (pendingPingRequests.isEmpty()) return; + var iterator = pendingPingRequests.iterator(); + while (iterator.hasNext()) { + var pr = iterator.next(); + if (pr.packetNumber() <= packetNumber) { + iterator.remove(); + pr.response().complete(pr.sent().until(now(), ChronoUnit.MILLIS)); + } else { + // this is a queue, so the PingRequest with the smaller + // packet number will be at the head. We can stop iterating + // as soon as we find a PingRequest that has a packet + // number larger than the one acknowledged. + break; + } + } + } + + private long largestAckSent(long packetNumber) { + long largestPN; + do { + largestPN = largestSentAckedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_SENT_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private boolean largestAckReceived(long packetNumber) { + long largestPN; + do { + largestPN = largestReceivedAckedPN; + if (largestPN >= packetNumber) return false; // already up to date + } while (!Handles.LARGEST_RECEIVED_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return true; // updated + } + + // records the time at which the last ACK-eliciting packet was sent. + // This has the side effect of resetting the nextPingTime to Deadline.MAX + // The logic is that a PING frame only need to be sent if no ACK-eliciting + // packet has been sent for some time (and the AckFrame has grown big enough). + // See RFC 9000 - Section 13.2.4 + private Deadline lastAckElicitingSent(Deadline now) { + Deadline max; + if (debug.on()) + debug.log("Updating last send time to %s", now); + do { + max = lastAckElicitingTime; + if (max != null && !now.isAfter(max)) return max; + } while (!Handles.LAST_ACK_ELICITING_TIME + .compareAndSet(this, max, now)); + return now; + } + + /** + * returns the TLS encryption level of this packet space as specified + * in RFC-9001, section 4, table 1. + */ + private QuicTLSEngine.KeySpace tlsEncryptionLevel() { + return switch (this.packetNumberSpace) { + case INITIAL -> QuicTLSEngine.KeySpace.INITIAL; + // APPLICATION packet space could even mean 0-RTT, but currently we don't support 0-RTT + case APPLICATION -> QuicTLSEngine.KeySpace.ONE_RTT; + case HANDSHAKE -> QuicTLSEngine.KeySpace.HANDSHAKE; + default -> throw new IllegalStateException("No known TLS encryption level" + + " for packet space: " + this.packetNumberSpace); + }; + } + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final class Handles { + private Handles() {throw new InternalError();} + static final VarHandle DEADLINE; + static final VarHandle NEXTACK; + static final VarHandle LARGEST_PROCESSED_PN; + static final VarHandle LARGEST_ACK_ELICITING_RECEIVED_PN; + static final VarHandle LARGEST_RECEIVED_ACKED_PN; + static final VarHandle LARGEST_SENT_ACKED_PN; + static final VarHandle LARGEST_ACK_ACKED_PN; + static final VarHandle LAST_ACK_ELICITING_TIME; + static final VarHandle IGNORE_ALL_PN_BEFORE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + Class srt = PacketTransmissionTask.class; + DEADLINE = lookup.findVarHandle(srt, "nextDeadline", Deadline.class); + + Class pmc = PacketSpaceManager.class; + LAST_ACK_ELICITING_TIME = lookup.findVarHandle(pmc, + "lastAckElicitingTime", Deadline.class); + NEXTACK = lookup.findVarHandle(pmc, "nextAckFrame", NextAckFrame.class); + LARGEST_RECEIVED_ACKED_PN = lookup + .findVarHandle(pmc, "largestReceivedAckedPN", long.class); + LARGEST_SENT_ACKED_PN = lookup + .findVarHandle(pmc, "largestSentAckedPN", long.class); + LARGEST_PROCESSED_PN = lookup + .findVarHandle(pmc, "largestProcessedPN", long.class); + LARGEST_ACK_ELICITING_RECEIVED_PN = lookup + .findVarHandle(pmc, "largestAckElicitingReceivedPN", long.class); + LARGEST_ACK_ACKED_PN = lookup + .findVarHandle(pmc, "largestAckedPNReceivedByPeer", long.class); + + Class eat = EmittedAckTracker.class; + IGNORE_ALL_PN_BEFORE = lookup + .findVarHandle(eat, "ignoreAllPacketsBefore", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + + } + } + } + + static final class OneRttPacketSpaceManager extends PacketSpaceManager + implements QuicOneRttContext { + + OneRttPacketSpaceManager(final QuicConnectionImpl connection) { + super(connection, PacketNumberSpace.APPLICATION); + } + } + + static final class HandshakePacketSpaceManager extends PacketSpaceManager { + private final PacketSpaceManager initialPktSpaceMgr; + private final boolean isClientConnection; + private final AtomicBoolean firstPktSent = new AtomicBoolean(); + + HandshakePacketSpaceManager(final QuicConnectionImpl connection, + final PacketSpaceManager initialPktSpaceManager) { + super(connection, PacketNumberSpace.HANDSHAKE); + this.isClientConnection = connection.isClientConnection(); + this.initialPktSpaceMgr = initialPktSpaceManager; + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + super.packetSent(packet, previousPacketNumber, packetNumber); + if (!isClientConnection) { + // nothing additional to be done for server connections + return; + } + if (firstPktSent.compareAndSet(false, true)) { + // if this is the first packet we sent in the HANDSHAKE keyspace + // then we close the INITIAL space discard the INITIAL keys. + // RFC-9000, section 17.2.2.1: + // A client stops both sending and processing Initial packets when it sends + // its first Handshake packet. ... Though packets might still be in flight or + // awaiting acknowledgment, no further Initial packets need to be exchanged + // beyond this point. Initial packet protection keys are discarded along with + // any loss recovery and congestion control state + if (debug.on()) { + debug.log("first handshake packet sent by client, initiating close of" + + " INITIAL packet space"); + } + this.initialPktSpaceMgr.close(); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java new file mode 100644 index 00000000000..065d045b57c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by a peer of a connection. + * - Handles incoming NEW_CONNECTION_ID frames, + * - produces outgoing RETIRE_CONNECTION_ID frames, + * - registers received stateless reset tokens with the QuicEndpoint + * Additionally on the client side: + * - handles incoming transport parameters (preferred_address, stateless_reset_token) + * - stores original and retry peer IDs + */ +// TODO implement voluntary switching of connection IDs +final class PeerConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private final String logTag; + private final boolean isClient; + + private enum State { + INITIAL_PKT_NOT_RECEIVED_FROM_PEER, + RETRY_PKT_RECEIVED_FROM_PEER, + PEER_CONN_ID_FINALIZED + } + + private volatile State state = State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER; + + private QuicConnectionId clientSelectedDestConnId; + private QuicConnectionId peerDecidedRetryConnId; + // sequence number of active connection ID + private long activeConnIdSeq = -1; + private QuicConnectionId activeConnId; + + // the connection ids (there can be more than one) with which the peer identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap peerConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + // the connection id sequence numbers that we haven't received yet. + // We need to know which sequence numbers are retired, and which are not assigned yet + private final NavigableSet gaps = + Collections.synchronizedNavigableSet(new TreeSet<>()); + // the connection id sequence numbers that are awaiting retirement. + private final Queue toRetire = new ArrayDeque<>(); + // the largest retirePriorTo value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedRetirePriorTo = -1; // -1 implies none received so far + // the largest sequenceNumber value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedSequenceNumber; + private final ReentrantLock lock = new ReentrantLock(); + + PeerConnIdManager(final QuicConnectionImpl connection, final String dbTag) { + this.isClient = connection.isClientConnection(); + this.debug = Utils.getDebugLogger(() -> dbTag); + this.logTag = connection.logTag(); + this.connection = connection; + } + + /** + * Save the client-selected original server connection ID + * + * @param peerConnId the client-selected original server connection ID + */ + void originalServerConnId(final QuicConnectionId peerConnId) { + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a client selected peer id" + + " in current state " + st); + } + this.clientSelectedDestConnId = peerConnId; + this.activeConnId = peerConnId; + } finally { + lock.unlock(); + } + } + + /** + * {@return the client-selected original server connection ID} + */ + QuicConnectionId originalServerConnId() { + lock.lock(); + try { + final var id = this.clientSelectedDestConnId; + if (id == null) { + throw new IllegalArgumentException("Original (peer) connection id not yet set"); + } + return id; + } finally { + lock.unlock(); + } + } + + /** + * Save the server-selected retry connection ID + * + * @param peerConnId the server-selected retry connection ID + */ + void retryConnId(final QuicConnectionId peerConnId) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a peer id, from retry packet," + + " in current state " + st); + } + this.peerDecidedRetryConnId = peerConnId; + this.activeConnId = peerConnId; + this.state = State.RETRY_PKT_RECEIVED_FROM_PEER; + } finally { + lock.unlock(); + } + } + + /** + * Returns the connectionId the server included in the Source Connection ID field of a + * Retry packet. May be null. + * + * @return the connection id sent in the server's retry packet + */ + QuicConnectionId retryConnId() { + lock.lock(); + try { + return this.peerDecidedRetryConnId; + } finally { + lock.unlock(); + } + } + + /** + * The peer in its INITIAL packet would have sent a connection id representing itself. That + * connection id may not be the same that we might have sent in the INITIAL packet. If it isn't + * the same, then we switch the peer connection id, that we keep track off, to the one that + * the peer has chosen. + * + * @param initialPacket the INITIAL packet from the peer + */ + void finalizeHandshakePeerConnId(final InitialPacket initialPacket) throws QuicTransportException { + lock.lock(); + try { + final QuicConnectionId sourceId = initialPacket.sourceId(); + final var st = this.state; + if (st == State.PEER_CONN_ID_FINALIZED) { + // we have already finalized the peer connection id, through a previous INITIAL + // packet receipt (there can be more than one INITIAL packets). + // now we just verify that this INITIAL packet too has the finalized peer connection + // id and if it doesn't then we throw an exception + final QuicConnectionId handshakePeerConnId = this.peerConnectionIds.get(0L); + assert handshakePeerConnId != null : "Handshake peer connection id is unavailable"; + if (!handshakePeerConnId.equals(sourceId)) { + throw new QuicTransportException("Invalid source connection id in INITIAL packet", + QuicTLSEngine.KeySpace.INITIAL, 0, PROTOCOL_VIOLATION); + } + return; + } + // this is the first INITIAL packet from the peer, so we finalize the peer connection id + final PeerConnectionId handshakePeerConnId = new PeerConnectionId(sourceId.getBytes()); + // at this point we have either switched to a new peer connection id (chosen by the peer) + // or have agreed to use the one we chose for the peer. In either case, we register this + // as the handshake peer connection id with sequence number 0. + // RFC-9000, section 5.1.1: The initial connection ID issued by an endpoint is sent in + // the Source Connection ID field of the long packet header during the handshake. + // The sequence number of the initial connection ID is 0. + this.peerConnectionIds.put(0L, handshakePeerConnId); + this.state = State.PEER_CONN_ID_FINALIZED; + this.activeConnIdSeq = 0; + this.activeConnId = handshakePeerConnId; + if (debug.on()) { + debug.log("scid: %s finalized handshake peerConnectionId as: %s", + connection.localConnectionId().toHexString(), + handshakePeerConnId.toHexString()); + } + } finally { + lock.unlock(); + } + } + + /** + * Save the connection ID from the preferred address QUIC transport parameter + * + * @param preferredConnId preferred connection ID + * @param preferredStatelessResetToken preferred stateless reset token + */ + void handlePreferredAddress(final ByteBuffer preferredConnId, + final byte[] preferredStatelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final PeerConnectionId peerConnId = new PeerConnectionId(preferredConnId, + preferredStatelessResetToken); + // keep track of this peer connection id + // RFC-9000, section 5.1.1: If the preferred_address transport parameter is sent, + // the sequence number of the supplied connection ID is 1 + assert largestReceivedSequenceNumber == 0; + this.peerConnectionIds.put(1L, peerConnId); + largestReceivedSequenceNumber = 1; + } finally { + lock.unlock(); + } + } + + /** + * Save the stateless reset token QUIC transport parameter + * + * @param statelessResetToken stateless reset token + */ + void handshakeStatelessResetToken(final byte[] statelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final QuicConnectionId handshakeConnId = this.peerConnectionIds.get(0L); + if (handshakeConnId == null) { + throw new IllegalStateException("No handshake peer connection available"); + } + // recreate the conn id with the stateless token + this.peerConnectionIds.put(0L, new PeerConnectionId(handshakeConnId.asReadOnlyBuffer(), + statelessResetToken)); + // register with the endpoint + connection.endpoint().associateStatelessResetToken(statelessResetToken, connection); + } finally { + lock.unlock(); + } + } + + /** + * {@return the active peer connection ID} + */ + QuicConnectionId getPeerConnId() { + lock.lock(); + try { + if (activeConnIdSeq < largestReceivedRetirePriorTo) { + // stop using the old connection ID + switchConnectionId(); + } + return activeConnId; + } finally { + lock.unlock(); + } + } + + private QuicConnectionId getPeerConnId(final long sequenceNum) { + assert lock.isHeldByCurrentThread(); + return this.peerConnectionIds.get(sequenceNum); + } + + /** + * Process the incoming NEW_CONNECTION_ID frame. + * + * @param newCid the NEW_CONNECTION_ID frame + * @throws QuicTransportException if the frame violates the protocol + */ + void handleNewConnectionIdFrame(final NewConnectionIDFrame newCid) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received NEW_CONNECTION_ID frame: %s", newCid); + } + // pre-checks + final long sequenceNumber = newCid.sequenceNumber(); + assert sequenceNumber >= 0 : "negative sequence number disallowed in new connection id frame"; + final long retirePriorTo = newCid.retirePriorTo(); + if (retirePriorTo > sequenceNumber) { + // RFC 9000, section 19.15: Receiving a value in the Retire Prior To field that is greater + // than that in the Sequence Number field MUST be treated as a connection error of + // type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid retirePriorTo " + retirePriorTo, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer connectionId = newCid.connectionId(); + final int connIdLength = connectionId.remaining(); + if (connIdLength < 1 || connIdLength > MAX_CONNECTION_ID_LENGTH) { + // RFC-9000, section 19.15: Values less than 1 and greater than 20 are invalid and + // MUST be treated as a connection error of type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid connection id length " + connIdLength, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer statelessResetToken = newCid.statelessResetToken(); + assert statelessResetToken.remaining() == QuicConnectionImpl.RESET_TOKEN_LENGTH; + lock.lock(); + try { + // see if we have received any connection ids for this same sequence number. + // this is possible if the packet containing the new connection id frame was retransmitted. + // the connection id for such a (duplicate) sequence number is expected to be the same. + // RFC-9000, section 19.15: if a sequence number is used for different connection IDs, + // the endpoint MAY treat that receipt as a connection error of type PROTOCOL_VIOLATION + final QuicConnectionId previousConnIdForSeqNum = getPeerConnId(sequenceNumber); + if (previousConnIdForSeqNum != null) { + if (previousConnIdForSeqNum.matches(connectionId)) { + // frame with same sequence number and connection id, probably a retransmission. + // ignore this frame + if (Log.trace()) { + Log.logTrace("{0} Ignoring (duplicate) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (duplicate) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + // mismatch, throw protocol violation error + throw new QuicTransportException("Invalid connection id in (duplicated)" + + " new connection id frame with sequence number " + sequenceNumber, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), PROTOCOL_VIOLATION); + } + if ((sequenceNumber <= largestReceivedSequenceNumber && !gaps.contains(sequenceNumber)) + || sequenceNumber < largestReceivedRetirePriorTo) { + if (Log.trace()) { + Log.logTrace("{0} Ignoring (retired) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (retired) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + long numConnIdsToAdd = Math.max(sequenceNumber - largestReceivedSequenceNumber, 0); + final long numCurrentActivePeerConnIds = this.peerConnectionIds.size() + this.gaps.size(); + // we can temporarily store up to 3x the active connection ID limit, + // including active and retired IDs. + if (numCurrentActivePeerConnIds + numConnIdsToAdd + toRetire.size() + > 3 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + // end pre-checks + // if we reached here, the number of connection IDs is less than twice the active limit. + // Insert gaps for the sequence numbers we haven't seen yet + insertGaps(sequenceNumber); + // Update the list of sequence numbers to retire + retirePriorTo(retirePriorTo); + // insert the new connection ID + final byte[] statelessResetTokenBytes = new byte[QuicConnectionImpl.RESET_TOKEN_LENGTH]; + statelessResetToken.get(statelessResetTokenBytes); + final PeerConnectionId newPeerConnId = new PeerConnectionId(connectionId, statelessResetTokenBytes); + final var previous = this.peerConnectionIds.putIfAbsent(sequenceNumber, newPeerConnId); + assert previous == null : "A peer connection id already exists for sequence number " + + sequenceNumber; + // post-checks + // now we can accurately check the number of active and retired connection IDs + if (peerConnectionIds.size() + gaps.size() + > this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Active connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (toRetire.size() > 2 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.2: + // An endpoint SHOULD limit the number of connection IDs it has retired locally for + // which RETIRE_CONNECTION_ID frames have not yet been acknowledged. + // An endpoint SHOULD allow for sending and tracking a number + // of RETIRE_CONNECTION_ID frames of at least twice the value + // of the active_connection_id_limit transport parameter + throw new QuicTransportException("Retired connection id limit reached: " + toRetire, + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (this.largestReceivedRetirePriorTo < retirePriorTo) { + this.largestReceivedRetirePriorTo = retirePriorTo; + } + if (this.largestReceivedSequenceNumber < sequenceNumber) { + this.largestReceivedSequenceNumber = sequenceNumber; + } + } finally { + lock.unlock(); + } + } + + private void switchConnectionId() { + assert lock.isHeldByCurrentThread(); + // the caller is expected to retire the active connection id prior to calling this + assert !peerConnectionIds.containsKey(activeConnIdSeq); + Map.Entry entry = peerConnectionIds.ceilingEntry(largestReceivedRetirePriorTo); + activeConnIdSeq = entry.getKey(); + activeConnId = entry.getValue(); + // link the peer issued stateless reset token to this connection + final QuicEndpoint endpoint = this.connection.endpoint(); + endpoint.associateStatelessResetToken(entry.getValue().getStatelessResetToken(), this.connection); + + if (Log.trace()) { + Log.logTrace("{0} Switching to connection ID {1}", logTag, activeConnIdSeq); + } + if (debug.on()) { + debug.log("Switching to connection ID %d", activeConnIdSeq); + } + } + + private void insertGaps(long sequenceNumber) { + assert lock.isHeldByCurrentThread(); + for (long i = largestReceivedSequenceNumber + 1; i < sequenceNumber; i++) { + gaps.add(i); + } + } + + private void retirePriorTo(final long priorTo) { + assert lock.isHeldByCurrentThread(); + // remove/retire (in preparation of sending a RETIRE_CONNECTION_ID frames) + for (Iterator> iterator = peerConnectionIds.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + final long seqNumToRetire = entry.getKey(); + if (seqNumToRetire >= priorTo) { + break; + } + iterator.remove(); + toRetire.add(seqNumToRetire); + // Note that the QuicEndpoint only stores local connection ids and doesn't store peer + // connection ids. It does however store the peer-issued stateless reset token of a + // peer connection id, so we let the endpoint know that the stateless reset token needs + // to be forgotten since the corresponding peer connection id is being retired + final byte[] resetTokenToForget = entry.getValue().getStatelessResetToken(); + if (resetTokenToForget != null) { + this.connection.endpoint().forgetStatelessResetToken(resetTokenToForget); + } + } + for (Iterator iterator = gaps.iterator(); iterator.hasNext(); ) { + Long gap = iterator.next(); + if (gap >= priorTo) { + return; + } + iterator.remove(); + toRetire.add(gap); + } + } + + /** + * Produce a queued RETIRE_CONNECTION_ID frame, if it fits in the packet + * + * @param remaining bytes remaining in the packet + * @return a RetireConnectionIdFrame, or null if none is queued or remaining is too low + */ + public QuicFrame nextFrame(int remaining) { + // retire connection id: + // type - 1 byte + // sequence number - var int + if (remaining < 9) { + return null; + } + lock.lock(); + try { + final Long seqNumToRetire = toRetire.poll(); + if (seqNumToRetire != null) { + if (seqNumToRetire == activeConnIdSeq) { + // can't send this connection ID yet, we will send it in the next packet + toRetire.add(seqNumToRetire); + return null; + } + return new RetireConnectionIDFrame(seqNumToRetire); + } + return null; + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java new file mode 100644 index 00000000000..0c6f946d1a2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + + +/** + * A free-form connection ID to wrap the connection ID bytes + * sent by the peer. + * Client and server might impose some structure on the + * connection ID bytes. For instance, they might choose to + * encode the connection ID length in the connection ID bytes. + * This class makes no assumption on the structure of the + * connection id bytes. + */ +public final class PeerConnectionId extends QuicConnectionId { + private final byte[] statelessResetToken; + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + */ + public PeerConnectionId(final byte[] connId) { + super(ByteBuffer.wrap(connId.clone())); + this.statelessResetToken = null; + } + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + * @param statelessResetToken The stateless reset token to be associated with this connection id. + * Can be null. + * @throws IllegalArgumentException If the {@code statelessResetToken} is non-null and if its + * length isn't 16 bytes + * + */ + public PeerConnectionId(final ByteBuffer connId, final byte[] statelessResetToken) { + super(cloneBuffer(connId)); + if (statelessResetToken != null) { + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + + statelessResetToken.length); + } + this.statelessResetToken = statelessResetToken.clone(); + } else { + this.statelessResetToken = null; + } + } + + private static ByteBuffer cloneBuffer(ByteBuffer src) { + final byte[] idBytes = new byte[src.remaining()]; + src.get(idBytes); + return ByteBuffer.wrap(idBytes); + } + + /** + * {@return the stateless reset token associated with this connection id. returns null if no + * token exists} + */ + public byte[] getStatelessResetToken() { + return this.statelessResetToken == null ? null : this.statelessResetToken.clone(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "(length:" + length() + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java new file mode 100644 index 00000000000..58a07c22f66 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongFunction; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.AltServicesRegistry; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicEndpointFactory; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class represents a QuicClient. + * The QuicClient is responsible for creating/returning instances + * of QuicConnection for a given AltService, and for linking them + * with an instance of QuicEndpoint and QuicSelector for reading + * and writing Datagrams off the network. + * A QuicClient is also a factory for QuicConnectionIds. + * There is a 1-1 relationship between a QuicClient and an Http3Client. + * A QuicClient can be closed: closing a QuicClient will close all + * quic connections opened on that client. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public final class QuicClient implements QuicInstance, AutoCloseable { + private static final AtomicLong IDS = new AtomicLong(); + private static final AtomicLong CONNECTIONS = new AtomicLong(); + + private final Logger debug = Utils.getDebugLogger(this::name); + + // See RFC 9000 section 14 + static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = 1200; + static final int INITIAL_SERVER_CONNECTION_ID_LENGTH = 17; + static final int MAX_ENDPOINTS_LIMIT = 16; + static final int DEFAULT_MAX_ENDPOINTS = Utils.getIntegerNetProperty( + "jdk.httpclient.quic.maxEndpoints", 1); + + private final String clientId; + private final String name; + private final Executor executor; + private final QuicTLSContext quicTLSContext; + private final SSLParameters sslParameters; + // QUIC versions in their descending order of preference + private final List availableVersions; + private final InetSocketAddress bindAddress; + private final QuicTransportParameters transportParams; + private final ReentrantLock lock = new ReentrantLock(); + private final QuicEndpoint[] endpoints = new QuicEndpoint[computeMaxEndpoints()]; + private int insertionPoint; + private volatile QuicSelector selector; + private volatile boolean closed; + // keep track of any initial tokens that a server has advertised for use. The key in this + // map is the server's host and port representation and the value is the token to use. + private final Map initialTokens = new ConcurrentHashMap<>(); + private final QuicEndpointFactory endpointFactory = new QuicEndpointFactory(); + private final LongFunction appErrorCodeToString; + + private QuicClient(final QuicClient.Builder builder) { + Objects.requireNonNull(builder, "Quic client builder"); + if (builder.availableVersions == null) { + throw new IllegalStateException("Need at least one available Quic version"); + } + if (builder.tlsContext == null) { + throw new IllegalStateException("No QuicTLSContext set"); + } + this.clientId = builder.clientId == null ? nextName() : builder.clientId; + this.name = "QuicClient(%s)".formatted(clientId); + this.appErrorCodeToString = builder.appErrorCodeToString == null + ? QuicInstance.super::appErrorToString + : builder.appErrorCodeToString; + // verify that QUIC TLS supports all requested QUIC versions + var test = new ArrayList<>(builder.availableVersions); + test.removeAll(builder.tlsContext.createEngine().getSupportedQuicVersions()); + if (!test.isEmpty()) { + throw new IllegalArgumentException( + "Requested QUIC versions not supported by TLS: " + test); + } + this.availableVersions = builder.availableVersions; + this.quicTLSContext = builder.tlsContext; + this.bindAddress = builder.bindAddr == null ? new InetSocketAddress(0) : builder.bindAddr; + this.executor = builder.executor; + this.sslParameters = builder.sslParams == null + ? new SSLParameters() + : requireTLS13(builder.sslParams); + this.transportParams = builder.transportParams; + if (debug.on()) debug.log("created"); + } + + + private static int computeMaxEndpoints() { + // available processors may change according to the API doc, + // so recompute this for each new client... + int availableProcessors = Runtime.getRuntime().availableProcessors(); + int max = DEFAULT_MAX_ENDPOINTS <= 0 ? availableProcessors >> 1 : DEFAULT_MAX_ENDPOINTS; + return Math.clamp(max, 1, MAX_ENDPOINTS_LIMIT); + } + + // verifies that the TLS protocol(s) configured in SSLParameters, if any, + // allows TLSv1.3 + private static SSLParameters requireTLS13(final SSLParameters parameters) { + final String[] protos = parameters.getProtocols(); + if (protos == null || protos.length == 0) { + // no specific protocols specified, so it's OK + return parameters; + } + for (final String proto : protos) { + if ("TLSv1.3".equals(proto)) { + // TLSv1.3 is allowed, that's good + return parameters; + } + } + // explicit TLS protocols have been configured in SSLParameters and it doesn't + // include TLSv1.3. QUIC mandates TLSv1.3, so we can't use this SSLParameters + throw new IllegalArgumentException("TLSv1.3 is required for QUIC," + + " but SSLParameters is configured with " + Arrays.toString(protos)); + } + + @Override + public String appErrorToString(long code) { + return appErrorCodeToString.apply(code); + } + + @Override + public QuicTransportParameters getTransportParameters() { + if (this.transportParams == null) { + return null; + } + // return a copy + return new QuicTransportParameters(this.transportParams); + } + + private static String nextName() { + return "quic-client-" + IDS.incrementAndGet(); + } + + /** + * The address that the QuicEndpoint will bind to. + * @implNote By default, this is wildcard:0 + * @return the address that the QuicEndpoint will bind to. + */ + public InetSocketAddress bindAddress() { + return bindAddress; + } + + @Override + public boolean isVersionAvailable(final QuicVersion quicVersion) { + return this.availableVersions.contains(quicVersion); + } + + /** + * {@return the versions that are available for use on this instance, in the descending order + * of their preference} + */ + @Override + public List getAvailableVersions() { + return this.availableVersions; + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code service}. + * + * @param service the alternate service for which to create the connection for + * @return a new unconnected {@code QuicConnection} + * @throws IllegalArgumentException if the ALPN of this transport isn't the same as that of the + * passed alternate service + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final AltServicesRegistry.AltService service) { + final InetSocketAddress peerAddress = new InetSocketAddress(service.identity().host(), + service.identity().port()); + final String alpn = service.alpn(); + if (alpn == null) { + throw new IllegalArgumentException("missing ALPN on alt service"); + } + final SSLParameters sslParameters = createSSLParameters(new String[]{alpn}); + return new QuicConnectionImpl(null, this, peerAddress, + service.origin().host(), service.origin().port(), sslParameters, "QuicClientConnection(%s)", + CONNECTIONS.incrementAndGet()); + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code peerAddress}. + * + * @param peerAddress the address of the peer + * @return a new unconnected {@code QuicConnection} + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final InetSocketAddress peerAddress, + final String[] alpns) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(alpns); + if (alpns.length == 0) { + throw new IllegalArgumentException("at least one ALPN is needed"); + } + final SSLParameters sslParameters = createSSLParameters(alpns); + return new QuicConnectionImpl(null, this, peerAddress, peerAddress.getHostString(), + peerAddress.getPort(), sslParameters, "QuicClientConnection(%s)", CONNECTIONS.incrementAndGet()); + } + + private SSLParameters createSSLParameters(final String[] alpns) { + final SSLParameters sslParameters = Utils.copySSLParameters(this.getSSLParameters()); + sslParameters.setApplicationProtocols(alpns); + // section 4.2, RFC-9001 (QUIC) Clients MUST NOT offer TLS versions older than 1.3 + sslParameters.setProtocols(new String[] {"TLSv1.3"}); + return sslParameters; + } + + @Override + public String instanceId() { + return clientId; + } + + @Override + public QuicTLSContext getQuicTLSContext() { + return quicTLSContext; + } + + @Override + public SSLParameters getSSLParameters() { + return Utils.copySSLParameters(sslParameters); + } + + /** + * The name identifying this QuicClient, used in debug traces. + * @implNote This is {@code "QuicClient()"}. + * @return the name identifying this QuicClient. + */ + public String name() { + return name; + } + + /** + * The HttpClientImpl Id. used to identify the client in + * debug traces. + * @return A string identifying the HttpClientImpl instance. + */ + public String clientId() { + return clientId; + } + + /** + * The executor used by this QuicClient when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + @Override + public Executor executor() { + return executor; + } + + @Override + public QuicEndpoint getEndpoint() throws IOException { + return chooseEndpoint(); + } + + private QuicEndpoint chooseEndpoint() throws IOException { + QuicEndpoint endpoint; + lock.lock(); + try { + if (closed) throw new IllegalStateException("QuicClient is closed"); + int index = insertionPoint; + if (index >= endpoints.length) index = 0; + endpoint = endpoints[index]; + if (endpoint != null) { + if (endpoints.length == 1) return endpoint; + if (endpoint.connectionCount() < 2) return endpoint; + for (int i = 1; i < endpoints.length - 1; i++) { + var nexti = (index + i) % endpoints.length; + var next = endpoints[nexti]; + if (next == null) continue; + if (next.connectionCount() < endpoint.connectionCount()) { + endpoint = next; + index = nexti; + } + } + if (++index >= endpoints.length) index = 0; + insertionPoint = index; + + if (Log.quicControl()) { + Log.logQuic("Selecting endpoint: " + endpoint.name()); + } else if (debug.on()) { + debug.log("Selecting endpoint: " + endpoint.name()); + } + + return endpoint; + } + + final var endpointName = "QuicEndpoint(" + clientId + "-" + index + ")"; + if (Log.quicControl()) { + Log.logQuic("Adding new endpoint: " + endpointName); + } else if (debug.on()) { + debug.log("Adding new endpoint: " + endpointName); + } + endpoint = createEndpoint(endpointName); + assert endpoints[index] == null; + endpoints[index] = endpoint; + insertionPoint = index + 1; + } finally { + lock.unlock(); + } + // register the newly created endpoint with the selector + QuicEndpoint.registerWithSelector(endpoint, selector, debug); + return endpoint; + } + + /** + * Creates an endpoint with the given name, and register it with a selector. + * @return the new QuicEndpoint + * @throws IOException if an error occurs when setting up the selector + * or linking the transport with the selector. + * @throws IllegalStateException if the client is closed. + */ + private QuicEndpoint createEndpoint(final String endpointName) throws IOException { + var selector = this.selector; + boolean newSelector = false; + final QuicEndpoint.ChannelType configuredChannelType = QuicEndpoint.CONFIGURED_CHANNEL_TYPE; + if (selector == null) { + // create a selector first + lock.lock(); + try { + if (closed) { + throw new IllegalStateException("QuicClient is closed"); + } + selector = this.selector; + if (selector == null) { + final String selectorName = "QuicSelector(" + clientId + ")"; + selector = this.selector = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + QuicSelector.createQuicNioSelector(this, selectorName); + case BLOCKING_WITH_VIRTUAL_THREADS -> + QuicSelector.createQuicVirtualThreadPoller(this, selectorName); + }; + newSelector = true; + } + } finally { + lock.unlock(); + } + } + if (newSelector) { + // we may be closed when we reach here. It doesn't matter though. + // if the selector is closed before it's started the thread will + // immediately exit (or exit after the first wakeup) + selector.start(); + } + final QuicEndpoint endpoint = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + endpointFactory.createSelectableEndpoint(this, endpointName, + bindAddress(), selector.timer()); + case BLOCKING_WITH_VIRTUAL_THREADS -> + endpointFactory.createVirtualThreadedEndpoint(this, endpointName, + bindAddress(), selector.timer()); + }; + assert endpoint.channelType() == configuredChannelType + : "bad endpoint for " + configuredChannelType + ": " + endpoint.getClass(); + return endpoint; + } + + @Override + public void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer) { + if (debug.on()) { + debug.log("dropping unmatched packet in buffer [%s, %d bytes, %s]", + type, buffer.remaining(), source); + } + } + + /** + * @param peerAddress The address of the server + * @return the initial token to use in INITIAL packets during connection establishment + * against a server represented by the {@code peerAddress}. Returns null if no token exists for + * the server. + */ + byte[] initialTokenFor(final InetSocketAddress peerAddress) { + if (peerAddress == null) { + return null; + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // an initial token (obtained through NEW_TOKEN frame) can be used only once against the + // peer which advertised it. Hence, we remove it. + return this.initialTokens.remove(recipient); + } + + /** + * Registers a token to use in INITIAL packets during connection establishment against a server + * represented by the {@code peerAddress}. + * + * @param peerAddress The address of the server + * @param token The token to use + * @throws NullPointerException If either of {@code peerAddress} or {@code token} is null + * @throws IllegalArgumentException If the token is of zero length + */ + void registerInitialToken(final InetSocketAddress peerAddress, final byte[] token) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(token); + if (token.length == 0) { + throw new IllegalArgumentException("Empty token"); + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // multiple initial tokens (through NEW_TOKEN frame) can be sent by the same peer, but as + // per RFC-9000, section 8.1.3, it's OK for clients to just use the last received token, + // since the rest are less likely to be useful + this.initialTokens.put(recipient, token); + } + + @Override + public void close() { + // TODO: ignore exceptions while closing? + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) closeEndpoint(endpoint); + } + var selector = this.selector; + if (selector != null) selector.close(); + } + + private void closeEndpoint(QuicEndpoint endpoint) { + try { endpoint.close(); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close endpoint: %s: %s", endpoint.name(), t); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) abortEndpoint(endpoint, t); + } + var selector = this.selector; + if (selector != null) selector.abort(t); + } + + private void abortEndpoint(QuicEndpoint endpoint, Throwable cause) { + try { endpoint.abort(cause); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to abort endpoint: %s: %s", endpoint.name(), t); + } + } + } + + private record InitialTokenRecipient (String host, int port) { + } + + public static final class Builder { + private String clientId; + private List availableVersions; + private Executor executor; + private SSLParameters sslParams; + private QuicTLSContext tlsContext; + private QuicTransportParameters transportParams; + private InetSocketAddress bindAddr; + private LongFunction appErrorCodeToString; + + public Builder availableVersions(final List versions) { + Objects.requireNonNull(versions, "Quic versions"); + if (versions.isEmpty()) { + throw new IllegalArgumentException("Need at least one available Quic version"); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder applicationErrors(LongFunction errorCodeToString) { + this.appErrorCodeToString = errorCodeToString; + return this; + } + + public Builder availableVersions(final QuicVersion version, final QuicVersion... more) { + Objects.requireNonNull(version, "Quic version"); + if (more == null) { + this.availableVersions = List.of(version); + return this; + } + final List versions = new ArrayList<>(); + versions.add(version); + for (final QuicVersion v : more) { + Objects.requireNonNull(v, "Quic version"); + versions.add(v); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder clientId(final String clientId) { + this.clientId = clientId; + return this; + } + + public Builder tlsContext(final QuicTLSContext tlsContext) { + this.tlsContext = tlsContext; + return this; + } + + public Builder sslParameters(final SSLParameters sslParameters) { + this.sslParams = sslParameters; + return this; + } + + public Builder bindAddress(final InetSocketAddress bindAddr) { + this.bindAddr = bindAddr; + return this; + } + + public Builder executor(final Executor executor) { + this.executor = executor; + return this; + } + + public Builder transportParameters(final QuicTransportParameters transportParams) { + this.transportParams = transportParams; + return this; + } + + public QuicClient build() { + return new QuicClient(this); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java new file mode 100644 index 00000000000..4bfad2c5560 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; + +public interface QuicCongestionController { + + /** + * {@return true if a new non-ACK packet can be sent at this time} + */ + boolean canSendPacket(); + + /** + * Update the maximum datagram size + * @param newSize new maximum datagram size. + */ + void updateMaxDatagramSize(int newSize); + + /** + * Update CC with a non-ACK packet + * @param packetBytes packet size in bytes + */ + void packetSent(int packetBytes); + + /** + * Update CC after a non-ACK packet is acked + * + * @param packetBytes acked packet size in bytes + * @param sentTime time when packet was sent + */ + void packetAcked(int packetBytes, Deadline sentTime); + + /** + * Update CC after packets are declared lost + * + * @param lostPackets collection of lost packets + * @param sentTime time when the most recent lost packet was sent + * @param persistent true if persistent congestion detected, false otherwise + */ + void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent); + + /** + * Update CC after packets are discarded + * @param discardedPackets collection of discarded packets + */ + void packetDiscarded(Collection discardedPackets); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java new file mode 100644 index 00000000000..05bfa1adc8c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.quic.QuicTLSEngine; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server + * over a QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the + * same server. + * This abstract class exposes public methods used by the higher level + * protocol. + * + *

      A typical call flow to establish a connection would be: + * {@snippet : + * AltService service = ...; + * QuicClient client = ...; + * QuicConnection connection = client.createConnectionFor(service); + * connection.startHandshake() + * .thenApply((r) -> { ... }) + * ...; + * } + * + */ +public abstract class QuicConnection { + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public abstract CompletableFuture startHandshake(); + + /** + * Creates a new locally initiated bidirectional stream. + *

      + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

      + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the bidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * bidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalBidiStream( + Duration limitIncreaseDuration); + + /** + * Creates a new locally initiated unidirectional stream. Locally created unidirectional streams + * are write-only streams. + *

      + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

      + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the unidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * unidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalUniStream( + Duration limitIncreaseDuration); + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote + * The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) stream type} of the given + * streamId. + * The listener should return {@code true} if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + */ + public abstract void addRemoteStreamListener(Predicate streamConsumer); + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public abstract boolean removeRemoteStreamListener(Predicate streamConsumer); + + /** + * {@return a stream of all currently opened {@link QuicStream} in the connection} + * + * @apiNote + * All current quic streams are included, whether local or remote, and whether they + * have been acquired or not. + * + * @see #addRemoteStreamListener(Predicate) + */ + public abstract Stream quicStreams(); + + /** + * {@return true if this connection is open} + */ + public abstract boolean isOpen(); + + /** + * {@return a long identifier that can be used to uniquely + * identify a quic connection in the context of the + * {@link QuicInstance} that created it} + */ + public long uniqueId() { return 0; } + + /** + * {@return a debug tag to be used with {@linkplain + * jdk.internal.net.http.common.Logger lower level logging}} + * This typically includes both the connection {@link #uniqueId()} + * and the {@link QuicInstance#instanceId()}. + */ + public abstract String dbgTag(); + + /** + * {@return a debug tag} + * Typically used with {@linkplain jdk.internal.net.http.common.Log + * higher level logging} + */ + public abstract String logTag(); + + /** + * {@return the {@link TerminationCause} if the connection has + * closed or is being closed, otherwise returns null} + */ + public abstract TerminationCause terminationCause(); + + public abstract QuicTLSEngine getTLSEngine(); + + public abstract InetSocketAddress peerAddress(); + + public abstract SocketAddress localAddress(); + + /** + * {@return a {@code CompletableFuture} that gets completed when + * the peer has acknowledged, or replied to the first {@link + * QuicPacket.PacketType#INITIAL INITIAL} + * packet + */ + public abstract CompletableFuture handshakeReachedPeer(); + + /** + * Requests to send a PING frame to the peer. + * An implementation may decide to support sending of out-of-band ping + * frames (triggered by the application layer) only for a subset of the + * {@linkplain jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace + * packet number spaces}. It may complete with -1 if it doesn't want to request + * sending of a ping frame at the time {@code requestSendPing()} is called. + * @return A completable future that will be completed with the number of + * milliseconds it took to get a valid response. It may also complete + * exceptionally, or with {@code -1L} if the ping was not sent. + */ + public abstract CompletableFuture requestSendPing(); + + /** + * {@return this connection {@code QuicConnectionId} or null} + * @implSpec + * The default implementation of this method returns null + */ + public QuicConnectionId localConnectionId() { return null; } + + /** + * {@return the {@link ConnectionTerminator} for this connection} + */ + public abstract ConnectionTerminator connectionTerminator(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java new file mode 100644 index 00000000000..21c40a69588 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + +/** + * Models a Quic Connection id. + * QuicConnectionId instance are typically created by a Quic client or server. + */ +// Connection IDs are used as keys in an ID to connection map. +// They implement Comparable to mitigate the penalty of hash collisions. +public abstract class QuicConnectionId implements Comparable { + + /** + * The maximum length, in bytes, of a connection id. + * This is supposed to be version-specific, but for now, we + * are going to treat that as a universal constant. + */ + public static final int MAX_CONNECTION_ID_LENGTH = 20; + protected final int hashCode; + protected final ByteBuffer buf; + + protected QuicConnectionId(ByteBuffer buf) { + this.buf = buf.asReadOnlyBuffer(); + hashCode = this.buf.hashCode(); + } + + /** + * Returns the length of this connection id, in bytes; + * @return the length of this connection id + */ + public int length() { + return buf.remaining(); + } + + /** + * Returns this connection id bytes as a read-only buffer. + * @return A new read only buffer containing this connection id bytes. + */ + public ByteBuffer asReadOnlyBuffer() { + return buf.asReadOnlyBuffer(); + } + + /** + * Returns this connection id bytes as a byte array. + * @return A new byte array containing this connection id bytes. + */ + public byte[] getBytes() { + var length = length(); + byte[] bytes = new byte[length]; + buf.get(buf.position(), bytes, 0, length); + return bytes; + } + + /** + * Compare this connection id bytes with the bytes in the + * given byte buffer. + *

      The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

      This method does not advance the buffer position. + * + * @implSpec This is equivalent to:

      {@code
      +     *  this.asReadOnlyBuffer().comparesTo(idbytes)
      +     *  }
      + * + * @param idbytes A byte buffer containing the id bytes of another + * connection id. + * @return {@code -1}, {@code 0}, or {@code 1} if this connection's id + * bytes are less, equal, or greater than the provided bytes. + */ + public int compareBytes(ByteBuffer idbytes) { + return buf.compareTo(idbytes); + } + + /** + * Tells whether the given byte buffer matches this connection id. + * The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

      This method does not advance the buffer position. + * + * @implSpec + * This is equivalent to:

      {@code
      +     *  this.asReadOnlyBuffer().mismatch(idbytes) == -1
      +     *  }
      + * + * @param idbytes A buffer that delimits a connection id. + * @return true if the bytes in the given buffer match this + * connection id bytes. + */ + public boolean matches(ByteBuffer idbytes) { + return buf.equals(idbytes); + } + + @Override + public int compareTo(QuicConnectionId o) { + return buf.compareTo(o.buf); + } + + + @Override + public final boolean equals(Object o) { + if (o instanceof QuicConnectionId that) { + return buf.equals(that.buf); + } + return false; + } + + @Override + public final int hashCode() { + return hashCode; + } + + /** + * {@return an hexadecimal string representing this connection id bytes, + * as returned by {@code HexFormat.of().formatHex(getBytes())}} + */ + public String toHexString() { + return HexFormat.of().formatHex(getBytes()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java new file mode 100644 index 00000000000..04cb2e6c263 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A class to generate connection ids bytes. + * This algorithm is specific to our implementation - it's not defined + * in any RFC (connection id bytes are free form). + * For the purpose of validation we encode the length of + * the connection id into the connection id bytes. + * For the purpose of uniqueness we encode a unique id. + * The rest of the connection id are random bytes. + */ +public class QuicConnectionIdFactory { + private static final Random RANDOM = new SecureRandom(); + private static final String CLIENT_DESC = "QuicClientConnectionId"; + private static final String SERVER_DESC = "QuicServerConnectionId"; + + private static final int MIN_CONNECTION_ID_LENGTH = 9; + + private final AtomicLong tokens = new AtomicLong(); + private volatile boolean wrapped; + private final byte[] scrambler; + private final Key statelessTokenKey; + private final String simpleDesc; + private final int connectionIdLength = RANDOM.nextInt(MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH+1); + + public static QuicConnectionIdFactory getClient() { + return new QuicConnectionIdFactory(CLIENT_DESC); + } + + public static QuicConnectionIdFactory getServer() { + return new QuicConnectionIdFactory(SERVER_DESC); + } + + private QuicConnectionIdFactory(String simpleDesc) { + this.simpleDesc = simpleDesc; + byte[] temp = new byte[MAX_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(temp); + scrambler = temp; + try { + KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256"); + statelessTokenKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("HmacSHA256 key generator not available", e); + } + } + + /** + * The connection ID length used by this Quic instance. + * This is the source connection id length for outgoing packets, + * and the destination connection id length for incoming packets. + * @return the connection ID length used by this instance + */ + public int connectionIdLength() { + return connectionIdLength; + } + + /** + * Creates a new connection ID for a connection. + * @return a new connection ID + */ + public QuicConnectionId newConnectionId() { + long token = newToken(); + return new QuicLocalConnectionId(token, simpleDesc, + newConnectionId(connectionIdLength, token)); + } + + /** + * Quick validation to see if the buffer can contain a connection + * id generated by this instance. The byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

      This method does not advance the buffer position, and + * returns a connection id that wraps the given buffer. + * The returned connection id is only safe to use as long as + * the buffer is not modified. + *

      It is usually only used temporarily as a lookup key + * to locate an existing {@code QuicConnection}. + * + * @param buffer A buffer that delimits a connection id. + * @return a new QuicConnectionId if the buffer can contain + * a connection id generated by this instance, {@code null} + * otherwise. + */ + public QuicConnectionId unsafeConnectionIdFor(ByteBuffer buffer) { + int expectedLength = connectionIdLength; + + int remaining = buffer.remaining(); + if (remaining < MIN_CONNECTION_ID_LENGTH) return null; + if (remaining != expectedLength) return null; + + byte first = buffer.get(0); + int len = extractConnectionIdLength(first); + if (len < MIN_CONNECTION_ID_LENGTH) return null; + if (len > MAX_CONNECTION_ID_LENGTH) return null; + if (len != expectedLength) return null; + + long token = peekConnectionIdToken(buffer); + if (!isValidToken(token)) return null; + var cid = new QuicLocalConnectionId(buffer, token, simpleDesc); + assert cid.length() == expectedLength; + return cid; + } + + /** + * Returns a stateless reset token for the given connection ID + * @param connectionId connection ID + * @return stateless reset token for the given connection ID + * @throws IllegalArgumentException if the connection ID was not generated by this factory + */ + public byte[] statelessTokenFor(QuicConnectionId connectionId) { + if (!(connectionId instanceof QuicLocalConnectionId)) { + throw new IllegalArgumentException("Not a locally-generated connection ID"); + } + Mac mac; + try { + mac = Mac.getInstance("HmacSHA256"); + mac.init(statelessTokenKey); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("HmacSHA256 is not available", e); + } + byte[] result = mac.doFinal(connectionId.getBytes()); + return Arrays.copyOf(result, 16); + } + + // visible for testing + public long newToken() { + var token = tokens.incrementAndGet(); + if (token < 0) { + token = -token - 1; + wrapped = true; + } + return token; + } + + // visible for testing + public byte[] newConnectionId(int length, long token) { + length = Math.clamp(length, MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH); + assert length <= MAX_CONNECTION_ID_LENGTH; + assert length >= MIN_CONNECTION_ID_LENGTH; + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + + if (token < 0) token = -token - 1; + assert token >= 0; + int len = variableLengthLength(token); + assert len < 8; + + bytes[0] = (byte) ((length << 3) & 0xF8); + bytes[0] = (byte) (bytes[0] | len); + assert (bytes[0] & 0x07) == len; + assert ((bytes[0] & 0xFF) >> 3) == length : + "%s != %s".formatted(bytes[0] & 0xFF, length); + int shift = 8 * len; + for (int i = 0; i <= len; i++) { + assert shift <= 56; + bytes[i + 1] = (byte) ((token >> shift) & 0xFF); + shift -= 8; + } + for (int i = 0; i < length; i++) { + bytes[i] = (byte) ((bytes[i] & 0xFF) ^ (scrambler[i] & 0xFF)); + } + + assert length == getConnectionIdLength(bytes); + assert token == getConnectionIdToken(bytes); + return bytes; + } + + // visible for testing + public int getConnectionIdLength(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + var length = extractConnectionIdLength(bytes[0]); + assert length <= MAX_CONNECTION_ID_LENGTH; + return length; + } + + // visible for testing + public long getConnectionIdToken(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes[0]); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes[j] & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + assert token >= 0; + return token; + } + + private long peekConnectionIdToken(ByteBuffer bytes) { + assert bytes.remaining() >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes.get(0)); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes.get(j) & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + return token; + } + + private boolean isValidToken(long token) { + if (token < 0) return false; + long prevToken = tokens.get(); + boolean wrapped = prevToken < 0 || this.wrapped; + // if `tokens` has wrapped, we can say nothing... + // otherwise, we can say it should not be coded on more bytes than + // the previous token that was distributed + if (!wrapped) { + return token <= prevToken; + } + return true; + } + + private int extractConnectionIdLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + bits = bits >> 3; + return bits; + } + + private int extractTokenLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + return bits & 0x07; + } + + private static int variableLengthLength(long token) { + assert token >= 0; + int len = 0; + int shift = 0; + for (int i = 1; i < 8; i++) { + shift += 8; + if ((token >> shift) == 0) break; + len++; + } + assert len < 8; + return len; + } + + /** + * Checks if {@code connId} looks like a connection ID we could possibly generate. + * If it does, returns a stateless reset datagram. + * @param connId the destination connection id that was received on the packet + * @param length maximum length of the stateless reset packet + * @return stateless reset datagram payload, or null + */ + public ByteBuffer statelessReset(ByteBuffer connId, int length) { + // 43 bytes max: + // first byte bits 01xx xxxx + // followed by random bytes + // terminated by 16 bytes reset token + length = Math.min(length, 43); + if (length < 21) { // minimum QUIC short datagram length + return null; + } + + var cid = (QuicLocalConnectionId)unsafeConnectionIdFor(connId); + if (cid != null) { + var localToken = statelessTokenFor(cid); + assert localToken != null; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.put((byte)(0x40 + RANDOM.nextInt(0x40))); + byte[] random = new byte[length - 17]; + RANDOM.nextBytes(random); + buf.put(random); + buf.put(localToken); + assert !buf.hasRemaining() : buf.remaining(); + buf.flip(); + return buf; + } + return null; + } + + // A connection id generated by this instance. + private static final class QuicLocalConnectionId extends QuicConnectionId { + private final long token; + private final String simpleDesc; + + // Connection Ids created with this constructor are safer + // to use in maps as the buffer wraps a safe byte array in + // this constructor. + private QuicLocalConnectionId(long token, String simpleDesc, byte[] bytes) { + super(ByteBuffer.wrap(bytes)); + this.token = token; + this.simpleDesc = simpleDesc; + } + + // Connection Ids created with this constructor are only + // safe to use as long as the caller abstain from mutating + // the provided byte buffer. + // Typically, they will be transiently used to look up some + // connection in a map indexed by a connection id. + private QuicLocalConnectionId(ByteBuffer buffer, long token, String simpleDesc) { + super(buffer); + assert token >= 0; + this.token = token; + this.simpleDesc = simpleDesc; + } + + @Override + public String toString() { + return "%s(length=%s, token=%s, hash=%s)" + .formatted(simpleDesc, length(), token, hashCode); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java new file mode 100644 index 00000000000..f05519d339b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -0,0 +1,4353 @@ +/* + * Copyright (c) 2020, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.net.ConnectException; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NetworkChannel; +import java.nio.channels.UnresolvedAddressException; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.CryptoDataFlow; +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.QuicTransportParameters.VersionInformation; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.DataBlockedFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.MaxDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PathChallengeFrame; +import jdk.internal.net.http.quic.frames.PathResponseFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.http.quic.packets.HandshakePacket; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder.OutgoingQuicPacket; +import jdk.internal.net.http.quic.packets.RetryPacket; +import jdk.internal.net.http.quic.packets.VersionNegotiationPacket; +import jdk.internal.net.http.quic.streams.CryptoWriterQueue; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicBidiStreamImpl; +import jdk.internal.net.http.quic.streams.QuicConnectionStreams; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.HandshakeState; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicVersion; + +import static jdk.internal.net.http.quic.QuicClient.INITIAL_SERVER_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.active_connection_id_limit; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_data; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_idle_timeout; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_udp_payload_size; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.version_information; +import static jdk.internal.net.http.quic.TerminationCause.forException; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.QuicRttEstimator.MIN_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.streams.QuicStreams.isUnidirectional; +import static jdk.internal.net.http.quic.streams.QuicStreams.streamType; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server over a + * QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the same server. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicConnectionImpl extends QuicConnection implements QuicPacketReceiver { + + private static final int MAX_IPV6_MTU = 65527; + private static final int MAX_IPV4_MTU = 65507; + + // Quic assumes a minimum packet size of 1200 + // See https://www.rfc-editor.org/rfc/rfc9000#name-datagram-size + public static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = + QuicClient.SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + public static final int DEFAULT_MAX_INITIAL_TIMEOUT = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxInitialTimeout", 30), + 1, Integer.MAX_VALUE); + public static final long DEFAULT_INITIAL_MAX_DATA = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxInitialData", 15 << 20), + 0, 1L << 60); + public static final long DEFAULT_INITIAL_STREAM_MAX_DATA = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxStreamInitialData", 6 << 20), + 0, 1L << 60); + public static final int DEFAULT_MAX_BIDI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxBidiStreams", 100); + public static final int DEFAULT_MAX_UNI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxUniStreams", 100); + public static final boolean USE_DIRECT_BUFFER_POOL = Utils.getBooleanProperty( + "jdk.internal.httpclient.quic.poolDirectByteBuffers", !QuicEndpoint.DGRAM_SEND_ASYNC); + + public static final int RESET_TOKEN_LENGTH = 16; // RFC states 16 bytes for stateless token + public static final long MAX_STREAMS_VALUE_LIMIT = 1L << 60; // cannot exceed 2^60 as per RFC + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final VarHandle VERSION_NEGOTIATED; + private static final VarHandle STATE; + private static final VarHandle MAX_SND_DATA; + private static final VarHandle MAX_RCV_DATA; + public static final int DEFAULT_DATAGRAM_SIZE; + private static final int MAX_INCOMING_CRYPTO_CAPACITY = 64 << 10; + + static { + try { + Lookup lookup = MethodHandles.lookup(); + VERSION_NEGOTIATED = lookup + .findVarHandle(QuicConnectionImpl.class, "versionNegotiated", boolean.class); + STATE = lookup.findVarHandle(QuicConnectionImpl.class, "state", int.class); + MAX_SND_DATA = lookup.findVarHandle(OneRttFlowControlledSendingQueue.class, "maxData", long.class); + MAX_RCV_DATA = lookup.findVarHandle(OneRttFlowControlledReceivingQueue.class, "maxData", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + int size = Utils.getIntegerProperty("jdk.httpclient.quic.defaultMTU", + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2: + // The default for this parameter is the maximum permitted UDP payload of 65527. + // Values below 1200 are invalid. + if (size < SMALLEST_MAXIMUM_DATAGRAM_SIZE || size > MAX_IPV6_MTU) { + // fallback to SMALLEST_MAXIMUM_DATAGRAM_SIZE + size = SMALLEST_MAXIMUM_DATAGRAM_SIZE; + } + DEFAULT_DATAGRAM_SIZE = size; + } + + protected final Logger debug = Utils.getDebugLogger(this::dbgTag); + + final QuicRttEstimator rttEstimator = new QuicRttEstimator(); + final QuicCongestionController congestionController; + /** + * The state of the quic connection. + * The handshake is confirmed when HANDSHAKE_DONE has been received, + * or when the first 1-RTT packet has been successfully decrypted. + * See RFC 9001 section 4.1.2 + * https://www.rfc-editor.org/rfc/rfc9001#name-handshake-confirmed + */ + private final StateHandle stateHandle = new StateHandle(); + private final AtomicBoolean startHandshakeCalled = new AtomicBoolean(); + private final InetSocketAddress peerAddress; + private final QuicInstance quicInstance; + private final String dbgTag; + private final QuicTLSEngine quicTLSEngine; + private final CodingContext codingContext; + private final PacketSpaces packetSpaces; + private final OneRttFlowControlledSendingQueue oneRttSndQueue = + new OneRttFlowControlledSendingQueue(); + private final OneRttFlowControlledReceivingQueue oneRttRcvQueue = + new OneRttFlowControlledReceivingQueue(this::logTag); + protected final QuicConnectionStreams streams; + protected final Queue outgoing1RTTFrames = new ConcurrentLinkedQueue<>(); + // for one-rtt crypto data (session tickets) + private final CryptoDataFlow peerCryptoFlow = new CryptoDataFlow(); + private final CryptoWriterQueue localCryptoFlow = new CryptoWriterQueue(); + private final HandshakeFlow handshakeFlow = new HandshakeFlow(); + final ConnectionTerminatorImpl terminator; + protected final IdleTimeoutManager idleTimeoutManager; + protected final QuicTransportParameters transportParams; + // the initial (local) connection ID + private final QuicConnectionId connectionId; + private final PeerConnIdManager peerConnIdManager; + private final LocalConnIdManager localConnIdManager; + private volatile QuicConnectionId incomingInitialPacketSourceId; + protected final QuicEndpoint endpoint; + private volatile QuicTransportParameters localTransportParameters; + private volatile QuicTransportParameters peerTransportParameters; + private volatile byte[] initialToken; + // the number of (active) connection ids the peer is willing to accept for a given connection + private volatile long peerActiveConnIdsLimit = 2; // default is 2 as per RFC + + private volatile int state; + // the quic version currently in use + private volatile QuicVersion quicVersion; + // the quic version from the first packet + private final QuicVersion originalVersion; + private volatile QuicPacketDecoder decoder; + private volatile QuicPacketEncoder encoder; + // (client-only) if true, we no longer accept VERSIONS packets + private volatile boolean versionCompatible; + // if true, we no longer accept version changes + private volatile boolean versionNegotiated; + // true if we changed version in response to VERSIONS packet + private volatile boolean processedVersionsPacket; + // start off with 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int maxPeerAdvertisedPayloadSize = DEFAULT_DATAGRAM_SIZE; + // max MTU size on the connection: either MAX_IPV4_MTU or MAX_IPV6_MTU, + // depending on whether the peer address is IPv6 or IPv4 + private final int maxConnectionMTU; + // we start with a pathMTU that is 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int pathMTU = DEFAULT_DATAGRAM_SIZE; + private final SequentialScheduler handshakeScheduler = + SequentialScheduler.lockingScheduler(this::continueHandshake0); + private final ReentrantLock handshakeLock = new ReentrantLock(); + private final String cachedToString; + private final String logTag; + private final long labelId; + // incoming PATH_CHALLENGE frames waiting for PATH_RESPONSE + private final Queue pathChallengeFrameQueue = new ConcurrentLinkedQueue<>(); + + private volatile MaxInitialTimer maxInitialTimer; + + static String dbgTag(QuicInstance quicInstance, String logTag) { + return String.format("QuicConnection(%s, %s)", + quicInstance.instanceId(), logTag); + } + + protected QuicConnectionImpl(final QuicVersion firstFlightVersion, + final QuicInstance quicInstance, + final InetSocketAddress peerAddress, + final String peerName, + final int peerPort, + final SSLParameters sslParameters, + final String logTagFormat, + final long labelId) { + this.labelId = labelId; + this.quicInstance = Objects.requireNonNull(quicInstance, "quicInstance"); + try { + this.endpoint = quicInstance.getEndpoint(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + this.peerAddress = peerAddress; + this.maxConnectionMTU = peerAddress.getAddress() instanceof Inet6Address + ? MAX_IPV6_MTU + : MAX_IPV4_MTU; + this.pathMTU = Math.clamp(DEFAULT_DATAGRAM_SIZE, SMALLEST_MAXIMUM_DATAGRAM_SIZE, maxConnectionMTU); + this.cachedToString = String.format(logTagFormat.formatted("quic:%s:%s:%s"), labelId, + Arrays.toString(sslParameters.getApplicationProtocols()), peerAddress); + this.connectionId = this.endpoint.idFactory().newConnectionId(); + this.logTag = logTagFormat.formatted(labelId); + this.dbgTag = dbgTag(quicInstance, logTag); + this.congestionController = new QuicRenoCongestionController(dbgTag); + this.originalVersion = this.quicVersion = firstFlightVersion == null + ? QuicVersion.firstFlightVersion(quicInstance.getAvailableVersions()) + : firstFlightVersion; + final boolean isClientConn = isClientConnection(); + this.peerConnIdManager = new PeerConnIdManager(this, dbgTag); + this.localConnIdManager = new LocalConnIdManager(this, dbgTag, connectionId); + this.decoder = QuicPacketDecoder.of(this.quicVersion); + this.encoder = QuicPacketEncoder.of(this.quicVersion); + this.codingContext = new QuicCodingContext(); + final QuicTLSEngine engine = this.quicInstance.getQuicTLSContext() + .createEngine(peerName, peerPort); + engine.setUseClientMode(isClientConn); + engine.setSSLParameters(sslParameters); + this.quicTLSEngine = engine; + quicTLSEngine.setRemoteQuicTransportParametersConsumer(this::consumeQuicParameters); + packetSpaces = PacketSpaces.forConnection(this); + quicTLSEngine.setOneRttContext(packetSpaces.getOneRttContext()); + streams = new QuicConnectionStreams(this, debug); + if (quicInstance instanceof QuicClient quicClient) { + // use the (INITIAL) token that a server might have sent to this client (through + // NEW_TOKEN frame) on a previous connection against that server + this.initialToken = quicClient.initialTokenFor(this.peerAddress); + } + terminator = new ConnectionTerminatorImpl(this); + idleTimeoutManager = new IdleTimeoutManager(this); + transportParams = quicInstance.getTransportParameters() == null + ? new QuicTransportParameters() + : quicInstance.getTransportParameters(); + if (debug.on()) debug.log("Quic Connection Created"); + } + + @Override + public final long uniqueId() { + return labelId; + } + + /** + * An abstraction to represent the connection state as a bit mask. + * This is not an enum as some stages can overlap. + */ + public abstract static class QuicConnectionState { + public static final int + NEW = 0, // the connection is new + HISENT = 1, // first initial hello packet sent + HSCOMPLETE = 16, // handshake completed + CLOSING = 128, // connection has entered "Closing" state as defined in RFC-9000 + DRAINING = 256, // connection has entered "Draining" state as defined in RFC-9000 + CLOSED = 512; // CONNECTION_CLOSE ACK sent or received + public abstract int state(); + public boolean helloSent() {return isMarked(HISENT);} + public boolean handshakeComplete() { return isMarked(HSCOMPLETE);} + public boolean closing() { return isMarked(CLOSING);} + public boolean draining() { return isMarked(DRAINING);} + public boolean opened() { return (state() & (CLOSED | DRAINING | CLOSING)) == 0; } + public boolean isMarked(int mask) { return isMarked(state(), mask); } + public String toString() { return toString(state()); } + public static boolean isMarked(int state, int mask) { + return mask == 0 ? state == 0 : (state & mask) == mask; + } + public static String toString(int state) { + if (state == NEW) return "new"; + if (isMarked(state, CLOSED)) return "closed"; + if (isMarked(state, DRAINING)) return "draining"; + if (isMarked(state, CLOSING)) return "closing"; + if (isMarked(state, HSCOMPLETE)) return "handshakeComplete"; + if (isMarked(state, HISENT)) return "helloSent"; + return "Unknown(" + state + ")"; + } + } + + /** + * A {link QuicTimedEvent} used to interrupt the handshake + * if no response to the first initial packet is received within + * a reasonable delay (default is ~ 30s). + * This avoids waiting more than 30s for ConnectionException + * to be raised if no server is available at the peer address. + * This class is only used on the client side. + */ + final class MaxInitialTimer implements QuicTimedEvent { + private final Deadline maxInitialDeadline; + private final QuicTimerQueue timerQueue; + private final long eventId; + private volatile Deadline deadline; + private volatile boolean initialPacketReceived; + private volatile boolean connectionClosed; + + // optimization: if done is true it avoids volatile read + // of initialPacketReceived and/or connectionClosed + // from initialPacketReceived() + private boolean done; + private MaxInitialTimer(QuicTimerQueue timerQueue, Deadline maxDeadline) { + this.eventId = QuicTimerQueue.newEventId(); + this.timerQueue = timerQueue; + maxInitialDeadline = deadline = maxDeadline; + assert isClientConnection() : "MaxInitialTimer should only be used on QuicClients"; + } + + /** + * Called when an initial packet is received from the + * peer. At this point the MaxInitialTimer is disarmed, + * and further calls to this method are no-op. + */ + void initialPacketReceived() { + if (done) return; // races are OK - avoids volatile read + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (done = (firsPacketReceived || closed)) return; + initialPacketReceived = true; + if (debug.on()) { + debug.log("Quic initial timer disarmed after %s seconds", + DEFAULT_MAX_INITIAL_TIMEOUT - + Deadline.between(now(), maxInitialDeadline).toSeconds()); + } + if (!closed) { + // rescheduling with Deadline.MAX will take the + // MaxInitialTimer out of the timer queue. + timerQueue.reschedule(this, Deadline.MAX); + } + } + + @Override + public Deadline deadline() { + return deadline; + } + + /** + * This method is called if the timer expires. + * If no initial packet has been received ( + * {@link #initialPacketReceived()} was never called), + * the connection's handshakeCF is completed with a + * {@link ConnectException}. + * Calling this method a second time is a no-op. + * @return {@link Deadline#MAX}, always. + */ + @Override + public Deadline handle() { + if (done) return Deadline.MAX; + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (!firsPacketReceived && !closed) { + assert !now().isBefore(maxInitialDeadline); + var connectException = new ConnectException("No response from peer for %s seconds" + .formatted(DEFAULT_MAX_INITIAL_TIMEOUT)); + if (QuicConnectionImpl.this.handshakeFlow.handshakeCF() + .completeExceptionally(connectException)) { + // abandon the connection, but sends ConnectionCloseFrame + TerminationCause cause = TerminationCause.forException( + new QuicTransportException(connectException.getMessage(), + KeySpace.INITIAL, 0, QuicTransportErrors.APPLICATION_ERROR)); + terminator.terminate(cause); + } + connectionClosed = done = closed = true; + } + assert firsPacketReceived || closed; + return Deadline.MAX; + } + + @Override + public long eventId() { + return eventId; + } + + @Override + public Deadline refreshDeadline() { + boolean firstPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + Deadline newDeadlne = deadline; + if (closed || firstPacketReceived) newDeadlne = deadline = Deadline.MAX; + return newDeadlne; + } + + private Deadline now() { + return QuicConnectionImpl.this.endpoint().timeSource().instant(); + } + } + + /** + * A state handle is a mutable implementation of {@link QuicConnectionState} + * that allows to view the volatile connection int variable {@code state} as + * a {@code QuicConnectionState}, and provides methods to mutate it in + * a thread safe atomic way. + */ + protected final class StateHandle extends QuicConnectionState { + public int state() { return state;} + + /** + * Updates the state to a new state value with the passed bit {@code mask} set. + * + * @param mask The state mask + * @return true if previously the state value didn't have the {@code mask} set and this + * method successfully updated the state value to set the {@code mask} + */ + final boolean mark(final int mask) { + int state, desired; + do { + state = desired = state(); + if ((state & mask) == mask) return false; // already set + desired = state | mask; + } while (!STATE.compareAndSet(QuicConnectionImpl.this, state, desired)); + return true; // compareAndSet switched the old state to the desired state + } + public boolean markHelloSent() { return mark(HISENT); } + public boolean markHandshakeComplete() { return mark(HSCOMPLETE); } + } + + /** + * Keeps track of: + * - handshakeCF the handshake completable future + * - localInitial the local initial crypto writer queue + * - peerInitial the peer initial crypto flow + * - localHandshake the local handshake crypto queue + * - peerHandshake the peer handshake crypto flow + */ + protected final class HandshakeFlow { + + private final CompletableFuture handshakeCF; + // a CompletableFuture which will get completed when the handshake initiated locally, + // has "reached" the peer i.e. when the peer acknowledges or replies to the first + // INITIAL packet sent by an endpoint + final CompletableFuture handshakeReachedPeerCF; + private final CryptoWriterQueue localInitial = new CryptoWriterQueue(); + private final CryptoDataFlow peerInitial = new CryptoDataFlow(); + private final CryptoWriterQueue localHandshake = new CryptoWriterQueue(); + private final CryptoDataFlow peerHandshake = new CryptoDataFlow(); + private final AtomicBoolean handshakeStarted = new AtomicBoolean(); + + private HandshakeFlow() { + this.handshakeCF = new MinimalFuture<>(); + this.handshakeReachedPeerCF = new MinimalFuture<>(); + // ensure that the handshakeReachedPeerCF gets completed exceptionally + // if an exception is raised before the first INITIAL packet is + // acked by the peer. + handshakeCF.whenComplete((r, t) -> { + if (Log.quicHandshake()) { + Log.logQuic("{0} handshake completed {1}", + logTag(), + t == null ? "successfully" : ("exceptionally: " + t)); + } + if (t != null) { + handshakeReachedPeerCF.completeExceptionally(t); + } + }); + } + + /** + * {@return the CompletableFuture representing a handshake} + */ + public CompletableFuture handshakeCF() { + return this.handshakeCF; + } + + public void failHandshakeCFs(final Throwable cause) { + assert cause != null : "missing cause when failing handshake CFs"; + SSLHandshakeException sslHandshakeException = null; + if (!handshakeCF.isDone()) { + sslHandshakeException = sslHandshakeException(cause); + handshakeCF.completeExceptionally(sslHandshakeException); + } + if (!handshakeReachedPeerCF.isDone()) { + if (sslHandshakeException == null) { + sslHandshakeException = sslHandshakeException(cause); + } + handshakeReachedPeerCF.completeExceptionally(sslHandshakeException); + } + } + + private SSLHandshakeException sslHandshakeException(final Throwable cause) { + if (cause instanceof SSLHandshakeException ssl) { + return ssl; + } + return new SSLHandshakeException("QUIC connection establishment failed", cause); + } + + /** + * Marks the start of a handshake. + * @throws IllegalStateException If handshake has already started + */ + private void markHandshakeStart() { + if (!handshakeStarted.compareAndSet(false, true)) { + throw new IllegalStateException("Handshake has already started on " + + QuicConnectionImpl.this); + } + } + } + + public record PacketSpaces(PacketSpace initial, PacketSpace handshake, PacketSpace app) { + public PacketSpace get(PacketNumberSpace pnspace) { + return switch (pnspace) { + case INITIAL -> initial(); + case HANDSHAKE -> handshake(); + case APPLICATION -> app(); + default -> throw new IllegalArgumentException(String.valueOf(pnspace)); + }; + } + + private QuicOneRttContext getOneRttContext() { + final var appPacketSpaceMgr = app(); + assert appPacketSpaceMgr instanceof QuicOneRttContext + : "unexpected 1-RTT packet space manager"; + return (QuicOneRttContext) appPacketSpaceMgr; + } + + public static PacketSpaces forConnection(final QuicConnectionImpl connection) { + final var initialPktSpaceMgr = new PacketSpaceManager(connection, PacketNumberSpace.INITIAL); + return new PacketSpaces(initialPktSpaceMgr, + new PacketSpaceManager.HandshakePacketSpaceManager(connection, initialPktSpaceMgr), + new PacketSpaceManager.OneRttPacketSpaceManager(connection)); + } + + public void close() { + initial.close(); + handshake.close(); + app.close(); + } + } + + private final ConcurrentLinkedQueue incoming = new ConcurrentLinkedQueue<>(); + private final SequentialScheduler incomingLoopScheduler = + SequentialScheduler.lockingScheduler(this::incoming); + + + /* + * delegate handling of the datagrams to the executor to free up + * the endpoint readLoop. Helps with processing ACKs in a more + * timely fashion, which avoids too many retransmission. + * The endpoint readLoop runs on a single thread, while this loop + * will have one thread per connection which helps with a better + * utilization of the system resources. + */ + private void scheduleForDecryption(IncomingDatagram datagram) { + // Processes an incoming encrypted packet that has just been + // read off the network. + var received = datagram.buffer.remaining(); + if (incomingLoopScheduler.isStopped()) { + if (debug.on()) { + debug.log("scheduleForDecryption closed: dropping datagram (%d bytes)", + received); + } + return; + } + if (debug.on()) { + debug.log("scheduleForDecryption: %d bytes", received); + } + endpoint.buffer(received); + incoming.add(datagram); + + incomingLoopScheduler.runOrSchedule(quicInstance().executor()); + } + + private void incoming() { + try { + IncomingDatagram datagram; + while ((datagram = incoming.poll()) != null) { + ByteBuffer buffer = datagram.buffer; + int remaining = buffer.remaining(); + try { + if (incomingLoopScheduler.isStopped()) { + // we still need to unbuffer, continue here will + // ensure we skip directly to the finally-block + // below. + continue; + } + + internalProcessIncoming(datagram.source(), + datagram.destConnId(), + datagram.headersType(), + datagram.buffer()); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to process datagram: " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } finally { + endpoint.unbuffer(remaining); + } + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Schedule an incoming quic packet for decryption. + * The ByteBuffer should contain a single packet, and its + * limit should be set at the end of the packet. + * + * @param buffer a byte buffer containing the incoming packet + */ + private void decrypt(ByteBuffer buffer) { + // Processes an incoming encrypted packet that has just been + // read off the network. + PacketType packetType = decoder.peekPacketType(buffer); + var received = buffer.remaining(); + var pos = buffer.position(); + if (debug.on()) { + debug.log("decrypt %s(pos=%d, remaining=%d)", + packetType, pos, received); + } + try { + assert packetType != PacketType.VERSIONS; + var packet = codingContext.parsePacket(buffer); + if (packet != null) { + processDecrypted(packet); + } else { + if (packetType == PacketType.HANDSHAKE) { + packetSpaces.initial.fastRetransmit(); + } + } + } catch (QuicTransportException qte) { + // close the connection on this fatal error + if (Log.errors() || debug.on()) { + final String msg = "closing connection due to error while decoding" + + " packet (type=" + packetType + "): " + qte; + Log.logError(logTag() + " " + msg); + debug.log(msg, qte); + } + terminator.terminate(TerminationCause.forException(qte)); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to decode packet (type=" + packetType + "): " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } + } + + public void closeIncoming() { + incomingLoopScheduler.stop(); + IncomingDatagram icd; + // we still need to unbuffer all datagrams in the queue + while ((icd = incoming.poll()) != null ) { + endpoint.unbuffer(icd.buffer().remaining()); + } + } + + /** + * A protection record contains a packet to encrypt, and a datagram that may already + * contain encrypted packets. The firstPacketOffset indicates the position of the + * first encrypted packet in the datagram. The packetOffset indicates the position + * at which this packet will be - or has been - written in the datagram. + * Before the packet is encrypted and written to the datagram, the packetOffset + * should be the same as the datagram buffer position. + * After the packet has been written, the packetOffset should indicate + * at which position the packet has been written. The datagram position + * indicates where to write the next packet. + *

      + * Additionally, a {@code ProtectionRecord} may carry some flags indicating the + * intended usage of the datagram. The following flags are supported: + *

        + *
      • {@link #SINGLE_PACKET}: the default - it is not expected that the + * datagram will contain more packets
      • + *
      • {@link #COALESCED}: should be used if it is expected that the + * datagram will contain more than one packet
      • + *
      • {@link #LAST_PACKET}: should be used in conjunction with {@link #COALESCED} + * to indicate that the packet being protected is the last that will be + * added to the datagram
      • + *
      + * + * @apiNote + * Flag values can be combined, but some combinations + * may not make sense. A single packet can also be identified as any + * packet that doesn't have the {@code COALESCED} bit on. + * The flag is used to convey information that may be used to figure + * out whether to send the datagram right away, or whether to wait for + * more packet to be coalesced inside it. + * + * @param packet the packet to encrypt + * @param datagram the datagram in which the encrypted packet should be written + * @param firstPacketOffset the position of the first encrypted packet in the datagram + * @param packetOffset the offset at which the packet should be / has been written in the datagram + * @param flags a bit mask containing some details about the datagram being sent out + */ + public record ProtectionRecord(QuicPacket packet, ByteBuffer datagram, + int firstPacketOffset, int packetOffset, + long retransmittedPacketNumber, int flags) { + /** + * This is the default. + * This protection record is adding a single packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int SINGLE_PACKET = 0; + /** + * This can be used when it is expected that more than one packet + * will be added to this datagram. We should wait until the last packet + * has been added before sending the datagram out. + */ + public static final int COALESCED = 1; + /** + * This protection record is adding the last packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int LAST_PACKET = 2; + + // indicate that the packet is not retransmitted + private static final long NOT_RETRANSMITTED = -1L; + + ProtectionRecord withOffset(int packetOffset) { + if (this.packetOffset == packetOffset) { + return this; + } + return new ProtectionRecord(packet, datagram, firstPacketOffset, + packetOffset, retransmittedPacketNumber, flags); + } + + public ProtectionRecord encrypt(final CodingContext codingContext) + throws QuicKeyUnavailableException, QuicTransportException { + final PacketType packetType = packet.packetType(); + assert packetType != PacketType.VERSIONS; + // keep track of position before encryption + final int preEncryptPos = datagram.position(); + codingContext.writePacket(packet, datagram); + final ProtectionRecord encrypted = withOffset(preEncryptPos); + return encrypted; + } + + /** + * Records the intent of protecting a packet that will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord single(QuicPacket packet, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, + offset, offset, NOT_RETRANSMITTED, 0); + } + + /** + * Records the intent of protecting a packet that retransmits + * a previously transmitted packet. The packet will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param retransmittedPacketNumber the packet number of the original + * packet that was considered lost + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord retransmitting(QuicPacket packet, + long retransmittedPacketNumber, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, offset, offset, + retransmittedPacketNumber, 0); + } + + /** + * Records the intent of protecting a packet that will be followed by + * more packets to be coalesced in the same datagram. The datagram + * should not be sent until the last packet has been coalesced. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord more(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, COALESCED); + } + + /** + * Records the intent of protecting the last packet that will be + * coalesced in the given datagram. The datagram can be sent as soon + * as the packet has been encrypted and coalesced into the given + * datagram. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord last(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, LAST_PACKET | COALESCED); + } + } + + final QuicPacket newQuicPacket(final KeySpace keySpace, final List frames) { + final PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.of(keySpace)); + return encoder.newOutgoingPacket(keySpace, packetSpace, + localConnectionId(), peerConnectionId(), initialToken(), + frames, + codingContext); + } + + /** + * Encrypt an outgoing quic packet. + * The ProtectionRecord indicates the position at which the encrypted packet + * should be written in the datagram, as well as the position of the + * first packet in the datagram. After encrypting the packet, this method calls + * {@link #pushEncryptedDatagram(ProtectionRecord)} + * + * @param protectionRecord a record containing a quic packet to encrypt, + * a destination byte buffer, and various offset information. + */ + final void pushDatagram(final ProtectionRecord protectionRecord) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicPacket packet = protectionRecord.packet(); + if (debug.on()) { + debug.log("encrypting packet into datagram %s(pn:%s, %s)", packet.packetType(), + packet.packetNumber(), packet.frames()); + } + // Processes an outgoing unencrypted packet that needs to be + // encrypted before being packaged in a datagram. + final ProtectionRecord encrypted; + try { + encrypted = protectionRecord.encrypt(codingContext); + } catch (Throwable e) { + // release the datagram ByteBuffer on failure to encrypt + datagramDiscarded(new QuicDatagram(this, peerAddress, protectionRecord.datagram())); + if (Log.errors()) { + Log.logError("Failed to encrypt packet: " + e); + // certain failures like key not being available are OK + // in some situations. log the stacktrace only if this + // was an unexpected failure. + boolean skipStackTrace = false; + if (e instanceof QuicKeyUnavailableException) { + final PacketSpace packetSpace = packetSpace(protectionRecord.packet().numberSpace()); + skipStackTrace = packetSpace.isClosed(); + } + if (!skipStackTrace) { + Log.logError(e); + } + } + throw e; + } + // we currently don't support a ProtectionRecord with more than one QuicPacket + assert (encrypted.flags & ProtectionRecord.COALESCED) == 0 : "coalesced packets not supported"; + // encryption of the datagram is complete, now push the encrypted + // datagram through the endpoint + if (Log.quicPacketOutLoggable(packet)) { + Log.logQuicPacketOut(logTag(), packet); + } + pushEncryptedDatagram(encrypted); + } + + protected void completeHandshakeCF() { + // This can be called from the decrypt loop, and can trigger + // sending of 1-RTT application data from within the same + // thread: we use an executor here to avoid running the application + // sending loop from within the Quic decrypt loop. + completeHandshakeCF(quicInstance().executor()); + } + + protected final void completeHandshakeCF(Executor executor) { + final var handshakeCF = handshakeFlow.handshakeCF(); + if (handshakeCF.isDone()) { + return; + } + var handshakeState = quicTLSEngine.getHandshakeState(); + if (executor != null) { + handshakeCF.completeAsync(() -> handshakeState, executor); + } else { + handshakeCF.complete(handshakeState); + } + } + + /** + * A class used to check that 1-RTT received data doesn't exceed + * the MAX_DATA of the connection + */ + class OneRttFlowControlledReceivingQueue { + private static final long MIN_BUFFER_SIZE = 16L << 10; // 16k + private volatile long receivedData; + private volatile long maxData; + private volatile long processedData; + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize = Math.clamp(DEFAULT_INITIAL_MAX_DATA, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + private final Supplier logTag; + + OneRttFlowControlledReceivingQueue(Supplier logTag) { + this.logTag = Objects.requireNonNull(logTag); + } + + /** + * Called when new local parameters are available + * @param localParameters the new local paramaters + */ + void newLocalParameters(QuicTransportParameters localParameters) { + if (localParameters.isPresent(ParameterId.initial_max_data)) { + long maxData = this.maxData; + long newMaxData = localParameters.getIntParameter(ParameterId.initial_max_data); + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) break; + maxData = this.maxData; + } + } + } + + /** + * Checks whether the give frame would cause the connection max data + * to be exceeded. If no, increase the amount of data processed by + * this connection by the length of the frame. If yes, sends a + * ConnectionCloseFrame with FLOW_CONTROL_ERROR. + * + * @param diff number of bytes newly received + * @param frameType type of frame received + * @throws QuicTransportException if processing this frame would cause the connection + * max data to be exceeded + */ + void checkAndIncreaseReceivedData(long diff, long frameType) throws QuicTransportException { + assert diff > 0; + long max, processed; + boolean exceeded; + synchronized (this) { + max = maxData; + processed = receivedData; + if (max - processed < diff) { + exceeded = true; + } else { + try { + receivedData = processed = Math.addExact(processed, diff); + exceeded = false; + } catch (ArithmeticException x ) { + // should not happen - flow control should have + // caught that + receivedData = processed = Long.MAX_VALUE; + exceeded = true; + } + } + } + if (exceeded) { + String reason = "Connection max data exceeded: max data processed=%s, max connection data=%s" + .formatted(processed, max); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.FLOW_CONTROL_ERROR); + } + } + + public void increaseProcessedData(long diff) { + long processed, received, max; + synchronized (this) { + processed = processedData += diff; + received = receivedData; + max = maxData; + } + if (Log.quicProcessed()) { + Log.logQuic(logTag()+ " Processed: " + processed + + ", received: " + received + + ", max:" + max); + } + if (needSendMaxData()) { + runAppPacketSpaceTransmitter(); + } + } + + private long bumpMaxData() { + long newMaxData = processedData + desiredBufferSize; + long maxData = this.maxData; + if (newMaxData - maxData < (desiredBufferSize / 5)) { + return 0; + } + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) + return newMaxData; + maxData = this.maxData; + } + return 0; + } + + public boolean needSendMaxData() { + return maxData - processedData < desiredBufferSize/2; + } + + String logTag() { return logTag.get(); } + } + + /** + * An event loop triggered when stream data is available for sending. + * We use a sequential scheduler here to make sure we don't send + * more data than allowed by the connection's flow control. + * This guarantee that only one thread composes flow controlled + * OneRTT packets at a given time, which in turn guarantees that the + * credit computed at the beginning of the loop will still be + * available after the packet has been composed. + */ + class OneRttFlowControlledSendingQueue { + private volatile long dataProcessed; + private volatile long maxData; + + /** + * Called when a MAX_DATA frame is received. + * This method is a no-op if the given value is less than the + * current max stream data for the connection. + * + * @param maxData the maximum data offset that the peer is prepared + * to accept for the whole connection + * @param isInitial true when processing transport parameters, + * false when processing MaxDataFrame + * @return the actual max data after taking the given value into account + */ + public long setMaxData(long maxData, boolean isInitial) { + long max; + long processed; + boolean wasblocked, unblocked = false; + do { + synchronized (this) { + max = this.maxData; + processed = dataProcessed; + } + wasblocked = max <= processed; + if (max < maxData) { + if (MAX_SND_DATA.compareAndSet(this, max, maxData)) { + max = maxData; + unblocked = (wasblocked && max > processed); + } + } + } while (max < maxData); + if (unblocked && !isInitial) { + packetSpaces.app.runTransmitter(); + } + return max; + } + + /** + * {@return the remaining credit for this connection} + */ + public long credit() { + synchronized (this) { + return maxData - dataProcessed; + } + } + + // We can continue sending if we have credit and data is available to send + private boolean canSend() { + return credit() > 0 && streams.hasAvailableData() + || streams.hasControlFrames() + || hasQueuedFrames() + || oneRttRcvQueue.needSendMaxData(); + } + + // implementation of the sending loop. + private boolean send1RTTData() { + Throwable failure; + try { + return doSend1RTTData(); + } catch (Throwable t) { + failure = t; + } + if (failure instanceof QuicKeyUnavailableException qkue) { + if (!QuicConnectionImpl.this.stateHandle().opened()) { + // connection is already being closed and that explains the + // key unavailability (they might have been discarded). just log + // and return + if (debug.on()) { + debug.log("failed to send stream data, reason: " + qkue.getMessage()); + } + return false; + } + // connection is still open but a key unavailability exception was raised. + // close the connection and use an IOException instead of the internal + // QuicKeyUnavailableException as the cause for the connection close. + failure = new IOException(qkue.getMessage()); + } + if (debug.on()) { + debug.log("failed to send stream data", failure); + } + // close the connection to make sure it's not just ignored + terminator.terminate(TerminationCause.forException(failure)); + return false; + } + + private boolean doSend1RTTData() throws QuicKeyUnavailableException, QuicTransportException { + // Loop over all sending streams to see if data is available - include + // as much data as possible in the quic packet before sending it. + // The QuicConnectionStreams make sure that streams are polled in a fair + // manner (using round-robin?) + // This loop is called through a sequential scheduler to make + // sure we only have one thread emitting flow control data for + // this connection + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId().length(); + if (!canSend()) { + return false; + } + final long packetNumber = space.allocateNextPN(); + final long largestPeerAckedPN = space.getLargestPeerAckedPN(); + int remaining = QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, packetNumber, dstIdLength, maxDatagramSize, largestPeerAckedPN); + if (remaining == 0) { + // not enough space to send available data + return false; + } + final List frames = new ArrayList<>(); + remaining -= addConnectionControlFrames(remaining, frames); + assert remaining >= 0 : remaining; + long produced = streams.produceFramesToSend(encoder, remaining, credit(), frames); + if (frames.isEmpty()) { + // produced cannot be > 0 unless there are some frames to send + assert produced == 0; + return false; + } + // non-atomic operation should be OK since sendStreamData0 is called + // only from the sending loop, and this is the only place where we + // mutate dataProcessed. + dataProcessed += produced; + final OneRttPacket packet = encoder.newOneRttPacket(peerConnectionId, + packetNumber, largestPeerAckedPN, frames, codingContext); + QuicConnectionImpl.this.send1RTTPacket(packet); + return true; + } + + /** + * Produces connection-level control frames for sending in the next one-rtt + * packet. The frames are added to the provided list. + * + * @param maxAllowedBytes maximum number of bytes the method is allowed to add + * @param frames list where the frames are added + * @return number of bytes added + */ + private int addConnectionControlFrames(final int maxAllowedBytes, + final List frames) { + assert maxAllowedBytes > 0 : "unexpected max allowed bytes: " + maxAllowedBytes; + int added = 0; + int remaining = maxAllowedBytes; + QuicFrame f; + while ((f = outgoing1RTTFrames.peek()) != null) { + final int frameSize = f.size(); + if (frameSize <= remaining) { + outgoing1RTTFrames.remove(); + frames.add(f); + added += frameSize; + remaining -= frameSize; + } else { + break; + } + } + PathChallengeFrame pcf; + while (remaining >= 9 && (pcf = pathChallengeFrameQueue.poll()) != null) { + f = new PathResponseFrame(pcf.data()); + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + // NEW_CONNECTION_ID + while ((f = localConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + // RETIRE_CONNECTION_ID + while ((f = peerConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + if (remaining == 0) { + return added; + } + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final AckFrame ack = space.getNextAckFrame(false, remaining); + if (ack != null) { + final int ackFrameSize = ack.size(); + assert ackFrameSize <= remaining; + if (debug.on()) { + debug.log("Adding AckFrame"); + } + frames.add(ack); + added += ackFrameSize; + remaining -= ackFrameSize; + } + final long credit = credit(); + if (credit < remaining && remaining > 10) { + if (debug.on()) { + debug.log("Adding DataBlockedFrame"); + } + DataBlockedFrame dbf = new DataBlockedFrame(maxData); + frames.add(dbf); + added += dbf.size(); + remaining -= dbf.size(); + } + // max data + if (remaining > 10) { + long maxData = oneRttRcvQueue.bumpMaxData(); + if (maxData != 0) { + if (debug.on()) { + debug.log("Adding MaxDataFrame (processed: %s)", + oneRttRcvQueue.processedData); + } + MaxDataFrame mdf = new MaxDataFrame(maxData); + frames.add(mdf); + added += mdf.size(); + remaining -= mdf.size(); + } + } + // session ticket + if (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT) { + try { + ByteBuffer payloadBuffer = quicTLSEngine.getHandshakeBytes(KeySpace.ONE_RTT); + if (payloadBuffer != null) { + localCryptoFlow.enqueue(payloadBuffer); + } + } catch (IOException e) { + throw new AssertionError("Should not happen!", e); + } + if (localCryptoFlow.remaining() > 0 && remaining > 3) { + CryptoFrame frame = localCryptoFlow.produceFrame(remaining); + if (frame != null) { + if (debug.on()) { + debug.log("Adding CryptoFrame"); + } + frames.add(frame); + added += frame.size(); + remaining -= frame.size(); + assert remaining >= 0; + } + } + } + return added; + } + } + + /** + * Invoked to send a ONERTT packet containing stream data or + * control frames. + * + * @apiNote + * This method can be overridden if some action needs to be + * performed after sending a packet containing certain type + * of frames. Typically, a server side connection may want + * to close the HANDSHAKE space only after sending the + * HANDSHAKE_DONE frame. + * + * @param packet The ONERTT packet to send. + */ + protected void send1RTTPacket(final OneRttPacket packet) + throws QuicKeyUnavailableException, QuicTransportException { + pushDatagram(ProtectionRecord.single(packet, + QuicConnectionImpl.this::allocateDatagramForEncryption)); + } + + /** + * Schedule a frame for sending in a 1-RTT packet. + *

      + * For use with frames that do not change with time + * (like MAX_* / *_BLOCKED / ACK), + * or with remaining datagram capacity (like STREAM or CRYPTO), + * and do not require certain path (PATH_CHALLENGE / RESPONSE). + *

      + * Use with frames like HANDSHAKE_DONE, NEW_TOKEN, + * NEW_CONNECTION_ID, RETIRE_CONNECTION_ID. + *

      + * Maximum accepted frame size is 1000 bytes to ensure that the frame + * will fit in a 1-RTT datagram in the foreseeable future. + * @param frame frame to send + * @throws IllegalArgumentException if frame is larger than 1000 bytes + */ + protected void enqueue1RTTFrame(final QuicFrame frame) { + if (frame.size() > 1000) { + throw new IllegalArgumentException("Frame too big"); + } + assert frame.isValidIn(PacketType.ONERTT) : "frame " + frame + " is not" + + " eligible in 1-RTT space"; + outgoing1RTTFrames.add(frame); + } + + /** + * {@return true if queued frames are available for sending} + */ + private boolean hasQueuedFrames() { + return !outgoing1RTTFrames.isEmpty(); + } + + protected QuicPacketEncoder encoder() { return encoder;} + protected QuicPacketDecoder decoder() { return decoder; } + public QuicEndpoint endpoint() { return endpoint; } + protected final StateHandle stateHandle() { return stateHandle; } + protected CodingContext codingContext() { + return codingContext; + } + + public long largestAckedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestPeerAckedPN(); + } + + public long largestProcessedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestProcessedPN(); + } + + public int connectionIdLength() { + return localConnectionId().length(); + } + + public QuicInstance quicInstance() { + return this.quicInstance; + } + + public QuicVersion quicVersion() { + return this.quicVersion; + } + + protected class QuicCodingContext implements CodingContext { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestProcessedPN(packetSpace); + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestAckedPN(packetSpace); + } + @Override public int connectionIdLength() { + return QuicConnectionImpl.this.connectionIdLength(); + } + @Override public int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException { + int start = buffer.position(); + encoder.encode(packet, buffer, this); + return buffer.position() - start; + } + @Override public QuicPacket parsePacket(ByteBuffer src) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + return decoder.decode(src, this); + } + @Override + public QuicConnectionId originalServerConnId() { + return QuicConnectionImpl.this.originalServerConnId(); + } + + @Override + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return QuicConnectionImpl.this.verifyToken(destinationID, token); + } + } + + protected boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + // server must send zero-length token + return token == null; + } + + protected PacketEmitter emitter() { + return new PacketEmitter() { + @Override + public QuicTimerQueue timer() { + return QuicConnectionImpl.this.endpoint().timer(); + } + + @Override + public void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionImpl.this.retransmit(packetSpaceManager, packet, attempts); + } + + @Override + public long emitAckPacket(PacketSpace packetSpaceManager, + AckFrame frame, + boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.emitAckPacket(packetSpaceManager, frame, sendPing); + } + + @Override + public void acknowledged(QuicPacket packet) { + QuicConnectionImpl.this.packetAcknowledged(packet); + } + + @Override + public boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.sendData(packetNumberSpace); + } + + @Override + public Executor executor() { + return quicInstance().executor(); + } + + @Override + public void reschedule(QuicTimedEvent task) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task); + } + + @Override + public void reschedule(QuicTimedEvent task, Deadline deadline) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task, deadline); + } + + @Override + public void checkAbort(PacketNumberSpace packetNumberSpace) { + QuicConnectionImpl.this.checkAbort(packetNumberSpace); + } + + @Override + public void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: [%s] increase backoff to %s, duration %s ms: %s" + .formatted(QuicConnectionImpl.this.logTag(), + space.packetNumberSpace(), backoff, + space.getPtoDuration().toMillis(), + rttEstimator.state())); + } + } + + @Override + public String logTag() { + return QuicConnectionImpl.this.logTag(); + } + + @Override + public boolean isOpen() { + return QuicConnectionImpl.this.stateHandle.opened(); + } + }; + } + + private void checkAbort(PacketNumberSpace packetNumberSpace) { + // if pto backoff > 32 (i.e. PTO expired 5 times in a row), abort, + // unless we haven't reached MIN_PTO_BACKOFF_TIMEOUT + var backoff = rttEstimator.getPtoBackoff(); + if (backoff > QuicRttEstimator.MAX_PTO_BACKOFF) { + // If the maximum backoff is exceeded, we close the connection + // only if the associated backoff timeout exceeds the + // MIN_PTO_BACKOFF_TIMEOUT. Otherwise, we allow the backoff + // factor to grow again past the MAX_PTO_BACKOFF + if (rttEstimator.isMinBackoffTimeoutExceeded()) { + if (debug.on()) { + debug.log("%s Too many probe time outs: %s", packetNumberSpace, backoff); + debug.log(String.valueOf(rttEstimator.state())); + debug.log("State: %s", stateHandle().toString()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Too many probe timeouts %s" + .formatted(logTag(), packetNumberSpace, + rttEstimator.state())); + StringBuilder sb = new StringBuilder(logTag()); + sb.append(" State: ").append(stateHandle().toString()); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + sb.append("\nPacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + Log.logQuic(sb.toString()); + } else if (debug.on()) { + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + m.debugState(); + } + } + } + var pto = rttEstimator.getBasePtoDuration(); + var to = pto.multipliedBy(backoff); + if (to.compareTo(MAX_PTO_BACKOFF_TIMEOUT) > 0) to = MAX_PTO_BACKOFF_TIMEOUT; + String msg = "%s: Too many probe time outs (%s: backoff %s, duration %s, %s)" + .formatted(logTag(), packetNumberSpace, backoff, + to, rttEstimator.state()); + final TerminationCause terminationCause; + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + terminationCause = TerminationCause.forException(new SSLHandshakeException(msg)); + } else if (packetNumberSpace == PacketNumberSpace.INITIAL) { + terminationCause = TerminationCause.forException(new ConnectException(msg)); + } else { + terminationCause = TerminationCause.forException(new IOException(msg)); + } + terminator.terminate(terminationCause); + } else { + if (debug.on()) { + debug.log("%s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s)," + + " allow more backoff %s", + packetNumberSpace, backoff, MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s) - %s" + .formatted(QuicConnectionImpl.this.logTag(), packetNumberSpace, backoff, + MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state())); + } + } + } + } + + // this method is called when a packet has been acknowledged + private void packetAcknowledged(QuicPacket packet) { + // process packet frames to track acknowledgement + // of RESET_STREAM frames etc... + if (debug.on()) { + debug.log("Packet %s(pn:%s) is acknowledged by peer", + packet.packetType(), + packet.packetNumber()); + } + packet.frames().forEach(this::frameAcknowledged); + } + + // this method is called when a frame has been acknowledged + private void frameAcknowledged(QuicFrame frame) { + if (frame instanceof ResetStreamFrame reset) { + long streamId = reset.streamId(); + if (streams.isSendingStream(streamId)) { + streams.streamResetAcknowledged(reset); + } + } else if (frame instanceof StreamFrame streamFrame) { + if (streamFrame.isLast()) { + streams.streamDataSentAcknowledged(streamFrame); + } + } + } + + protected PacketSpaces packetNumberSpaces() { + return packetSpaces; + } + protected PacketSpace packetSpace(PacketNumberSpace packetNumberSpace) { + return packetSpaces.get(packetNumberSpace); + } + + public String dbgTag() { return dbgTag; } + + public String streamDbgTag(long streamId, String direction) { + String dir = direction == null || direction.isEmpty() + ? "" : ("(" + direction + ")"); + return dbgTag + "[streamId" + dir + "=" + streamId + "]"; + } + + + @Override + public CompletableFuture openNewLocalBidiStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) -> + streams.createNewLocalBidiStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public CompletableFuture openNewLocalUniStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) + -> streams.createNewLocalUniStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public void addRemoteStreamListener(Predicate streamConsumer) { + streams.addRemoteStreamListener(streamConsumer); + } + + @Override + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + return streams.removeRemoteStreamListener(streamConsumer); + } + + @Override + public Stream quicStreams() { + return streams.quicStreams(); + } + + @Override + public List connectionIds() { + return localConnIdManager.connectionIds(); + } + + LocalConnIdManager localConnectionIdManager() { + return localConnIdManager; + } + + /** + * {@return the local connection id} + */ + public QuicConnectionId localConnectionId() { + return connectionId; + } + + /** + * {@return the peer connection id} + */ + public QuicConnectionId peerConnectionId() { + return this.peerConnIdManager.getPeerConnId(); + } + + /** + * Returns the original connection id. + * This is the original destination connection id that + * the client generated when connecting to the server for + * the first time. + * @return the original connection id + */ + protected QuicConnectionId originalServerConnId() { + return this.peerConnIdManager.originalServerConnId(); + } + + private record IncomingDatagram(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) {} + + @Override + public boolean accepts(SocketAddress source) { + // The client ever accepts packets from two sources: + // => the original peer address + // => the preferred peer address (not implemented) + if (!source.equals(peerAddress)) { + // We only accept packets from the endpoint to + // which we send them. + if (debug.on()) { + debug.log("unexpected sender %s, skipping packet", source); + } + return false; + } + return true; + } + + public void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + // Processes an incoming datagram that has just been + // read off the network. + if (debug.on()) { + debug.log("processIncoming %s(pos=%d, remaining=%d)", + headersType, buffer.position(), buffer.remaining()); + } + if (!stateHandle.opened()) { + if (debug.on()) { + debug.log("connection closed, skipping packet"); + } + return; + } + + assert accepts(source); + + scheduleForDecryption(new IncomingDatagram(source, destConnId, headersType, buffer)); + } + + public void internalProcessIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + try { + int packetIndex = 0; + while(buffer.hasRemaining()) { + int startPos = buffer.position(); + packetIndex++; + boolean isLongHeader = QuicPacketDecoder.peekHeaderType(buffer, startPos) == QuicPacket.HeadersType.LONG; + // It's only safe to check version here if versionNegotiated is true. + // We might be receiving an INITIAL packet before the version negotiation + // has been handled. + if (isLongHeader) { + LongHeader header = QuicPacketDecoder.peekLongHeader(buffer); + if (header == null) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram): too short", packetIndex); + } + return; + } + if (!header.destinationId().matches(destConnId)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, + header.destinationId().toHexString(), + Utils.asHexString(destConnId)); + } + return; + } + var peekedVersion = header.version(); + final var version = this.quicVersion.versionNumber(); + if (version != peekedVersion) { + if (peekedVersion == 0) { + if (!versionCompatible) { + VersionNegotiationPacket packet = (VersionNegotiationPacket) codingContext.parsePacket(buffer); + processDecrypted(packet); + } else { + if (debug.on()) { + debug.log("Versions packet (%s in datagram) ignored", packetIndex); + } + } + return; + } + QuicVersion packetVersion = QuicVersion.of(peekedVersion).orElse(null); + if (packetVersion == null) { + if (debug.on()) { + debug.log("Unknown Quic version in long header packet" + + " (%s in datagram) %s: 0x%x", + packetIndex, headersType, peekedVersion); + } + return; + } else if (versionNegotiated) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with version %s, already negotiated %s", + packetIndex, packetVersion, quicVersion); + } + return; + } else if (!quicInstance().isVersionAvailable(packetVersion)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with disabled version %s", + packetIndex, packetVersion); + } + return; + } else { + // do we need to be less trusting here? + if (debug.on()) { + debug.log("Switching version to %s, previous: %s", + packetVersion, quicVersion); + } + switchVersion(packetVersion); + } + } + if (decoder.peekPacketType(buffer) == PacketType.INITIAL && + !quicTLSEngine.keysAvailable(KeySpace.INITIAL)) { + if (debug.on()) { + debug.log("Dropping INITIAL packet (%s in datagram): %s", + packetIndex, "keys discarded"); + } + decoder.skipPacket(buffer, startPos); + continue; + } + } else { + var cid = QuicPacketDecoder.peekShortConnectionId(buffer, destConnId.remaining()); + if (cid == null) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " too short", packetIndex); + } + return; + } + if (cid.mismatch(destConnId) != -1) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, Utils.asHexString(cid), Utils.asHexString(destConnId)); + } + return; + } + + } + ByteBuffer packet = decoder.nextPacketSlice(buffer, buffer.position()); + PacketType packetType = decoder.peekPacketType(packet); + if (debug.on()) { + debug.log("unprotecting packet (%s in datagram) %s(%s bytes)", + packetIndex, packetType, packet.remaining()); + } + decrypt(packet); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to process incoming packet", t); + } + } + } + + /** + * Called when an incoming packet has been decrypted. + *

      + * @param quicPacket the decrypted quic packet + */ + public void processDecrypted(QuicPacket quicPacket) { + PacketType packetType = quicPacket.packetType(); + long packetNumber = quicPacket.packetNumber(); + if (debug.on()) { + debug.log("processDecrypted %s(%d)", packetType, packetNumber); + } + if (Log.quicPacketInLoggable(quicPacket)) { + Log.logQuicPacketIn(logTag(), quicPacket); + } + if (packetType != PacketType.VERSIONS) { + versionCompatible = true; + // versions will also set versionCompatible later + } + if (isClientConnection() + && quicPacket instanceof InitialPacket longPacket + && quicPacket.frames().stream().anyMatch(CryptoFrame.class::isInstance)) { + markVersionNegotiated(longPacket.version()); + } + PacketSpace packetSpace = null; + if (packetNumber >= 0) { + packetSpace = packetSpace(quicPacket.numberSpace()); + + // From RFC 9000, Section 13.2.3: + // A receiver MUST retain an ACK Range unless it can ensure that + // it will not subsequently accept packets with numbers in + // that range. Maintaining a minimum packet number that increases + // as ranges are discarded is one way to achieve this with minimal + // state. + long threshold = packetSpace.getMinPNThreshold(); + if (packetNumber <= threshold) { + // discard the packet, as we are no longer acknowledging + // packets in this range. + if (debug.on()) + debug.log("discarding packet %s(%d) - threshold: %d", + packetType, packetNumber, threshold); + return; + } + if (packetSpace.isAcknowledged(packetNumber)) { + if (debug.on()) + debug.log("discarding packet %s(%d) - duplicated", + packetType, packetNumber, threshold); + } + + if (debug.on()) { + debug.log("receiving packet %s(pn:%s, %s)", packetType, + packetNumber, quicPacket.frames()); + } + } + switch (packetType) { + case VERSIONS -> processVersionNegotiationPacket(quicPacket); + case INITIAL -> processInitialPacket(quicPacket); + case ONERTT -> processOneRTTPacket(quicPacket); + case HANDSHAKE -> processHandshakePacket(quicPacket); + case RETRY -> processRetryPacket(quicPacket); + case ZERORTT -> { + if (debug.on()) { + debug.log("Dropping unhandled quic packet %s", packetType); + } + } + case NONE -> throw new InternalError("Unrecognized packet type"); + } + // packet has been processed successfully - connection isn't idle (RFC-9000, section 10.1) + this.terminator.keepAlive(); + if (packetSpace != null) { + packetSpace.packetReceived( + packetType, + packetNumber, + quicPacket.isAckEliciting()); + } + } + + /** + * {@return true if this is a stream initiated locally, and false if + * this is a stream initiated by the peer}. + * @param streamId a stream ID. + */ + protected final boolean isLocalStream(long streamId) { + return isClientConnection() == QuicStreams.isClientInitiated(streamId); + } + + /** + * If a stream with this streamId was already created, returns it. + * @param streamId the stream ID + * @return the stream identified by the given {@code streamId}, or {@code null}. + */ + protected QuicStream findStream(long streamId) { + return streams.findStream(streamId); + } + + /** + * @return true if this stream ID identifies a stream that was + * already opened + * @param streamId the stream id + */ + protected boolean isExistingStreamId(long streamId) { + long next = streams.peekNextStreamId(streamType(streamId)); + return streamId < next; + } + + /** + * Get or open a peer initiated stream with the given stream ID + * @param streamId the id of the remote stream + * @param frameType type of the frame received, used in exceptions + * @return the remote initiated stream identified by the given + * stream ID, or null + * @throws QuicTransportException if the streamID is higher than allowed + */ + protected QuicStream openOrGetRemoteStream(long streamId, long frameType) throws QuicTransportException { + assert !isLocalStream(streamId); + return streams.getOrCreateRemoteStream(streamId, frameType); + } + + /** + * Called to process a {@link OneRttPacket} after it has been successfully decrypted + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if the {@code quicPacket} isn't a 1-RTT packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processOneRTTPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.ONERTT) { + throw new IllegalArgumentException("Not a ONERTT packet: " + quicPacket.packetType()); + } + assert quicPacket instanceof OneRttPacket : "Unexpected ONERTT packet class type: " + + quicPacket.getClass(); + final OneRttPacket oneRTT = (OneRttPacket) quicPacket; + try { + if (debug.on()) { + debug.log("processing packet ONERTT(%s)", quicPacket.packetNumber()); + } + final var frames = oneRTT.frames(); + if (debug.on()) { + debug.log("processing frames: " + frames.stream() + .map(Object::getClass).map(Class::getSimpleName) + .collect(Collectors.joining(", ", "[", "]"))); + } + for (var frame : oneRTT.frames()) { + if (!frame.isValidIn(PacketType.ONERTT)) { + throw new QuicTransportException("Invalid frame in ONERTT packet", + KeySpace.ONE_RTT, frame.getTypeField(), + PROTOCOL_VIOLATION); + } + if (debug.on()) { + debug.log("received 1-RTT frame %s", frame); + } + switch (frame) { + case AckFrame ackFrame -> { + incoming1RTTFrame(ackFrame); + } + case StreamFrame streamFrame -> { + incoming1RTTFrame(streamFrame); + } + case CryptoFrame crypto -> { + incoming1RTTFrame(crypto); + } + case ResetStreamFrame resetStreamFrame -> { + incoming1RTTFrame(resetStreamFrame); + } + case DataBlockedFrame dataBlockedFrame -> { + incoming1RTTFrame(dataBlockedFrame); + } + case StreamDataBlockedFrame streamDataBlockedFrame -> { + incoming1RTTFrame(streamDataBlockedFrame); + } + case StreamsBlockedFrame streamsBlockedFrame -> { + incoming1RTTFrame(streamsBlockedFrame); + } + case PaddingFrame paddingFrame -> { + incoming1RTTFrame(paddingFrame); + } + case MaxDataFrame maxData -> { + incoming1RTTFrame(maxData); + } + case MaxStreamDataFrame maxStreamData -> { + incoming1RTTFrame(maxStreamData); + } + case MaxStreamsFrame maxStreamsFrame -> { + incoming1RTTFrame(maxStreamsFrame); + } + case StopSendingFrame stopSendingFrame -> { + incoming1RTTFrame(stopSendingFrame); + } + case PingFrame ping -> { + incoming1RTTFrame(ping); + } + case ConnectionCloseFrame close -> { + incoming1RTTFrame(close); + } + case HandshakeDoneFrame handshakeDoneFrame -> { + incoming1RTTFrame(handshakeDoneFrame); + } + case NewConnectionIDFrame newCid -> { + incoming1RTTFrame(newCid); + } + case RetireConnectionIDFrame retireCid -> { + incoming1RTTFrame(oneRTT, retireCid); + } + case NewTokenFrame newTokenFrame -> { + incoming1RTTFrame(newTokenFrame); + } + case PathResponseFrame pathResponseFrame -> { + incoming1RTTFrame(pathResponseFrame); + } + case PathChallengeFrame pathChallengeFrame -> { + incoming1RTTFrame(pathChallengeFrame); + } + default -> { + if (debug.on()) { + debug.log("Frame type: %s not supported yet", frame.getClass()); + } + } + } + } + } catch (Throwable t) { + onProcessingError(quicPacket, t); + } + } + + /** + * Gets a receiving stream instance for the given ID, used for processing + * incoming STREAM, RESET_STREAM and STREAM_DATA_BLOCKED frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return receiver stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid receiving stream + */ + private QuicReceiverStream getReceivingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (isLocalStream && isUnidirectional) { + // stream is write-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + // Note: The quic protocol allows any peer to open + // a bidirectional remote stream. + // The HTTP/3 protocol does not allow a server to open a + // bidirectional stream on the client. If this is a client + // connection and the stream type is bidirectional and + // remote, the connection will be closed by the HTTP/3 + // higher level protocol but not here, since this is + // not a Quic protocol error. + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicReceiverStream)stream; + } + + /** + * Gets a sending stream instance for the given ID, used for processing + * incoming MAX_STREAM_DATA and STOP_SENDING frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return sender stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid sending stream + */ + private QuicSenderStream getSendingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (!isLocalStream && isUnidirectional) { + // stream is read-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicSenderStream)stream; + } + + /** + * Called to process an {@link InitialPacket} after it has been decrypted. + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if {@code quicPacket} isn't a INITIAL packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processInitialPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.INITIAL) { + throw new IllegalArgumentException("Not a INITIAL packet: " + quicPacket.packetType()); + } + try { + if (quicPacket instanceof InitialPacket initial) { + MaxInitialTimer initialTimer = this.maxInitialTimer; + if (initialTimer != null) { + // will be a no-op after the first call; + initialTimer.initialPacketReceived(); + // we no longer need the timer + this.maxInitialTimer = null; + } + int total; + updatePeerConnectionId(initial); + total = processInitialPacketPayload(initial); + assert total == initial.payloadSize(); + // received initial packet from server - we won't need to replay anything now + handshakeFlow.localInitial.discardReplayData(); + continueHandshake(); + if (quicTLSEngine.getHandshakeState() == HandshakeState.NEED_RECV_CRYPTO && + quicTLSEngine.keysAvailable(KeySpace.HANDSHAKE)) { + // arm the anti-deadlock PTO timer + packetSpaces.handshake.runTransmitter(); + } + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + protected void updatePeerConnectionId(InitialPacket initial) throws QuicTransportException { + this.incomingInitialPacketSourceId = initial.sourceId(); + this.peerConnIdManager.finalizeHandshakePeerConnId(initial); + } + + public QuicConnectionId getIncomingInitialPacketSourceId() { + return incomingInitialPacketSourceId; + } + + @Override + public CompletableFuture handshakeReachedPeer() { + return this.handshakeFlow.handshakeReachedPeerCF; + } + + /** + * Process the payload of an incoming initial packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processInitialPacketPayload(final InitialPacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int initialPayloadSize = packet.payloadSize(); + if (debug.on()) { + debug.log("Processing initial packet pn:%s payload:%s", + packet.packetNumber(), initialPayloadSize); + } + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received INITIAL frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingInitialFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingInitialFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingInitialFrame(paddingFrame); + } + case PingFrame ping -> { + incomingInitialFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingInitialFrame(close); + } + default -> { + if (debug.on()) { + debug.log("Received invalid frame: " + frame); + } + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != initialPayloadSize) { + throw new IOException("Initial payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, initialPayloadSize)); + } + return total; + } + /** + * Process the payload of an incoming handshake packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processHandshakePacketPayload(final HandshakePacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int payloadSize = packet.payloadSize(); + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received HANDSHAKE frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingHandshakeFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingHandshakeFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingHandshakeFrame(paddingFrame); + } + case PingFrame ping -> { + incomingHandshakeFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingHandshakeFrame(close); + } + default -> { + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != payloadSize) { + throw new IOException("Handshake payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, payloadSize)); + } + return total; + } + + /** + * Called to process an {@link HandshakePacket} after it has been decrypted. + * @param quicPacket the handshake quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a HANDSHAKE packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processHandshakePacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.HANDSHAKE) { + throw new IllegalArgumentException("Not a HANDSHAKE packet: " + quicPacket.packetType()); + } + final var handshake = this.handshakeFlow.handshakeCF(); + if (handshake.isDone() && debug.on()) { + debug.log("Receiving HandshakePacket(%s) after handshake is done: %s", + quicPacket.packetNumber(), quicPacket.frames()); + } + try { + if (quicPacket instanceof HandshakePacket hs) { + int total; + total = processHandshakePacketPayload(hs); + assert total == hs.payloadSize(); + continueHandshake(); + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Called to process a {@link RetryPacket} after it has been decrypted. + * @param quicPacket the retry quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a RETRY packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processRetryPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.RETRY) { + throw new IllegalArgumentException("Not a RETRY packet: " + quicPacket.packetType()); + } + try { + if (!(quicPacket instanceof RetryPacket rt)) { + throw new InternalError("Bad packet type: " + quicPacket); + } + assert stateHandle.helloSent() : "unexpected message"; + if (rt.retryToken().length == 0) { + if (debug.on()) { + debug.log("Invalid retry, empty token"); + } + return; + } + final QuicConnectionId currentPeerConnId = this.peerConnIdManager.getPeerConnId(); + if (rt.sourceId().equals(currentPeerConnId)) { + if (debug.on()) { + debug.log("Invalid retry, same connection ID"); + } + return; + } + if (this.peerConnIdManager.retryConnId() != null) { + if (debug.on()) { + debug.log("Ignoring retry, already got one"); + } + return; + } + // ignore retry if we already received initial packets + if (incomingInitialPacketSourceId != null) { + if (debug.on()) { + debug.log("Already received initial, ignoring retry"); + } + return; + } + final int version = rt.version(); + final QuicVersion retryVersion = QuicVersion.of(version).orElse(null); + if (retryVersion == null) { + if (debug.on()) { + debug.log("Ignoring retry packet with unknown version 0x" + + Integer.toHexString(version)); + } + // ignore the packet + return; + } + final QuicVersion originalVersion = this.quicVersion; // the original version used to establish the connection + if (originalVersion != retryVersion) { + if (debug.on()) { + debug.log("Ignoring retry packet with version 0x" + + Integer.toHexString(version) + + " since it doesn't match the original version 0x" + + Integer.toHexString(originalVersion.versionNumber())); + } + // ignore the packet + return; + } + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + initialToken = rt.retryToken(); + final QuicConnectionId retryConnId = rt.sourceId(); + this.peerConnIdManager.retryConnId(retryConnId); + quicTLSEngine.deriveInitialKeys(originalVersion, retryConnId.asReadOnlyBuffer()); + this.packetSpace(PacketNumberSpace.INITIAL).retry(); + handshakeFlow.localInitial.replayData(); + } finally { + tl.unlock(); + } + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * {@return the next (higher) max streams limit that should be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased} + * + * @param bidi true if bidirectional stream, false otherwise + */ + public long nextMaxStreamsLimit(final boolean bidi) { + if (isClientConnection() && bidi) return 0; // server does not open bidi streams + return streams.nextMaxStreamsLimit(bidi); + } + + /** + * Called when a stateless reset token is received. + */ + @Override + public void processStatelessReset() { + terminator.incomingStatelessReset(); + } + + /** + * Called to process a received {@link VersionNegotiationPacket} + * @param quicPacket the {@link VersionNegotiationPacket} + * @throws IllegalArgumentException if {@code quicPacket} is not a {@link PacketType#VERSIONS} + * packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processVersionNegotiationPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.VERSIONS) { + throw new IllegalArgumentException("Not a VERSIONS packet type: " + quicPacket.packetType()); + } + // servers aren't expected to receive version negotiation packet + if (!this.isClientConnection()) { + if (debug.on()) { + debug.log("(server) ignoring version negotiation packet"); + } + return; + } + try { + final var handshakeCF = this.handshakeFlow.handshakeCF(); + // we must ignore version negotiation if we already had a successful exchange + var versionCompatible = this.versionCompatible; + if (versionCompatible || handshakeCF.isDone()) { + if (debug.on()) { + debug.log("ignoring version negotiation packet (neg: %s, state: %s, hs: %s)", + versionCompatible, stateHandle, handshakeCF); + } + return; + } + // we shouldn't receive unsolicited version negotiation packets + assert stateHandle.helloSent(); + if (!(quicPacket instanceof VersionNegotiationPacket negotiate)) { + if (debug.on()) { + debug.log("Bad packet type %s for %s", + quicPacket.getClass().getName(), quicPacket); + } + return; + } + if (!negotiate.sourceId().equals(originalServerConnId())) { + if (debug.on()) { + debug.log("Received version negotiation packet with wrong connection id"); + debug.log("expected source id: %s, received source id: %s", + originalServerConnId(), negotiate.sourceId()); + debug.log("ignoring version negotiation packet (wrong id)"); + } + return; + } + final int[] serverSupportedVersions = negotiate.supportedVersions(); + if (debug.on()) { + debug.log("Received version negotiation packet with supported=%s", + Arrays.toString(serverSupportedVersions)); + } + assert this.quicInstance() instanceof QuicClient : "Not a quic client"; + final QuicClient client = (QuicClient) this.quicInstance(); + QuicVersion negotiatedVersion = null; + for (final int v : serverSupportedVersions) { + final QuicVersion serverVersion = QuicVersion.of(v).orElse(null); + if (serverVersion == null) { + if (debug.on()) { + debug.log("Ignoring unrecognized server supported version %d", v); + } + continue; + } + if (serverVersion == this.quicVersion) { + // RFC-9000, section 6.2: + // A client MUST discard a Version Negotiation packet that lists + // the QUIC version selected by the client. + if (debug.on()) { + debug.log("ignoring version negotiation packet since the version" + + " %d matches the current quic version selected by the client", v); + } + return; + } + // check if the current quic client is enabled for this version + if (!client.isVersionAvailable(serverVersion)) { + if (debug.on()) { + debug.log("Ignoring server supported version %d because the " + + "client isn't enabled for it", v); + } + continue; + } + if (debug.on()) { + if (negotiatedVersion == null) { + debug.log("Accepting server supported version %d", + serverVersion.versionNumber()); + negotiatedVersion = serverVersion; + } else { + // currently all versions are equal + debug.log("Skipping server supported version %d", + serverVersion.versionNumber()); + } + } + } + // at this point if negotiatedVersion is null, then it implies that none of the server + // supported versions are supported by the client. The spec expects us to abandon the + // current connection attempt in such cases (RFC-9000, section 6.2) + if (negotiatedVersion == null) { + final String msg = "No support for any of the QUIC versions being negotiated: " + + Arrays.toString(serverSupportedVersions); + if (debug.on()) { + debug.log("No version could be negotiated: %s", msg); + } + terminator.terminate(forException(new IOException(msg))); + return; + } + // a different version than the current client chosen version has been negotiated, + // switch the client connection to use this negotiated version + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + if (switchVersion(negotiatedVersion)) { + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + quicTLSEngine.restartHandshake(); + handshakeFlow.localInitial.reset(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + this.versionCompatible = true; + processedVersionsPacket = true; + } + } finally { + tl.unlock(); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to handle packet", t); + } + } + + } + + /** + * Switch to a new version after receiving a version negotiation + * packet. This method checks that no version was previously + * negotiated, in which case it switches the connection to the + * new version and returns true. + * Otherwise, it returns false. + * + * @param negotiated the new version that was negotiated + * @return true if switching to the new version was successful + */ + protected boolean switchVersion(QuicVersion negotiated) { + try { + assert !versionNegotiated; + if (debug.on()) + debug.log("switch to negotiated version %s", negotiated); + this.quicVersion = negotiated; + this.decoder = QuicPacketDecoder.of(negotiated); + this.encoder = QuicPacketEncoder.of(negotiated); + this.packetSpace(PacketNumberSpace.INITIAL).versionChanged(); + // regenerate the INITIAL keys using the new negotiated Quic version + this.quicTLSEngine.deriveInitialKeys(negotiated, originalServerConnId().asReadOnlyBuffer()); + return true; + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new RuntimeException("failed to switch to version", t); + } + } + + /** + * Mark the version as negotiated. No further version changes are possible. + * + * @param packetVersion the packet version + */ + protected void markVersionNegotiated(int packetVersion) { + int version = this.quicVersion.versionNumber(); + assert packetVersion == version; + if (!versionNegotiated) { + if (VERSION_NEGOTIATED.compareAndSet(this, false, true)) { + // negotiated version finalized + quicTLSEngine.versionNegotiated(QuicVersion.of(version).get()); + } + } + } + + /** + * {@return a boolean value telling whether the datagram in the + * protection record is complete} + * The datagram is complete when no other packet need to be coalesced + * in the datagram. + * If a datagram is complete, it is ready to be sent. + * + * @param protectionRecord the protection record + */ + protected boolean isDatagramComplete(ProtectionRecord protectionRecord) { + return protectionRecord.datagram.remaining() == 0 + || protectionRecord.flags == ProtectionRecord.SINGLE_PACKET + || (protectionRecord.flags & ProtectionRecord.LAST_PACKET) != 0 + || (protectionRecord.flags & ProtectionRecord.COALESCED) == 0; + } + + /** + * {@return the peer address that should be used when sending datagram + * to the peer} + */ + public InetSocketAddress peerAddress() { + return peerAddress; + } + + /** + * {@return the local address of the quic endpoint} + * @throws UncheckedIOException if the address is not available + */ + public SocketAddress localAddress() { + try { + var endpoint = this.endpoint; + if (endpoint == null) { + throw new IOException("no endpoint defined"); + } + return endpoint.getLocalAddress(); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + /** + * Pushes the {@linkplain ProtectionRecord#datagram() datagram} contained in + * the {@code protectionRecord}, through the {@linkplain QuicEndpoint endpoint}. + * + * @param protectionRecord the ProtectionRecord containing the datagram + */ + private void pushEncryptedDatagram(final ProtectionRecord protectionRecord) { + final long packetNumber = protectionRecord.packet().packetNumber(); + assert packetNumber >= 0 : "unexpected packet number: " + packetNumber; + final long retransmittedPacketNumber = protectionRecord.retransmittedPacketNumber(); + assert packetNumber > retransmittedPacketNumber : "packet number: " + packetNumber + + " was expected to be greater than packet the packet being retransmitted: " + + retransmittedPacketNumber; + final boolean pktContainsConnClose = containsConnectionClose(protectionRecord.packet()); + // if the connection isn't open then except for the packet containing a CONNECTION_CLOSE + // frame, we don't push any other packets. + if (!isOpen() && !pktContainsConnClose) { + if (debug.on()) { + debug.log("connection isn't open - ignoring %s(pn:%s): frames:%s", + protectionRecord.packet.packetType(), + protectionRecord.packet.packetNumber(), + protectionRecord.packet.frames()); + } + datagramDropped(new QuicDatagram(this, peerAddress, protectionRecord.datagram)); + return; + } + // TODO: revisit this: we need to figure out how best to emit coalesced packet, + // and having one protection record per packet may not be the the best. + // Maybe a protection record should have a list of coalesced packets + // instead of a single packet? + final ByteBuffer datagram = protectionRecord.datagram(); + final int firstPacketOffset = protectionRecord.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + if (debug.on()) { + final PacketType packetType = protectionRecord.packet().packetType(); + final int packetOffset = protectionRecord.packetOffset(); + if (packetOffset == firstPacketOffset) { + debug.log("Pushing datagram([%s(%d)], %d)", packetType, packetNumber, + datagram.remaining()); + } else { + debug.log("Pushing coalesced datagram([%s(%d)], %d)", + packetType, packetNumber, datagram.remaining()); + } + } + + // upon successful sending of the datagram, notify that the packet was sent + // we call packetSent just before sending the packet here, to make sure + // that the PendingAcknowledgement will be present in the queue before + // we receive the ACK frame from the server. Not doing this would create + // a race where the peer might be able to send the ack, and we might process + // it, before the PendingAcknowledgement is added. + final QuicPacket packet = protectionRecord.packet(); + final PacketSpace packetSpace = packetSpace(packet.numberSpace()); + packetSpace.packetSent(packet, retransmittedPacketNumber, packetNumber); + + // if we are sending a packet containing a CONNECTION_CLOSE frame, then we + // also switch/remove the current connection instance in the endpoint. + if (pktContainsConnClose) { + if (stateHandle.isMarked(QuicConnectionState.DRAINING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in DRAINING. This implies that the local endpoint + // is responding to an incoming CONNECTION_CLOSE frame from the peer. + // we remove the connection from the endpoint for such cases. + endpoint.pushClosedDatagram(this, peerAddress(), datagram); + } else if (stateHandle.isMarked(QuicConnectionState.CLOSING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in CLOSING. For such cases, we switch this + // connection in the endpoint to one which responds with + // CONNECTION_CLOSE frame for any subsequent incoming packets + // from the peer. + endpoint.pushClosingDatagram(this, peerAddress(), datagram); + } else { + // should not happen + throw new IllegalStateException("connection is neither draining nor closing," + + " cannot send a connection close frame"); + } + } else { + pushDatagram(peerAddress(), datagram); + } + // RFC-9000, section 10.1: An endpoint also restarts its idle timer when sending + // an ack-eliciting packet ... + if (packet.isAckEliciting()) { + this.terminator.keepAlive(); + } + } + + /** + * Calls the {@link QuicEndpoint#pushDatagram(QuicPacketReceiver, SocketAddress, ByteBuffer)} + * + * @param destination The destination of this datagram + * @param datagram The datagram + */ + protected void pushDatagram(final SocketAddress destination, final ByteBuffer datagram) { + endpoint.pushDatagram(this, destination, datagram); + } + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + @Override + public void onWriteError(Throwable t) { + // log exception if still opened + if (stateHandle.opened()) { + if (Log.errors()) { + Log.logError("%s: Failed to write datagram: %s", dbgTag(), t ); + Log.logError(t); + } else if (debug.on()) { + debug.log("Failed to write datagram", t); + } + } + } + + /** + * Called when a packet couldn't be processed + * @param t the error that occurred + */ + public void onProcessingError(QuicPacket packet, Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public final CompletableFuture startHandshake() { + if (!isClientConnection()) { + throw new UnsupportedOperationException("Not a client connection, cannot start handshake"); + } + if (!this.startHandshakeCalled.compareAndSet(false, true)) { + throw new IllegalStateException("handshake has already been started on connection"); + } + if (this.peerAddress.isUnresolved()) { + // fail if address is unresolved + return MinimalFuture.failedFuture( + Utils.toConnectException(new UnresolvedAddressException())); + } + CompletableFuture cf; + try { + // register the connection with an endpoint + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + endpoint.registerNewConnection(this); + cf = MinimalFuture.completedFuture(null); + } catch (Throwable t) { + cf = MinimalFuture.failedFuture(t); + } + return cf.thenApply(this::sendFirstInitialPacket) + .exceptionally((t) -> { + // complete the handshake CFs with the failure + handshakeFlow.failHandshakeCFs(t); + return handshakeFlow; + }) + .thenCompose(HandshakeFlow::handshakeCF) + .thenApply(this::onHandshakeCompletion); + } + + /** + * This method is called when the handshake is successfully completed. + * @param result the result of the handshake + */ + protected QuicEndpoint onHandshakeCompletion(final HandshakeState result) { + if (debug.on()) { + debug.log("Quic handshake successfully completed with %s(%s)", + quicTLSEngine.getApplicationProtocol(), peerAddress()); + } + // now that the handshake has successfully completed, start the + // idle timeout management for this connection + this.idleTimeoutManager.start(); + return this.endpoint; + } + + protected HandshakeFlow handshakeFlow() { + return handshakeFlow; + } + + protected void startInitialTimer() { + if (!isClientConnection()) return; + MaxInitialTimer initialTimer = maxInitialTimer; + if (initialTimer == null && DEFAULT_MAX_INITIAL_TIMEOUT < Integer.MAX_VALUE) { + Deadline maxInitialDeadline = null; + synchronized (this) { + initialTimer = maxInitialTimer; + if (initialTimer == null) { + Deadline now = this.endpoint().timeSource().instant(); + maxInitialDeadline = now.plusSeconds(DEFAULT_MAX_INITIAL_TIMEOUT); + initialTimer = maxInitialTimer = new MaxInitialTimer(this.endpoint().timer(), maxInitialDeadline); + } + } + if (maxInitialDeadline != null) { + if (Log.quic()) { + Log.logQuic("{0}: Arming quic initial timer for {1}", logTag(), + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline)); + } + if (debug.on()) { + debug.log("Arming quic initial timer for %s seconds", + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline).toSeconds()); + } + initialTimer.timerQueue.reschedule(initialTimer, maxInitialDeadline); + } + } + } + + // adaptation to Function + private HandshakeFlow sendFirstInitialPacket(Void unused) { + // may happen if connection cancelled before endpoint is + // created + final TerminationCause tc = terminationCause(); + if (tc != null) { + throw new CompletionException(tc.getCloseCause()); + } + try { + startInitialTimer(); + if (Log.quic()) { + Log.logQuic(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } else if (debug.on()) { + debug.log(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } + var localAddress = endpoint.getLocalAddress(); + var conflict = Utils.addressConflict(localAddress, peerAddress); + if (conflict != null) { + String msg = conflict; + if (debug.on()) { + debug.log("%s (local: %s, remote: %s)", msg, localAddress, peerAddress); + } + Log.logError("{0} {1} (local: {2}, remote: {3})", logTag(), + msg, localAddress, peerAddress); + throw new SSLHandshakeException(msg); + } + final QuicConnectionId clientSelectedPeerId = initialServerConnectionId(); + this.peerConnIdManager.originalServerConnId(clientSelectedPeerId); + handshakeFlow.markHandshakeStart(); + stateHandle.markHelloSent(); + // the "original version" used to establish the connection + final QuicVersion originalVersion = this.quicVersion; + quicTLSEngine.deriveInitialKeys(originalVersion, clientSelectedPeerId.asReadOnlyBuffer()); + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + handshakeFlow.localInitial.keepReplayData(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new CompletionException(terminationCause().getCloseCause()); + } + return handshakeFlow; + } + + private static final Random RANDOM = new SecureRandom(); + + private QuicConnectionId initialServerConnectionId() { + byte[] bytes = new byte[INITIAL_SERVER_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(bytes); + return new PeerConnectionId(bytes); + } + + /** + * Compose a list of Quic frames containing a crypto frame and an ack frame, + * omitting null frames. + * @param crypto the crypto frame + * @param ack the ack frame + * @return A list of {@link QuicFrame}. + */ + private List makeList(CryptoFrame crypto, AckFrame ack) { + List frames = new ArrayList<>(2); + if (crypto != null) { + frames.add(crypto); + } + if (ack != null) { + frames.add(ack); + } + return frames; + } + + /** + * Allocate a {@link ByteBuffer} that can be used to encrypt the + * given packet. + * @param packet the packet to encrypt + * @return a new {@link ByteBuffer} with sufficient space to encrypt + * the given packet. + */ + protected ByteBuffer allocateDatagramForEncryption(QuicPacket packet) { + int size = packet.size(); + if (packet.hasLength()) { // packet can be coalesced + size = Math.max(size, getMaxDatagramSize()); + } + if (size > getMaxDatagramSize()) { + + if (Log.errors()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + Log.logError(logTag() + ": Packet too big: " + packet.prettyPrint()); + Log.logError(error); + } else if (debug.on()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + debug.log("Packet too big: " + packet.prettyPrint()); + debug.log(error); + } + // Revisit: if we implement Path MTU detection, then the max datagram size + // may evolve, increasing or decreasing as the path change. + // In which case - we may want to tune this, down and only + // log an error or warning? + final String errMsg = "Failed to encode packet, too big: " + size; + terminator.terminate(forTransportError(PROTOCOL_VIOLATION).loggedAs(errMsg)); + throw new UncheckedIOException(terminator.getTerminationCause().getCloseCause()); + } + return getOutgoingByteBuffer(size); + } + + /** + * {@return the maximum datagram size that can be used on the + * connection path} + * @implSpec + * Initially this is {@link #DEFAULT_DATAGRAM_SIZE}, but the + * value will then be decided if the peer sends a specific size + * in the transport parameters and the value can further evolve based + * on path MTU. + */ + // this is public for use in tests + public int getMaxDatagramSize() { + // TODO: we should implement path MTU detection, or maybe let + // this be configurable. Sizes of 32256 or 64512 seem to + // be giving much better throughput when downloading. + // large files + return Math.min(maxPeerAdvertisedPayloadSize, pathMTU); + } + + /** + * Retrieves cryptographic messages from TLS engine, enqueues them for sending + * and starts the transmitter. + */ + protected void continueHandshake() { + handshakeScheduler.runOrSchedule(); + } + + protected void continueHandshake0() { + try { + continueHandshake1(); + } catch (Throwable t) { + var flow = handshakeFlow; + flow.handshakeReachedPeerCF.completeExceptionally(t); + flow.handshakeCF.completeExceptionally(t); + } + } + + private void continueHandshake1() throws IOException { + HandshakeFlow flow = handshakeFlow; + // make sure the localInitialQueue is not modified concurrently + // while we are in this loop + boolean handshakeDataAvailable = false; + boolean initialDataAvailable = false; + for (;;) { + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("continueHandshake: state: %s", handshakeState); + } + if (handshakeState == QuicTLSEngine.HandshakeState.NEED_SEND_CRYPTO) { + // buffer next TLS message + KeySpace keySpace = quicTLSEngine.getCurrentSendKeySpace(); + ByteBuffer payloadBuffer; + handshakeLock.lock(); + try { + payloadBuffer = quicTLSEngine.getHandshakeBytes(keySpace); + assert payloadBuffer != null; + assert payloadBuffer.hasRemaining(); + if (keySpace == KeySpace.INITIAL) { + flow.localInitial.enqueue(payloadBuffer); + initialDataAvailable = true; + } else if (keySpace == KeySpace.HANDSHAKE) { + flow.localHandshake.enqueue(payloadBuffer); + handshakeDataAvailable = true; + } + } finally { + handshakeLock.unlock(); + } + + assert payloadBuffer != null; + if (debug.on()) { + debug.log("continueHandshake: buffered %s bytes in %s keyspace", + payloadBuffer.remaining(), keySpace); + } + } else if (handshakeState == QuicTLSEngine.HandshakeState.NEED_TASK) { + quicTLSEngine.getDelegatedTask().run(); + } else { + if (debug.on()) { + debug.log("continueHandshake: nothing to do (state: %s)", handshakeState); + } + if (initialDataAvailable) { + packetSpaces.initial.runTransmitter(); + } + if (handshakeDataAvailable && flow.localInitial.remaining() == 0) { + packetSpaces.handshake.runTransmitter(); + } + return; + } + } + } + + private boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (packetNumberSpace != PacketNumberSpace.APPLICATION) { + // This method can be called by two packet spaces: INITIAL and HANDSHAKE. + // We need to lock to make sure that the method is not run concurrently. + handshakeLock.lock(); + try { + return sendInitialOrHandshakeData(packetNumberSpace); + } finally { + handshakeLock.unlock(); + } + } else { + return oneRttSndQueue.send1RTTData(); + } + } + + private boolean sendInitialOrHandshakeData(final PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: Send %s data", logTag(), packetNumberSpace)); + } + final HandshakeFlow flow = handshakeFlow; + final QuicConnectionId peerConnId = peerConnectionId(); + if (packetNumberSpace == PacketNumberSpace.INITIAL && flow.localInitial.remaining() > 0) { + // process buffered initial data + byte[] token = initialToken(); + int tksize = token == null ? 0 : token.length; + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.INITIAL); + int maxDstIdLength = isClientConnection() ? + MAX_CONNECTION_ID_LENGTH : // reserve space for the id to grow + peerConnId.length(); + int maxSrcIdLength = connectionId.length(); + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, 4, tksize, + maxSrcIdLength, maxDstIdLength, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // compute how many bytes were reserved to allow smooth retransmission + // of packets + int reserved = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, + computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + tksize, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE) - maxPayloadSize; + assert reserved >= 0 : "reserved is negative: " + reserved; + if (debug.on()) { + debug.log("reserved %s byte in initial packet", reserved); + } + if (maxPayloadSize < 5) { + // token too long, can't fit a crypto frame in this packet. Abort. + final String msg = "Initial token too large, maxPayload: " + maxPayloadSize; + terminator.terminate(TerminationCause.forException(new IOException(msg))); + return false; + } + AckFrame ackFrame = packetSpace.getNextAckFrame(false, maxPayloadSize); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + + CryptoFrame crypto = flow.localInitial.produceFrame(maxPayloadSize - ackSize); + int cryptoSize = crypto == null ? 0 : crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building initial packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newInitialPacket( + connectionId, peerConnId, token, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("initial packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size == SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + stateHandle.markHelloSent(); + if (debug.on()) { + debug.log("protecting initial quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + if (flow.localHandshake.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local handshake has remaining, starting HANDSHAKE transmitter", logTag())); + } + packetSpaces.handshake.runTransmitter(); + } + } else if (packetNumberSpace == PacketNumberSpace.HANDSHAKE && flow.localHandshake.remaining() > 0) { + // process buffered handshake data + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.HANDSHAKE); + AckFrame ackFrame = packetSpace.getNextAckFrame(false); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxHandshakePayloadSize(codingContext, + packetNumber, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + maxPayloadSize = maxPayloadSize - ackSize; + + final CryptoFrame crypto = flow.localHandshake.produceFrame(maxPayloadSize); + assert crypto != null : "Handshake data was available (" + + flow.localHandshake.remaining() + " bytes) for sending, but no CRYPTO" + + " frame was produced, for max frame size: " + maxPayloadSize; + int cryptoSize = crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building handshake packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newHandshakePacket( + connectionId, peerConnId, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("handshake packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size <= SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + if (debug.on()) { + debug.log("protecting handshake quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("Handshake state is now: %s", handshakeState); + } + if (flow.localHandshake.remaining() == 0 + && quicTLSEngine.isTLSHandshakeComplete() + && !flow.handshakeCF.isDone()) { + if (stateHandle.markHandshakeComplete()) { + if (debug.on()) { + debug.log("Handshake completed"); + } + completeHandshakeCF(); + } + } + if (!packetSpaces.initial().isClosed() && flow.localInitial.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local initial has remaining, starting INITIAL transmitter", logTag())); + } + packetSpaces.initial.runTransmitter(); + } + } else { + return false; + } + return true; + } + + public QuicTransportParameters peerTransportParameters() { + return peerTransportParameters; + } + + public QuicTransportParameters localTransportParameters() { + return localTransportParameters; + } + + protected void consumeQuicParameters(final ByteBuffer byteBuffer) throws QuicTransportException { + final QuicTransportParameters params = QuicTransportParameters.decode(byteBuffer); + if (debug.on()) { + debug.log("Received peer Quic transport params: %s", params.toStringWithValues()); + } + final QuicConnectionId retryConnId = this.peerConnIdManager.retryConnId(); + if (params.isPresent(ParameterId.retry_source_connection_id)) { + if (retryConnId == null) { + throw new QuicTransportException("Retry connection ID was set even though no retry was performed", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } else if (!params.matches(ParameterId.retry_source_connection_id, retryConnId)) { + throw new QuicTransportException("Retry connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } else { + if (retryConnId != null) { + throw new QuicTransportException("Retry connection ID was expected but absent", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (!params.isPresent(ParameterId.original_destination_connection_id)) { + throw new QuicTransportException( + "Original connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + if (!params.isPresent(initial_source_connection_id)) { + throw new QuicTransportException( + "Initial source connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + final QuicConnectionId clientSelectedPeerConnId = this.peerConnIdManager.originalServerConnId(); + if (!params.matches(ParameterId.original_destination_connection_id, clientSelectedPeerConnId)) { + throw new QuicTransportException( + "Original connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (!params.matches(initial_source_connection_id, incomingInitialPacketSourceId)) { + throw new QuicTransportException( + "Initial source connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + // RFC-9000, section 18.2: A server that chooses a zero-length connection ID MUST NOT + // provide a preferred address. + if (peerConnectionId().length() == 0 && + params.isPresent(ParameterId.preferred_address)) { + throw new QuicTransportException( + "Preferred address present but connection ID has zero length", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(active_connection_id_limit)) { + final long limit = params.getIntParameter(active_connection_id_limit); + if (limit < 2) { + throw new QuicTransportException( + "Invalid active_connection_id_limit " + limit, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (params.isPresent(ParameterId.stateless_reset_token)) { + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + if (statelessResetToken.length != RESET_TOKEN_LENGTH) { + // RFC states 16 bytes for stateless token + throw new QuicTransportException( + "Invalid stateless reset token length " + statelessResetToken.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + VersionInformation vi = + params.getVersionInformationParameter(version_information); + if (vi != null) { + if (vi.chosenVersion() != quicVersion().versionNumber()) { + throw new QuicTransportException( + "[version_information] Chosen Version does not match version in use", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (processedVersionsPacket) { + if (vi.availableVersions().length == 0) { + throw new QuicTransportException( + "[version_information] available versions empty", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (Arrays.stream(vi.availableVersions()) + .anyMatch(i -> i == originalVersion.versionNumber())) { + throw new QuicTransportException( + "[version_information] original version was available", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + } else { + if (processedVersionsPacket && quicVersion != QuicVersion.QUIC_V1) { + throw new QuicTransportException( + "version_information parameter absent", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + handleIncomingPeerTransportParams(params); + + // params.setIntParameter(ParameterId.max_idle_timeout, TimeUnit.SECONDS.toMillis(30)); + // params.setParameter(ParameterId.stateless_reset_token, ...); // no token + // params.setIntParameter(ParameterId.initial_max_data, DEFAULT_INITIAL_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_local, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_remote, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_uni, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_streams_bidi, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.initial_max_streams_uni, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + // params.setPreferredAddressParameter(ParameterId.preferred_address, ...); + // params.setIntParameter(ParameterId.active_connection_id_limit, 2); + } + + /** + * {@return the number of (active) connection ids that this endpoint is willing + * to accept from the peer for a given connection} + */ + protected long getLocalActiveConnIDLimit() { + // currently we don't accept anything more than 2 (the RFC defined default minimum) + return 2; + } + + /** + * {@return the number of (active) connection ids that the peer is willing to accept + * for a given connection} + */ + protected long getPeerActiveConnIDLimit() { + return this.peerActiveConnIdsLimit; + } + + protected ByteBuffer buildInitialParameters() { + final QuicTransportParameters params = new QuicTransportParameters(this.transportParams); + setIntParamIfNotSet(params, active_connection_id_limit, this::getLocalActiveConnIDLimit); + final long idleTimeoutMillis = TimeUnit.SECONDS.toMillis( + Utils.getLongProperty("jdk.httpclient.quic.idleTimeout", 30)); + setIntParamIfNotSet(params, max_idle_timeout, () -> idleTimeoutMillis); + setIntParamIfNotSet(params, max_udp_payload_size, () -> { + assert this.endpoint != null : "Endpoint hasn't been set"; + return (long) this.endpoint.getMaxUdpPayloadSize(); + }); + setIntParamIfNotSet(params, initial_max_data, () -> DEFAULT_INITIAL_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_local, + () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_uni, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_remote, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_streams_uni, () -> (long) DEFAULT_MAX_UNI_STREAMS); + setIntParamIfNotSet(params, initial_max_streams_bidi, () -> (long) DEFAULT_MAX_BIDI_STREAMS); + if (!params.isPresent(initial_source_connection_id)) { + params.setParameter(initial_source_connection_id, connectionId.getBytes()); + } + if (!params.isPresent(version_information)) { + final VersionInformation vi = + QuicTransportParameters.buildVersionInformation(quicVersion, + quicInstance().getAvailableVersions()); + params.setVersionInformationParameter(version_information, vi); + } + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + final ByteBuffer buf = ByteBuffer.allocate(params.size()); + params.encode(buf); + buf.flip(); + if (debug.on()) { + debug.log("local transport params: %s", params.toStringWithValues()); + } + newLocalTransportParameters(params); + return buf; + } + + protected static void setIntParamIfNotSet(final QuicTransportParameters params, + final ParameterId paramId, + final Supplier valueSupplier) { + if (params.isPresent(paramId)) { + return; + } + params.setIntParameter(paramId, valueSupplier.get()); + } + + // the token to be included in initial packets, if any. + private byte[] initialToken() { + return initialToken; + } + + protected void newLocalTransportParameters(QuicTransportParameters params) { + localTransportParameters = params; + oneRttRcvQueue.newLocalParameters(params); + streams.newLocalTransportParameters(params); + final long idleTimeout = params.getIntParameter(max_idle_timeout, 0); + this.idleTimeoutManager.localIdleTimeout(idleTimeout); + } + + private List ackOrPing(AckFrame ack, boolean sendPing) { + if (sendPing) { + return ack == null ? List.of(new PingFrame()) : List.of(new PingFrame(), ack); + } + assert ack != null; + return List.of(ack); + } + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + private long emitAckPacket(final PacketSpace packetSpaceManager, final AckFrame ackFrame, + final boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + if (ackFrame == null && !sendPing) { + return -1L; + } + if (debug.on()) { + if (sendPing) { + debug.log("Sending PING packet %s ack", + ackFrame == null ? "without" : "with"); + } else { + debug.log("sending ACK packet"); + } + } + final List frames = ackOrPing(ackFrame, sendPing); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + if (debug.on()) { + debug.log("Sending packet for %s, frame=%s", packetNumberSpace, frames); + } + final KeySpace keySpace = switch (packetNumberSpace) { + case APPLICATION -> KeySpace.ONE_RTT; + case HANDSHAKE -> KeySpace.HANDSHAKE; + case INITIAL -> KeySpace.INITIAL; + default -> throw new UnsupportedOperationException( + "Invalid packet number space: " + packetNumberSpace); + }; + if (sendPing && Log.quicRetransmit()) { + Log.logQuic("{0} {1}: sending PingFrame", logTag(), keySpace); + } + final QuicPacket ackpacket = encoder.newOutgoingPacket(keySpace, + packetSpaceManager, localConnectionId(), + peerConnectionId(), initialToken(), frames, codingContext); + pushDatagram(ProtectionRecord.single(ackpacket, this::allocateDatagramForEncryption)); + return ackpacket.packetNumber(); + } + + private LinkedList removeOutdatedFrames(List frames) { + // Remove frames that should not be retransmitted + LinkedList result = new LinkedList<>(); + for (QuicFrame f : frames) { + if (!(f instanceof PaddingFrame) && + !(f instanceof AckFrame) && + !(f instanceof PathChallengeFrame) && + !(f instanceof PathResponseFrame)) { + result.add(f); + } + } + return result; + } + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + */ + private void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("Retransmitting packet [type=%s, pn=%d, attempts:%d]: %s", + packet.packetType(), packet.packetNumber(), attempts, packet); + } + + assert packetSpaceManager.packetNumberSpace() == packet.numberSpace(); + long oldPacketNumber = packet.packetNumber(); + assert oldPacketNumber >= 0; + + long largestAckedPN = packetSpaceManager.getLargestPeerAckedPN(); + long newPacketNumber = packetSpaceManager.allocateNextPN(); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId.length(); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + final int initialDstIdLength = MAX_CONNECTION_ID_LENGTH; // reserve space for the ID to grow + + int maxPayloadSize = switch (packetNumberSpace) { + case APPLICATION -> QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, newPacketNumber, dstIdLength, maxDatagramSize, largestAckedPN); + case INITIAL -> QuicPacketEncoder.computeMaxInitialPayloadSize( + codingContext, computePacketNumberLength(newPacketNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + ((InitialPacket) packet).tokenLength(), + localConnectionId().length(), initialDstIdLength, maxDatagramSize); + case HANDSHAKE -> QuicPacketEncoder.computeMaxHandshakePayloadSize( + codingContext, newPacketNumber, localConnectionId().length(), + dstIdLength, maxDatagramSize); + default -> throw new IllegalArgumentException( + "Invalid packet number space: " + packetNumberSpace); + }; + + // The new packet may have larger size(), which might no longer fit inside + // the maximum datagram size supported on the path. To avoid that, we + // strip the padding and old ack frame from the original packet, and + // include the new ack frame only if it fits in the available size. + LinkedList frames = removeOutdatedFrames(packet.frames()); + int size = frames.stream().mapToInt(QuicFrame::size).sum(); + int remaining = maxPayloadSize - size; + AckFrame ack = packetSpaceManager.getNextAckFrame(false, remaining); + if (ack != null) { + assert ack.size() <= remaining : "AckFrame size %s is bigger than %s" + .formatted(ack.size(), remaining); + frames.addFirst(ack); + } + QuicPacket retransmitted = + switch (packet.packetType()) { + case INITIAL -> encoder.newInitialPacket(localConnectionId(), + peerConnectionId, ((InitialPacket) packet).token(), + newPacketNumber, largestAckedPN, frames, + codingContext); + case HANDSHAKE -> encoder.newHandshakePacket(localConnectionId(), + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + case ONERTT, ZERORTT -> encoder.newOneRttPacket( + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + default -> throw new IllegalArgumentException("packetType: %s, packet: %s" + .formatted(packet.packetType(), packet.packetNumber())); + }; + + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: retransmitting packet [%s] pn:%s as pn:%s".formatted( + logTag(), packet.packetType(), oldPacketNumber, newPacketNumber)); + } + pushDatagram(ProtectionRecord.retransmitting(retransmitted, + oldPacketNumber, + this::allocateDatagramForEncryption)); + } + + @Override + public CompletableFuture requestSendPing() { + final KeySpace space = quicTLSEngine.getCurrentSendKeySpace(); + final PacketSpace spaceManager = packetSpaces.get(PacketNumberSpace.of(space)); + return spaceManager.requestSendPing(); + } + + /** + * {@return the underlying {@code NetworkChannel} used by this connection, + * which may be {@code null} if the endpoint has not been configured yet} + */ + public NetworkChannel channel() { + QuicEndpoint endpoint = this.endpoint; + return endpoint == null ? null : endpoint.channel(); + } + + @Override + public String toString() { + return cachedToString; + } + + @Override + public boolean isOpen() { + return stateHandle.opened(); + } + + @Override + public TerminationCause terminationCause() { + return terminator.getTerminationCause(); + } + + public final CompletableFuture futureTerminationCause() { + return terminator.futureTerminationCause(); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return this.terminator; + } + + /** + * {@return true if this connection is a client connection} + * Server side connections will return false. + */ + public boolean isClientConnection() { + return true; + } + + /** + * Called when new quic transport parameters are available from the peer. + * @param params the peer's new quic transport parameter + */ + protected void handleIncomingPeerTransportParams(final QuicTransportParameters params) { + peerTransportParameters = params; + this.idleTimeoutManager.peerIdleTimeout(params.getIntParameter(max_idle_timeout)); + // when we reach here, the value for max_udp_payload_size has already been + // asserted that it isn't outside the allowed range of 1200 to 65527. That has + // happened in QuicTransportParameters.checkParameter(). + // intentional cast to int since the value will be within int range + maxPeerAdvertisedPayloadSize = (int) params.getIntParameter(max_udp_payload_size); + congestionController.updateMaxDatagramSize(getMaxDatagramSize()); + if (params.isPresent(ParameterId.initial_max_data)) { + oneRttSndQueue.setMaxData(params.getIntParameter(ParameterId.initial_max_data), true); + } + streams.newPeerTransportParameters(params); + packetSpaces.app().updatePeerTransportParameters( + params.getIntParameter(ParameterId.max_ack_delay), + params.getIntParameter(ParameterId.ack_delay_exponent)); + // param value for this param is already validated outside of this method, so we just + // set the value without any validations + this.peerActiveConnIdsLimit = params.getIntParameter(active_connection_id_limit); + if (params.isPresent(ParameterId.stateless_reset_token)) { + // the stateless reset token for the handshake connection id + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + // register with peer connid manager + this.peerConnIdManager.handshakeStatelessResetToken(statelessResetToken); + } + if (params.isPresent(ParameterId.preferred_address)) { + final byte[] val = params.getParameter(ParameterId.preferred_address); + final ByteBuffer preferredConnId = QuicTransportParameters.getPreferredConnectionId(val); + final byte[] preferredStatelessResetToken = QuicTransportParameters + .getPreferredStatelessResetToken(val); + this.peerConnIdManager.handlePreferredAddress(preferredConnId, preferredStatelessResetToken); + } + if (debug.on()) { + debug.log("incoming peer parameters handled"); + } + } + + protected void incomingInitialFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.initial.processAckFrame(frame); + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + } + + protected int incomingInitialFrame(final CryptoFrame frame) throws QuicTransportException { + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + final CryptoDataFlow peerInitial = handshakeFlow.peerInitial; + final long buffer = frame.offset() + frame.length() - peerInitial.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.INITIAL, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerInitial.receive(frame); + while (nextFrame != null) { + if (debug.on()) { + debug.log("Provide crypto frame to engine: %s", nextFrame); + } + quicTLSEngine.consumeHandshakeBytes(KeySpace.INITIAL, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerInitial.poll(); + if (debug.on()) { + debug.log("Provided: " + provided); + } + } + return provided; + } + + protected void incomingInitialFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incomingHandshakeFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.handshake.processAckFrame(frame); + } + + protected int incomingHandshakeFrame(final CryptoFrame frame) throws QuicTransportException { + final CryptoDataFlow peerHandshake = handshakeFlow.peerHandshake; + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + final long buffer = frame.offset() + frame.length() - peerHandshake.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.HANDSHAKE, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerHandshake.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.HANDSHAKE, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerHandshake.poll(); + } + return provided; + } + + protected void incomingHandshakeFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final AckFrame ackFrame) throws QuicTransportException { + packetSpaces.app.processAckFrame(ackFrame); + } + + protected void incoming1RTTFrame(final StreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final CryptoFrame frame) throws QuicTransportException { + final long buffer = frame.offset() + frame.length() - peerCryptoFlow.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.ONE_RTT, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + CryptoFrame nextFrame = peerCryptoFlow.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.ONE_RTT, nextFrame.payload()); + nextFrame = peerCryptoFlow.poll(); + } + } + + protected void incoming1RTTFrame(final ResetStreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final StreamDataBlockedFrame frame) + throws QuicTransportException { + final QuicReceiverStream stream = getReceivingStream(frame.streamId(), frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final DataBlockedFrame frame) throws QuicTransportException { + // TODO implement similar logic as STREAM_DATA_BLOCKED frame receipt + // and increment gradually if consumption is more than 1/4th the window size of the + // connection + } + + protected void incoming1RTTFrame(final StreamsBlockedFrame frame) + throws QuicTransportException { + if (frame.maxStreams() > MAX_STREAMS_VALUE_LIMIT) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + streams.peerStreamsBlocked(frame); + } + + protected void incoming1RTTFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final MaxDataFrame frame) throws QuicTransportException { + oneRttSndQueue.setMaxData(frame.maxData(), false); + } + + protected void incoming1RTTFrame(final MaxStreamDataFrame frame) + throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.setMaxStreamData(stream, frame.maxStreamData()); + } + } + + protected void incoming1RTTFrame(final MaxStreamsFrame frame) throws QuicTransportException { + if (frame.maxStreams() >> 60 != 0) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final boolean increased = streams.tryIncreaseStreamLimit(frame); + if (debug.on()) { + debug.log((increased ? "increased" : "did not increase") + + " " + (frame.isBidi() ? "bidi" : "uni") + + " stream limit to " + frame.maxStreams()); + } + } + + protected void incoming1RTTFrame(final StopSendingFrame frame) throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.stopSendingReceived(stream, + frame.errorCode()); + } + } + + protected void incoming1RTTFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final HandshakeDoneFrame frame) + throws QuicTransportException { + if (quicTLSEngine.tryReceiveHandshakeDone()) { + // now that HANDSHAKE_DONE is received (and thus handshake confirmed), + // close the HANDSHAKE packet space (and thus discard the keys) + if (debug.on()) { + debug.log("received HANDSHAKE_DONE from server, initiating close of" + + " HANDSHAKE packet space"); + } + packetSpaces.handshake.close(); + } + packetSpaces.app.confirmHandshake(); + } + + protected void incoming1RTTFrame(final NewConnectionIDFrame frame) + throws QuicTransportException { + if (peerConnectionId().length() == 0) { + throw new QuicTransportException( + "NEW_CONNECTION_ID not allowed here", + null, frame.getTypeField(), PROTOCOL_VIOLATION); + } + this.peerConnIdManager.handleNewConnectionIdFrame(frame); + } + + protected void incoming1RTTFrame(final OneRttPacket oneRttPacket, + final RetireConnectionIDFrame frame) + throws QuicTransportException { + this.localConnIdManager.handleRetireConnectionIdFrame(oneRttPacket.destinationId(), + PacketType.ONERTT, frame); + } + + protected void incoming1RTTFrame(final NewTokenFrame frame) throws QuicTransportException { + // as per RFC 9000, section 19.7, token cannot be empty and if it is, then + // a connection error of type FRAME_ENCODING_ERROR needs to be raised + final byte[] newToken = frame.token(); + if (newToken.length == 0) { + throw new QuicTransportException("Empty token in NEW_TOKEN frame", + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + final QuicClient quicClient = (QuicClient) this.quicInstance; + // set this as the initial token to be used in INITIAL packets when attempting + // any new subsequent connections against this same target server + quicClient.registerInitialToken(this.peerAddress, newToken); + if (debug.on()) { + debug.log("Registered a new (initial) token for peer " + this.peerAddress); + } + } + + protected void incoming1RTTFrame(final PathResponseFrame frame) + throws QuicTransportException { + throw new QuicTransportException("Unmatched PATH_RESPONSE frame", + KeySpace.ONE_RTT, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + protected void incoming1RTTFrame(final PathChallengeFrame frame) + throws QuicTransportException { + pathChallengeFrameQueue.offer(frame); + if (pathChallengeFrameQueue.size() > 3) { + // we don't expect to hold more than 1 PathChallenge per path. + // If there's more than 3 outstanding challenges, drop the oldest one. + pathChallengeFrameQueue.poll(); + } + } + + /** + * Signal the connection that some stream data is available for sending on one or more streams. + * @param streamIds the stream ids + */ + public void streamDataAvailableForSending(final Set streamIds) { + for (final long id : streamIds) { + streams.enqueueForSending(id); + } + packetSpaces.app.runTransmitter(); + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + streams.notifyTerminalState(streamId, state); + } + + /** + * Called to request sending of a RESET_STREAM frame. + * + * @apiNote + * Should only be called for sending streams. For stopping a + * receiving stream then {@link #scheduleStopSendingFrame(long, long)} should be called. + * This method should only be called from {@code QuicSenderStreamImpl}, after + * switching the state of the stream to RESET_SENT. + * + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert streams.isSendingStream(streamId); + streams.requestResetStream(streamId, errorCode); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a STOP_SENDING frame. + * @apiNote + * Should only be called for receiving streams. For stopping a + * sending stream then {@link #requestResetStream(long, long)} + * should be called. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param errorCode the application error code + */ + public void scheduleStopSendingFrame(long streamId, long errorCode) { + assert streams.isReceivingStream(streamId); + streams.scheduleStopSendingFrame(new StopSendingFrame(streamId, errorCode)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a MAX_STREAM_DATA frame. + * @apiNote + * Should only be called for receiving streams. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param maxStreamData the new max data we are prepared to receive on + * this stream + */ + public void requestSendMaxStreamData(long streamId, long maxStreamData) { + assert streams.isReceivingStream(streamId); + streams.requestSendMaxStreamData(new MaxStreamDataFrame(streamId, maxStreamData)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called when frame data can be safely added to the amount of + * data received by the connection for MAX_DATA flow control + * purpose. + * @throws QuicTransportException if flow control was exceeded + * @param frameType type of frame received + */ + public void increaseReceivedData(long diff, long frameType) throws QuicTransportException { + oneRttRcvQueue.checkAndIncreaseReceivedData(diff, frameType); + } + + /** + * Called when frame data is removed from the connection + * and the amount of data can be added to MAX_DATA window. + * @param diff amount of data processed + */ + public void increaseProcessedData(long diff) { + oneRttRcvQueue.increaseProcessedData(diff); + } + + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + /** + * {@return the computed PTO for the current packet number space, + * adjusted by our max ack delay} + */ + public long peerPtoMs() { + return rttEstimator.getBasePtoDuration().toMillis() + + (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT ? + PacketSpaceManager.ADVERTISED_MAX_ACK_DELAY : 0); + } + + public void runAppPacketSpaceTransmitter() { + this.packetSpaces.app.runTransmitter(); + } + + public void shutdown() { + packetSpaces.close(); + } + + public final String logTag() { + return logTag; + } + + /* ======================================================== + * Direct Byte Buffer Pool + * ======================================================== */ + + // Maximum size of the connection's Direct ByteBuffer Pool. + // For a connection configured to attempt sending datagrams in thread + // (QuicEndpoint.SEND_DGRAM_ASYNC == false), 2 should be enough, as we + // shouldn't have more than 2 packet number spaces active at the same time. + private static final int MAX_DBB_POOL_SIZE = 3; + // The ByteBuffer pool, which contains available byte buffers + private final ConcurrentLinkedQueue bbPool = new ConcurrentLinkedQueue<>(); + // The number of Direct Byte Buffers allocated for sending and managed by the pool. + // This is the number of Direct Byte Buffers currently in flight, plus the number + // of available byte buffers present in the pool. It will never exceed + // MAX_DBB_POOL_SIZE. + private final AtomicInteger bbAllocated = new AtomicInteger(); + + // Some counters used for printing debug statistics when Log quic:dbb is enabled + // Byte Buffers in flight: the number of byte buffers that were returned by + // getOutgoingByteBuffer() minus the number of byte buffers that were released + // through datagramReleased() + private final AtomicInteger bbInFlight = new AtomicInteger(); + // Peak number of byte buffers in flight. Never decreases. + private final AtomicInteger bbPeak = new AtomicInteger(); + // Number of unreleased byte buffers. This should eventually reach 0. + final AtomicInteger bbUnreleased = new AtomicInteger(); + + /** + * {@return a new {@code ByteBuffer} to encode and encrypt packets in a datagram} + * This method may either allocate a new heap BteBuffer or return a (possibly + * new) Direct ByteBuffer from the connection's Direct Byte Buffer Pool. + * @param size the maximum size of the datagram + */ + protected ByteBuffer getOutgoingByteBuffer(int size) { + bbUnreleased.incrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + if (size <= getMaxDatagramSize()) { + ByteBuffer buffer = bbPool.poll(); + if (buffer != null) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: got direct buffer from pool" + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return buffer; + } + bbAllocated.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer"); + } + buffer = null; + } + + assert buffer == null; + int allocated; + while ((allocated = bbAllocated.get()) < MAX_DBB_POOL_SIZE) { + if (bbAllocated.compareAndSet(allocated, allocated + 1)) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: allocating direct buffer #" + (allocated + 1) + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocateDirect(getMaxDatagramSize()); + } + } + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: too many buffers allocated: " + allocated + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: wrong size " + size); + } + } + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocate(size); + } + @Override + public void datagramSent(QuicDatagram datagram) { + datagramReleased(datagram); + } + + @Override + public void datagramDiscarded(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram discarded " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + public void datagramDropped(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram dropped " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + /** + * Returns a {@link jdk.internal.net.http.quic.QuicEndpoint.Datagram} which contains + * an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

      + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame or + * an {@linkplain Optional#empty() empty Optional} if the datagram couldn't + * be constructed. + */ + final Optional connectionCloseDatagram() { + try { + final ByteBuffer quicPktPayload = this.terminator.makeConnectionCloseDatagram(); + return Optional.of(new QuicDatagram(this, peerAddress, quicPktPayload)); + } catch (Exception e) { + // ignore any exception because providing the connection close datagram + // when the endpoint is being closed, is on best-effort basis + return Optional.empty(); + } + } + + /** + * Called when a datagram is being released, either from + * {@link #datagramSent(QuicDatagram)}, {@link #datagramDiscarded(QuicDatagram)}, + * or {@link #datagramDropped(QuicDatagram)}. + * This method may either release the datagram and let it get garbage collected, + * or return it to the pool. + * @param datagram the released datagram + */ + protected void datagramReleased(QuicDatagram datagram) { + bbUnreleased.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram released " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + bbInFlight.decrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + ByteBuffer buffer = datagram.payload(); + buffer.clear(); + if (buffer.isDirect()) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: offering buffer to pool"); + } + bbPool.offer(buffer); + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer (too small)"); + } + bbAllocated.decrementAndGet(); + } + } + } + } + + public String loggableState() { + // for HTTP3 debugging + // If the connection was active (open bidi streams), log connection state + if (streams.quicStreams().noneMatch(QuicStream::isBidirectional)) { + // no active requests + return "No active requests"; + } + Deadline now = TimeSource.now(); + StringBuilder result = new StringBuilder("sending: {canSend:" + oneRttSndQueue.canSend() + + ", credit: " + oneRttSndQueue.credit() + + ", sendersReady: " + streams.hasAvailableData() + + ", hasControlFrames: " + streams.hasControlFrames() + + "}, cc: { backoff: " + rttEstimator.getPtoBackoff() + + ", duration: " + ((PacketSpaceManager) packetSpaces.app).getPtoDuration() + + ", current deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).deadline()) + + ", prospective deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).prospectiveDeadline()) + + "}, streams: ["); + streams.quicStreams().filter(QuicStream::isBidirectional).forEach( + s -> { + QuicBidiStreamImpl qb = (QuicBidiStreamImpl) s; + result.append("{id:" + s.streamId() + + ", available: " + qb.senderPart().available() + + ", blocked: " + qb.senderPart().isBlocked() + "}," + ); + } + ); + result.append("]"); + return result.toString(); + } + + /** + * {@return true if the packet contains a CONNECTION_CLOSE frame, false otherwise} + * @param packet the QUIC packet + */ + private static boolean containsConnectionClose(final QuicPacket packet) { + for (final QuicFrame frame : packet.frames()) { + if (frame instanceof ConnectionCloseFrame) { + return true; + } + } + return false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java new file mode 100644 index 00000000000..3df9b599f82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java @@ -0,0 +1,2062 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicSelector.QuicNioSelector; +import jdk.internal.net.http.quic.QuicSelector.QuicVirtualThreadPoller; +import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.util.OperatingSystem; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; + +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.BLOCKING_WITH_VIRTUAL_THREADS; +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.NON_BLOCKING_WITH_SELECTOR; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + + +/** + * A QUIC Endpoint. A QUIC endpoint encapsulate a DatagramChannel + * and is registered with a Selector. It subscribes for read and + * write events from the selector, and implements a readLoop and + * a writeLoop. + *

      + * The read event or write event are triggered by the selector + * thread. When the read event is triggered, all available datagrams + * are read from the channel and pushed into a read queue. + * Then the readLoop is triggered. + * When the write event is triggered, the key interestOps are + * modified to pause write events, and the writeLoop is triggered. + *

      + * The readLoop and writeLoop should never execute on the selector + * thread, but rather, in the client's executor. + *

      + * When the writeLoop is triggered, it polls the writeQueue and + * writes as many datagram as it can to the channel. At the end, + * if there still remains some datagrams in the writeQueue, the + * write event is resumed. Otherwise, the writeLoop is next + * triggered when new datagrams are added to the writeQueue. + *

      + * When the readLoop is triggered, it polls the read queue + * and attempts to match each received packet with a + * QuicConnection. If no connection matches, it attempts + * to match the packet with stateless reset tokens. + * If no stateless reset token match, the packet is + * discarded. + *

      + */ +public abstract sealed class QuicEndpoint implements AutoCloseable + permits QuicEndpoint.QuicSelectableEndpoint, QuicEndpoint.QuicVirtualThreadedEndpoint { + + private static final int INCOMING_MAX_DATAGRAM; + static final boolean DGRAM_SEND_ASYNC; + static final int MAX_BUFFERED_HIGH; + static final int MAX_BUFFERED_LOW; + static { + // This default value is the maximum payload size of + // an IPv6 datagram, which is 65527 (which is bigger + // than that of an IPv4). + // We have only one direct buffer of this size per endpoint. + final int defSize = 65527; + // This is the value that will be transmitted to the server in the + // max_udp_payload_size parameter + int size = Utils.getIntegerProperty("jdk.httpclient.quic.maxUdpPayloadSize", defSize); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2. + if (size < 1200 || size > 65527) { + // fallback to default size + size = defSize; + } + INCOMING_MAX_DATAGRAM = size; + // TODO: evaluate pros and cons WRT performance and decide for one or the other + // before GA. + DGRAM_SEND_ASYNC = Utils.getBooleanProperty("jdk.internal.httpclient.quic.sendAsync", false); + int maxBufferHigh = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedHigh", + 512 << 10), 128 << 10, 6 << 20); + int maxBufferLow = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedLow", + 384 << 10), 64 << 10, 6 << 20); + if (maxBufferLow >= maxBufferHigh) maxBufferLow = maxBufferHigh >> 1; + MAX_BUFFERED_HIGH = maxBufferHigh; + MAX_BUFFERED_LOW = maxBufferLow; + } + + /** + * This interface represent a UDP Datagram. This could be + * either an incoming datagram or an outgoing datagram. + */ + public sealed interface Datagram + permits QuicDatagram, StatelessReset, SendStatelessReset, UnmatchedDatagram { + /** + * {@return the peer address} + * For incoming datagrams, this is the sender address. + * For outgoing datagrams, this is the destination address. + */ + SocketAddress address(); + + /** + * {@return the datagram payload} + */ + ByteBuffer payload(); + } + + /** + * An incoming UDP Datagram for which no connection was found. + * On the server side it may represent a new connection attempt. + * @param address the {@linkplain Datagram#address() sender address} + * @param payload {@inheritDoc} + */ + public record UnmatchedDatagram(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * A stateless reset that should be sent in response + * to an incoming datagram targeted at a deleted connection. + * @param address the {@linkplain Datagram#address() destination address} + * @param payload the outgoing stateless reset + */ + public record SendStatelessReset(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An incoming datagram containing a stateless reset + * @param connection the connection to reset + * @param address the {@linkplain Datagram#address() sender address} + * @param payload the datagram payload + */ + public record StatelessReset(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An outgoing datagram, or an incoming datagram for which + * a connection was identified. + * @param connection the sending or receiving connection + * @param address {@inheritDoc} + * @param payload {@inheritDoc} + */ + public record QuicDatagram(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) + implements Datagram {} + + /** + * An enum identifying the type of channels used and supported by QuicEndpoint and + * QuicSelector + */ + public enum ChannelType { + NON_BLOCKING_WITH_SELECTOR, + BLOCKING_WITH_VIRTUAL_THREADS; + public boolean isBlocking() { + return this == BLOCKING_WITH_VIRTUAL_THREADS; + } + } + + // A temporary internal property to switch between two QuicSelector implementation: + // - if true, uses QuicNioSelector, an implementation based non-blocking and channels + // and an NIO Selector + // - if false, uses QuicVirtualThreadPoller, an implementation that use Virtual Threads + // to poll blocking channels + // On windows, we default to using non-blocking IO with a Selector in order + // to avoid a potential deadlock in WEPoll (see JDK-8334574). + private static final boolean USE_NIO_SELECTOR = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.useNioSelector", + OperatingSystem.isWindows()); + /** + * The configured channel type + */ + public static final ChannelType CONFIGURED_CHANNEL_TYPE = USE_NIO_SELECTOR + ? NON_BLOCKING_WITH_SELECTOR + : BLOCKING_WITH_VIRTUAL_THREADS; + + final Logger debug = Utils.getDebugLogger(this::name); + private final QuicInstance quicInstance; + private final String name; + private final DatagramChannel channel; + private final ByteBuffer receiveBuffer; + final Executor executor; + final ConcurrentLinkedQueue readQueue = new ConcurrentLinkedQueue<>(); + final ConcurrentLinkedQueue writeQueue = new ConcurrentLinkedQueue<>(); + final QuicTimerQueue timerQueue; + private volatile boolean closed; + + // A synchronous scheduler to consume the readQueue list; + final SequentialScheduler readLoopScheduler = + SequentialScheduler.lockingScheduler(this::readLoop); + + // A synchronous scheduler to consume the writeQueue list; + final SequentialScheduler writeLoopScheduler = + SequentialScheduler.lockingScheduler(this::writeLoop); + + // A ConcurrentMap to store registered connections. + // The connection IDs might come from external sources. They implement Comparable + // to mitigate collision attacks. + // This map must not share the idFactory with other maps, + // see RFC 9000 section 21.11. Stateless Reset Oracle + private final ConcurrentMap connections = + new ConcurrentHashMap<>(); + + // a factory of local connection IDs. + private final QuicConnectionIdFactory idFactory; + + // Key used to encrypt tokens before storing in {@link #peerIssuedResetTokens} + private final Key tokenEncryptionKey; + + // keeps a link of the peer issued stateless reset token to the corresponding connection that + // will be closed if the specific stateless reset token is received + private final ConcurrentMap peerIssuedResetTokens = + new ConcurrentHashMap<>(); + + private static ByteBuffer allocateReceiveBuffer() { + return ByteBuffer.allocateDirect(INCOMING_MAX_DATAGRAM); + } + + private final AtomicInteger buffered = new AtomicInteger(); + volatile boolean readingStalled; + + public QuicConnectionIdFactory idFactory() { + return idFactory; + } + + public int buffer(int bytes) { + return buffered.addAndGet(bytes); + } + + public int unbuffer(int bytes) { + var newval = buffered.addAndGet(-bytes); + assert newval >= 0; + if (newval <= MAX_BUFFERED_LOW) { + resumeReading(); + } + return newval; + } + + boolean bufferTooBig() { + return buffered.get() >= MAX_BUFFERED_HIGH; + } + + public int buffered() { + return buffered.get(); + } + + boolean readingPaused() { + return readingStalled; + } + + abstract void resumeReading(); + + abstract void pauseReading(); + + private QuicEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + this.quicInstance = quicInstance; + this.name = name; + this.channel = channel; + this.receiveBuffer = allocateReceiveBuffer(); + this.executor = quicInstance.executor(); + this.timerQueue = timerQueue; + if (debug.on()) debug.log("created for %s", channel); + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + tokenEncryptionKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("AES key generator not available", e); + } + idFactory = isServer() + ? QuicConnectionIdFactory.getServer() + : QuicConnectionIdFactory.getClient(); + } + + public String name() { + return name; + } + + public DatagramChannel channel() { + return channel; + } + + Executor writeLoopExecutor() { return executor; } + + public SocketAddress getLocalAddress() throws IOException { + return channel.getLocalAddress(); + } + + public String getLocalAddressString() { + try { + return String.valueOf(channel.getLocalAddress()); + } catch (IOException io) { + return "No address available"; + } + } + + int getMaxUdpPayloadSize() { + return INCOMING_MAX_DATAGRAM; + } + + protected abstract ChannelType channelType(); + + /** + * A {@link QuicEndpoint} implementation based on non blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to a {@link QuicNioSelector}. + */ + static final class QuicSelectableEndpoint extends QuicEndpoint { + volatile SelectionKey key; + + private QuicSelectableEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + assert !channel.isBlocking() : "SelectableQuicEndpoint channel is blocking"; + } + + @Override + public ChannelType channelType() { + return NON_BLOCKING_WITH_SELECTOR; + } + + /** + * Attaches this endpoint to a selector. + * + * @param selector the selector to attach to + * @throws ClosedChannelException if the channel is already closed + */ + public void attach(Selector selector) throws ClosedChannelException { + var key = this.key; + assert key == null; + // this block is needed to coordinate with detach() and + // selected(). See comment in selected(). + synchronized (this) { + this.key = super.channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); + } + } + + @Override + void resumeReading() { + boolean resumed = false; + SelectionKey key; + synchronized (this) { + key = this.key; + if (key != null && key.isValid()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops | SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsOr(SelectionKey.OP_READ); + readingStalled = false; + resumed = true; + } + } + } + if (resumed) { + // System.out.println(this + " endpoint resumed reading"); + if (debug.on()) debug.log("endpoint resumed reading"); + key.selector().wakeup(); + } + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (readingStalled) return; + if (key != null && key.isValid() && bufferTooBig()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops & ~SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsAnd(~SelectionKey.OP_READ); + readingStalled = true; + paused = true; + } + } + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + /** + * Invoked by the {@link QuicSelector} when this endpoint's channel + * is selected. + * + * @param readyOps The operations that are ready for this endpoint. + */ + public void selected(int readyOps) { + var key = this.key; + try { + if (key == null) { + // null keys have been observed here. + // key can only be null if it's been cancelled, by detach() + // or if the call to channel::register hasn't returned yet + // the synchronized block below will block until + // channel::register returns if needed. + // This can only happen once, when attaching the channel, + // so there should be no performance issue in synchronizing + // here. + synchronized (this) { + key = this.key; + } + } + + if (key == null) { + if (debug.on()) { + debug.log("key is null"); + if (QuicEndpoint.class.desiredAssertionStatus()) { + Thread.dumpStack(); + } + } + return; + } + + if (debug.on()) { + debug.log("selected(interest=%s, ready=%s)", + Utils.interestOps(key), + Utils.readyOps(key)); + } + + int interestOps = key.interestOps(); + + // Some operations may be ready even when we are not interested. + // Typically, a channel may be ready for writing even if we have + // nothing to write. The events we need to invoke are therefore + // at the intersection of the ready set with the interest set. + int event = readyOps & interestOps; + if ((event & SelectionKey.OP_READ) == SelectionKey.OP_READ) { + onReadEvent(); + if (isClosed()) { + key.interestOpsAnd(~SelectionKey.OP_READ); + } + } + if ((event & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { + onWriteEvent(); + } + if (debug.on()) { + debug.log("interestOps: %s", Utils.interestOps(key)); + } + } finally { + if (!channel().isOpen()) { + if (key != null) key.cancel(); + close(); + } + } + } + + private void onReadEvent() { + var key = this.key; + try { + if (debug.on()) debug.log("onReadEvent"); + channelReadLoop(); + } finally { + if (debug.on()) { + debug.log("Leaving readEvent: ops=%s", Utils.interestOps(key)); + } + } + } + + private void onWriteEvent() { + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + // Do we need a pub/sub mechanism here? + // The write event will be paused/resumed by the + // writeLoop if needed + if (debug.on()) debug.log("onWriteEvent"); + var key = this.key; + if (key != null && key.isValid()) { + int previous; + synchronized (this) { + previous = key.interestOpsAnd(~SelectionKey.OP_WRITE); + } + if (debug.on()) debug.log("key changed from %s to: %s", + Utils.describeOps(previous), Utils.interestOps(key)); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + if (debug.on() && key != null) { + debug.log("Leaving writeEvent: ops=%s", Utils.interestOps(key)); + } + } + + @Override + void writeLoop() { + super.writeLoop(); + // update selection key if needed + var key = this.key; + try { + if (key != null && key.isValid()) { + int ops, newops; + synchronized (this) { + ops = newops = key.interestOps(); + if (writeQueue.isEmpty()) { + // we have nothing else to write for now + newops &= ~SelectionKey.OP_WRITE; + } else { + // there's more to write + newops |= SelectionKey.OP_WRITE; + } + if (newops != ops && key.selector().isOpen()) { + key.interestOps(newops); + key.selector().wakeup(); + } + } + if (debug.on()) { + debug.log("leaving writeLoop: ops=%s", Utils.describeOps(newops)); + } + } + } catch (CancelledKeyException x) { + if (debug.on()) debug.log("key cancelled"); + if (writeQueue.isEmpty()) return; + else { + closeWriteQueue(x); + } + } + } + + @Override + void readLoop() { + try { + super.readLoop(); + } finally { + if (debug.on()) { + debug.log("leaving readLoop: ops=%s", Utils.interestOps(key)); + } + } + } + + @Override + public void detach() { + var key = this.key; + if (key == null) return; + if (debug.on()) { + debug.log("cancelling key: " + key); + } + // this block is needed to coordinate with attach() and + // selected(). See comment in selected(). + synchronized (this) { + key.cancel(); + this.key = null; + } + } + } + + /** + * A {@link QuicEndpoint} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channel. + * This implementation is tied to a {@link QuicVirtualThreadPoller}. + */ + static final class QuicVirtualThreadedEndpoint extends QuicEndpoint { + Future key; + volatile QuicVirtualThreadPoller poller; + boolean readingDone; + + private QuicVirtualThreadedEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + } + + @Override + boolean readingPaused() { + synchronized (this) { + return readingDone = super.readingPaused(); + } + } + + @Override + void resumeReading() { + boolean resumed; + boolean resumedInOtherThread = false; + QuicVirtualThreadPoller poller; + synchronized (this) { + resumed = readingStalled; + readingStalled = false; + poller = this.poller; + // readingDone is false here, it means reading already resumed + // no need to start a new reading thread + if (poller != null && (resumedInOtherThread = readingDone)) { + readingDone = false; + attach(poller); + } + } + if (resumedInOtherThread) { + // last time readingPaused() was called it returned true, so we know + // the previous poller thread has stopped reading and will exit. + // We attached a new poller above, so reading will resume in that + // other thread + // System.out.println(this + " endpoint resumed reading in new virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in new virtual thread"); + } else if (resumed) { + // readingStalled was true, and readingDone was false - which means some + // poller thread is already active, and will find readingStalled == true + // and will continue reading. So reading will resume in the currently + // active poller thread + // System.out.println(this + " endpoint resumed reading in same virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in same virtual thread"); + } // if readingStalled was false and readingDone was false there is nothing to do. + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (bufferTooBig()) paused = readingStalled = true; + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + @Override + public ChannelType channelType() { + return BLOCKING_WITH_VIRTUAL_THREADS; + } + + void attach(QuicVirtualThreadPoller poller) { + this.poller = poller; + var future = poller.startReading(this); + synchronized (this) { + this.key = future; + } + } + + Executor writeLoopExecutor() { + QuicVirtualThreadPoller poller = this.poller; + if (poller == null) return executor; + return poller.readLoopExecutor(); + } + + private final SequentialScheduler channelScheduler = SequentialScheduler.lockingScheduler(this::channelReadLoop0); + + @Override + void channelReadLoop() { + channelScheduler.runOrSchedule(); + } + + private void channelReadLoop0() { + super.channelReadLoop(); + } + + @Override + public void detach() { + var key = this.key; + try { + if (key != null) { + // do not interrupt the reading task if running: + // closing the channel later on will ensure that the + // task eventually terminates. + key.cancel(false); + } + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to cancel future: " + e); + } + } + } + } + + private ByteBuffer copyOnHeap(ByteBuffer buffer) { + ByteBuffer onHeap = ByteBuffer.allocate(buffer.remaining()); + return onHeap.put(buffer).flip(); + } + + void channelReadLoop() { + // we can't prevent incoming datagram from being received + // at this level of the stack. If there is a datagram available, + // we must read it immediately and put it in the read queue. + // + // We maintain a counter of the number of bytes currently + // in the read queue. If that number exceeds a high watermark + // threshold, we will pause reading, and thus stop adding + // to the queue. + // + // As the read queue gets emptied, reading will be resumed + // when a low watermark threshold is crossed in the other + // direction. + // + // At the moment we have a single channel per endpoint, + // and we're using a single endpoint by default. + // + // We have a single selector thread, and we copy off + // the data from off-heap to on-heap before adding it + // to the queue. + // + // We can therefore do the reading directly in the + // selector thread and offload the parsing (the readLoop) + // to the executor. + // + // The readLoop will in turn resume the reading, if needed, + // when it crosses the low watermark threshold. + // + boolean nonBlocking = channelType() == NON_BLOCKING_WITH_SELECTOR; + int count; + final var buffer = this.receiveBuffer; + buffer.clear(); + final int initialStart = 1; // start readloop at first buffer + // if blocking we want to nudge the scheduler after each read since we don't + // know how much the next receive will take. If non-blocking, we nudge it + // after three consecutive read. + final int maxBeforeStart = nonBlocking ? 3 : 1; // nudge again after 3 buffers + int readLoopStarted = initialStart; + int totalpkt = 0; + try { + int sincepkt = 0; + while (!isClosed() && !readingPaused()) { + var pos = buffer.position(); + var limit = buffer.limit(); + if (debug.on()) + debug.log("receiving with buffer(pos=%s, limit=%s)", pos, limit); + assert pos == 0; + assert limit > pos; + + final SocketAddress source = channel.receive(buffer); + assert source != null || !channel.isBlocking(); + if (source == null) { + if (debug.on()) debug.log("nothing to read..."); + if (nonBlocking) break; + } + + totalpkt++; + sincepkt++; + buffer.flip(); + count = buffer.remaining(); + if (debug.on()) { + debug.log("received %s bytes from %s", count, source); + } + if (count > 0) { + // Optimization: add some basic check here to drop the packet here if: + // - it is too small, it is not a quic packet we would handle + Datagram datagram; + if ((datagram = matchDatagram(source, buffer)) == null) { + if (debug.on()) { + debug.log("dropping invalid packet for this instance (%s bytes)", count); + } + buffer.clear(); + continue; + } + // at this point buffer has been copied. We only buffer what's + // needed. + int rcv = datagram.payload().remaining(); + int buffered = buffer(rcv); + if (debug.on()) { + debug.log("adding %s in read queue from %s, queue size %s, buffered %s, type %s", + rcv, source, readQueue.size(), buffered, datagram.getClass().getSimpleName()); + } + readQueue.add(datagram); + buffer.clear(); + if (--readLoopStarted == 0 || buffered >= MAX_BUFFERED_HIGH) { + readLoopStarted = maxBeforeStart; + if (debug.on()) debug.log("triggering readLoop"); + readLoopScheduler.runOrSchedule(executor); + Deadline now; + Deadline pending; + if (nonBlocking && totalpkt > 1 && (pending = timerQueue.pendingScheduledDeadline()) + .isBefore(now = timeSource().instant())) { + // we have read 3 packets, some events are pending, return + // to the selector to process the event queue + assert this instanceof QuicEndpoint.QuicSelectableEndpoint + : "unexpected endpoint type: " + this.getClass() + "@[" + name + "]"; + assert Thread.currentThread() instanceof QuicSelector.QuicSelectorThread; + if (Log.quicRetransmit() || Log.quicTimer()) { + Log.logQuic(name() + ": reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } else if (debug.on()) { + debug.log("reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } + timerQueue.processEventsAndReturnNextDeadline(now, executor); + sincepkt = 0; + } + } + // check buffered.get() directly as it may have + // been decremented by the read loop already + if (this.buffered.get() >= MAX_BUFFERED_HIGH) { + // we passed the high watermark, let's pause reading. + // the read loop should already have been kicked + // of above, or will be below when we exit the while + // loop + pauseReading(); + } + } else { + if (debug.on()) debug.log("Dropped empty datagram"); + } + } + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + if (!readQueue.isEmpty() && readLoopStarted != maxBeforeStart) { + if (debug.on()) debug.log("triggering readLoop: queue size " + readQueue.size()); + readLoopScheduler.runOrSchedule(executor); + } + } catch (Throwable t) { + // TODO: special handling for interrupts? + onReadError(t); + } finally { + if (nonBlocking) { + if ((Log.quicRetransmit() && Log.channel()) || Log.quicTimer()) { + Log.logQuic(name() + ": channelReadLoop totalpkt:" + totalpkt); + } else if (debug.on()) { + debug.log("channelReadLoop totalpkt:" + totalpkt); + } + } + } + } + + /** + * This method tries to figure out whether the received packet + * matches a connection, or a stateless reset. + * @param source the source address + * @param buffer the incoming datagram payload + * @return a {@link Datagram} to be processed by the read loop + * if a match is found, or null if the datagram can be dropped + * immediately + */ + private Datagram matchDatagram(SocketAddress source, ByteBuffer buffer) { + HeadersType headersType = QuicPacketDecoder.peekHeaderType(buffer, buffer.position()); + // short header packets whose length is < 21 are never valid + if (headersType == HeadersType.SHORT && buffer.remaining() < 21) { + return null; + } + final ByteBuffer cidbytes = switch (headersType) { + case LONG, SHORT -> peekConnectionBytes(headersType, buffer); + default -> null; + }; + if (cidbytes == null) { + return null; + } + int length = cidbytes.remaining(); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + return null; + } + if (debug.on()) { + debug.log("headers(%s), connectionId(%d), datagram(%d)", + headersType, cidbytes.remaining(), buffer.remaining()); + } + QuicPacketReceiver connection = findQuicConnectionFor(source, cidbytes, headersType == HeadersType.LONG); + // check stateless reset + if (connection == null) { + if (headersType == HeadersType.SHORT) { + // a short packet may be a stateless reset, or may + // trigger a stateless reset + connection = checkStatelessReset(source, buffer); + if (connection != null) { + // We received a stateless reset, process it later in the readLoop + return new StatelessReset(connection, source, copyOnHeap(buffer)); + } else if (buffer.remaining() > 21) { + // check if we should send a stateless reset + final ByteBuffer reset = idFactory.statelessReset(cidbytes, buffer.remaining() - 1); + if (reset != null) { + // will send stateless reset later from the read loop + return new SendStatelessReset(source, reset); + } + } + return null; // drop unmatched short packets + } + // client can drop all unmatched long quic packets here + if (isClient()) return null; + } + + if (connection != null) { + if (!connection.accepts(source)) return null; + return new QuicDatagram(connection, source, copyOnHeap(buffer)); + } else { + return new UnmatchedDatagram(source, copyOnHeap(buffer)); + } + } + + + private int send(ByteBuffer datagram, SocketAddress destination) throws IOException { + return channel.send(datagram, destination); + } + + void writeLoop() { + try { + writeLoop0(); + } catch (Throwable error) { + if (!expectExceptions && !closed) { + if (Log.errors()) { + Log.logError(name + ": failed to write to channel: " + error); + Log.logError(error); + } + abort(error); + } + } + } + + boolean sendDatagram(QuicDatagram datagram) throws IOException { + int sent; + var payload = datagram.payload(); + var tosend = payload.remaining(); + final var dest = datagram.address(); + if (isClosed() || isChannelClosed()) { + if (debug.on()) { + debug.log("endpoint or channel closed; skipping sending of datagram(%d) to %s", + tosend, dest); + } + return false; + } + if (debug.on()) { + debug.log("sending datagram(%d) to %s", + tosend, dest); + } + sent = send(payload, dest); + if (debug.on()) debug.log("sent %d bytes to %s", sent, dest); + if (sent == 0 && sent != tosend) return false; + assert sent == tosend; + if (datagram.connection != null) { + datagram.connection.datagramSent(datagram); + } + return true; + } + + void onSendError(QuicDatagram datagram, int tosend, IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + var connection = datagram.connection(); + var dest = datagram.address(); + String msg = x.getMessage(); + if (msg != null && msg.contains("too big")) { + int max = -1; + if (connection instanceof QuicConnectionImpl cimpl) { + max = cimpl.getMaxDatagramSize(); + } + msg = "on endpoint %s: Failed to send datagram (%s bytes, max: %s) to %s: %s" + .formatted(this.name, tosend, max, dest, x); + if (connection == null && debug.on()) debug.log(msg); + x = new SocketException(msg, x); + } + if (connection != null) { + connection.datagramDiscarded(datagram); + connection.onWriteError(x); + if (!channel.isOpen()) { + abort(x); + } + } + } + + private void writeLoop0() { + // write as much as we can + while (!writeQueue.isEmpty()) { + var datagram = writeQueue.peek(); + var payload = datagram.payload(); + var tosend = payload.remaining(); + try { + if (sendDatagram(datagram)) { + var rem = writeQueue.poll(); + assert rem == datagram; + } else break; + } catch (IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + onSendError(datagram, tosend, x); + var rem = writeQueue.poll(); + assert rem == datagram; + } + } + + } + + void closeWriteQueue(Throwable t) { + QuicDatagram qd; + while ((qd = writeQueue.poll()) != null) { + if (qd.connection != null) { + qd.connection.onWriteError(t); + } + } + } + + private ByteBuffer peekConnectionBytes(HeadersType headersType, ByteBuffer payload) { + var cidlen = idFactory.connectionIdLength(); + return switch (headersType) { + case LONG -> QuicPacketDecoder.peekLongConnectionId(payload); + case SHORT -> QuicPacketDecoder.peekShortConnectionId(payload, cidlen); + default -> null; + }; + } + + // The readloop is triggered whenever new datagrams are + // added to the read queue. + void readLoop() { + try { + if (debug.on()) debug.log("readLoop"); + while (!readQueue.isEmpty()) { + var datagram = readQueue.poll(); + var payload = datagram.payload(); + var source = datagram.address(); + int remaining = payload.remaining(); + var pos = payload.position(); + unbuffer(remaining); + if (debug.on()) { + debug.log("readLoop: type(%x) %d from %s", + payload.hasRemaining() ? payload.get(0) : 0, + remaining, + source); + } + try { + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring incoming datagram"); + } + return; + } + switch (datagram) { + case QuicDatagram(var connection, var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + var destConnId = peekConnectionBytes(headersType, payload); + connection.processIncoming(source, destConnId, headersType, payload); + } + case UnmatchedDatagram(var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + unmatchedQuicPacket(datagram, headersType, payload); + } + case StatelessReset(var connection, var _, var _) -> { + if (debug.on()) { + debug.log("Processing stateless reset from %s", source); + } + connection.processStatelessReset(); + } + case SendStatelessReset(var _, var _) -> { + if (debug.on()) { + debug.log("Sending stateless reset to %s", source); + } + send(payload, source); + } + } + + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to handle datagram: " + t, t); + Log.logError(t); + } + } + } catch (Throwable t) { + onReadError(t); + } + } + + private void onReadError(Throwable t) { + if (!expectExceptions) { + if (debug.on()) { + debug.log("Error handling event: ", t); + } + Log.logError(t); + if (t instanceof RejectedExecutionException + || t instanceof ClosedChannelException + || t instanceof AssertionError) { + expectExceptions = true; + abort(t); + } + } + } + + /** + * checks if the received datagram contains a stateless reset token; + * returns the associated connection if true, null otherwise + * @param source the sender's address + * @param buffer datagram contents + * @return connection associated with the stateless token, or {@code null} + */ + protected QuicPacketReceiver checkStatelessReset(SocketAddress source, final ByteBuffer buffer) { + // We couldn't identify the connection: maybe that's a stateless reset? + if (closed) return null; + if (debug.on()) { + debug.log("Check if received datagram could be stateless reset (datagram[%d, %s])", + buffer.remaining(), source); + } + if (buffer.remaining() < 21) { + // too short to be a stateless reset: + // RFC 9000: + // Endpoints MUST discard packets that are too small to be valid QUIC packets. + // To give an example, with the set of AEAD functions defined in [QUIC-TLS], + // short header packets that are smaller than 21 bytes are never valid. + if (debug.on()) { + debug.log("Packet too short for a stateless reset (%s bytes < 21)", + buffer.remaining()); + } + return null; + } + final byte[] tokenBytes = new byte[16]; + buffer.get(buffer.limit() - 16, tokenBytes); + final var token = makeToken(tokenBytes); + QuicPacketReceiver connection = peerIssuedResetTokens.get(token); + if (closed) return null; + if (connection != null) { + if (debug.on()) { + debug.log("Received reset token: %s for connection: %s", + HexFormat.of().formatHex(tokenBytes), connection); + } + } else { + if (debug.on()) { + debug.log("Not a stateless reset"); + } + } + return connection; + } + + private StatelessResetToken makeToken(byte[] tokenBytes) { + // encrypt token to block timing attacks, see RFC 9000 section 10.3.1 + try { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, tokenEncryptionKey); + byte[] encryptedBytes = cipher.doFinal(tokenBytes); + return new StatelessResetToken(encryptedBytes); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | + IllegalBlockSizeException | BadPaddingException | + InvalidKeyException e) { + throw new RuntimeException("AES encryption failed", e); + } + } + + /** + * Called when parsing a quic packet that couldn't be matched to any registered + * connection. + * + * @param datagram The datagram containing the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the unmatched quic packet. + * The buffer may contain more coalesced quic packets. + */ + protected void unmatchedQuicPacket(Datagram datagram, + HeadersType headersType, + ByteBuffer buffer) throws IOException { + QuicInstance instance = quicInstance; + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring unmatched datagram"); + } + return; + } + + var address = datagram.address(); + if (isServer() && headersType == HeadersType.LONG ) { + // long packets need to be rematched here for servers. + // we read packets in one thread and process them here in + // a different thread: + // the connection may have been added later on when processing + // a previous long packet in this thread, so we need to + // check the connection map again here. + var idbytes = peekConnectionBytes(headersType, buffer); + var connection = findQuicConnectionFor(address, idbytes, true); + if (connection != null) { + // a matching connection was found, this packet is no longer + // unmatched + if (connection.accepts(address)) { + connection.processIncoming(address, idbytes, headersType, buffer); + } + return; + } + } + + if (debug.on()) { + debug.log("Unmatched packet in datagram [%s, %d, %s] for %s", headersType, + buffer.remaining(), address, instance); + debug.log("Unmatched packet: delegating to instance"); + } + instance.unmatchedQuicPacket(address, headersType, buffer); + } + + private boolean isServer() { + return !isClient(); + } + + private boolean isClient() { + return quicInstance instanceof QuicClient; + } + + // Parses the list of active connection + // Attempts to find one that matches + // If none match return null + // Revisit: + // if we had an efficient sorted tree where we could locate a connection id + // from the idbytes we wouldn't need to use an "unsafe connection id" + // quick and dirty solution for now: we use a ConcurrentHashMap and construct + // a throw away QuicConnectionId that wrap our mutable idbytes. + // This is OK since the classes that may see these bytes are all internal + // and won't mutate them. + QuicPacketReceiver findQuicConnectionFor(SocketAddress peerAddress, ByteBuffer idbytes, boolean longHeaders) { + if (idbytes == null) return null; + var cid = idFactory.unsafeConnectionIdFor(idbytes); + if (cid == null) { + if (!longHeaders || isClient()) { + if (debug.on()) { + debug.log("No connection match for: %s", Utils.asHexString(idbytes)); + } + return null; + } + // this is a long headers packet and we're the server; + // the client might still be using the original connection ID + cid = new PeerConnectionId(idbytes, null); + } + if (debug.on()) { + debug.log("Looking up QuicConnection for: %s", cid); + } + var quicConnection = connections.get(cid); + assert quicConnection == null || allConnectionIds(quicConnection).anyMatch(cid::equals); + return quicConnection; + } + + private static Stream allConnectionIds(QuicPacketReceiver quicConnection) { + return Stream.concat(quicConnection.connectionIds().stream(), quicConnection.initialConnectionId().stream()); + } + + /** + * Detach the channel from the selector implementation + */ + public abstract void detach(); + + private void silentTerminateConnection(QuicPacketReceiver c) { + try { + if (c instanceof QuicConnectionImpl connection) { + final TerminationCause st = forSilentTermination("QUIC endpoint closed - no error"); + connection.terminator.terminate(st); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + void abortConnection(QuicPacketReceiver c, Throwable error) { + try { + if (c instanceof QuicConnectionImpl connection) { + connection.terminator.terminate(TerminationCause.forException(error)); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + boolean isClosed() { + return closed; + } + + private void detachAndCloseChannel() throws IOException { + try { + detach(); + } finally { + channel.close(); + } + } + + volatile boolean expectExceptions; + + @Override + public void close() { + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + try { + while (!connections.isEmpty()) { + if (debug.on()) { + debug.log("closing %d connections", connections.size()); + } + final Set connCloseSent = new HashSet<>(); + for (var cid : connections.keySet()) { + // endpoint is closing, so (on a best-effort basis) we send out a datagram + // containing a QUIC packet with a CONNECTION_CLOSE frame to the peer. + // Immediately after that, we silently terminate the connection since + // there's no point maintaining the connection's infrastructure for + // sending (or receiving) additional packets when the endpoint itself + // won't be around for dealing with the packets. + final QuicPacketReceiver rcvr = connections.remove(cid); + if (rcvr instanceof QuicConnectionImpl quicConn) { + final boolean shouldSendConnClose = connCloseSent.add(quicConn); + // send the datagram containing the CONNECTION_CLOSE frame only once + // per connection + if (shouldSendConnClose) { + sendConnectionCloseQuietly(quicConn); + } + } + silentTerminateConnection(rcvr); + } + } + } finally { + try { + // TODO: do we need to wait for something (ACK?) + // before actually stopping all loop and closing the channel? + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + expectExceptions = true; + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + // sends a datagram with a CONNECTION_CLOSE frame for the connection and ignores + // any exceptions that may occur while trying to do so. + private void sendConnectionCloseQuietly(final QuicConnectionImpl quicConn) { + try { + final Optional datagram = quicConn.connectionCloseDatagram(); + if (datagram.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("sending CONNECTION_CLOSE datagram for connection %s", quicConn); + } + send(datagram.get().payload(), datagram.get().address()); + } catch (Exception e) { + // ignore + if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE datagram for" + + " connection %s due to %s", quicConn, e); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable error) { + + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + assert closed; + if (debug.on()) { + debug.log("aborting: " + error); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + try { + while (!connections.isEmpty()) { + if (debug.on()) + debug.log("closing %d connections", connections.size()); + for (var cid : connections.keySet()) { + abortConnection(connections.remove(cid), error); + } + } + } finally { + try { + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + + @Override + public String toString() { + return name; + } + + boolean forceSendAsync() { + return DGRAM_SEND_ASYNC || !writeQueue.isEmpty(); + // TODO remove + // perform all writes in a virtual thread. This should trigger + // JDK-8334574 more frequently. + // || (IS_WINDOWS + // && channelType().isBlocking() + // && !Thread.currentThread().isVirtual()); + } + + /** + * Schedule a datagram for writing to the underlying channel. + * If any datagram is pending the given datagram is appended + * to the list of pending datagrams for writing. + * @param source the source connection + * @param destination the destination address + * @param payload the encrypted datagram + */ + public void pushDatagram(QuicPacketReceiver source, SocketAddress destination, ByteBuffer payload) { + int tosend = payload.remaining(); + if (debug.on()) { + debug.log("attempting to send datagram [%s bytes]", tosend); + } + var datagram = new QuicDatagram(source, destination, payload); + try { + // if DGRAM_SEND_ASYNC is true we don't attempt to send from the current + // thread but push the datagram on the queue and invoke the write loop. + if (forceSendAsync() || !sendDatagram(datagram)) { + if (tosend == payload.remaining()) { + writeQueue.add(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] added to write queue, queue size %s", + tosend, writeQueue.size()); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + } else { + source.datagramDropped(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] dropped: payload partially consumed, remaining %s", + tosend, payload.remaining()); + } + } + } + } catch (IOException io) { + onSendError(datagram, tosend, io); + } + } + + /** + * Called to schedule sending of a datagram that contains a {@code ConnectionCloseFrame}. + * This will replace the {@link QuicConnectionImpl} with a {@link ClosedConnection} that + * will replay the datagram containing the {@code ConnectionCloseFrame} whenever a packet + * for that connection is received. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosingDatagram(QuicConnectionImpl connection, InetSocketAddress destination, ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closing datagram for " + connection.logTag()); + closing(connection, datagram.slice()); + pushDatagram(connection, destination, datagram); + } + + /** + * Called to schedule sending of a datagram that contains a single {@code ConnectionCloseFrame} + * sent in response to a {@code ConnectionClose} frame. + * This will completely remove the connection from the connection map. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosedDatagram(QuicConnectionImpl connection, + InetSocketAddress destination, + ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closed datagram for " + connection.logTag()); + removeConnection(connection); + pushDatagram(connection, destination, datagram); + } + + /** + * This will completely remove the connection from the endpoint. Any subsequent packets + * directed to this connection from a peer, may end up receiving a stateless reset + * from this endpoint. + * + * @param connection the connection to be removed + */ + void removeConnection(final QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection " + connection); + // remove the connection completely + connection.connectionIds().forEach(connections::remove); + assert !connections.containsValue(connection) : connection; + // remove references to this connection from the map which holds the peer issued + // reset tokens + dropPeerIssuedResetTokensFor(connection); + } + + /** + * Add the cid to connection mapping to the endpoint. + * + * @param cid the connection ID to be added + * @param connection the connection that should be mapped to the cid + * @return true if connection ID was added, false otherwise + */ + public boolean addConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + var old = connections.putIfAbsent(cid, connection); + return old == null; + } + + /** + * Remove the cid to connection mapping from the endpoint. + * + * @param cid the connection ID to be removed + * @param connection the connection that is mapped to the cid + * @return true if connection ID was removed, false otherwise + */ + public boolean removeConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection ID " + cid); + return connections.remove(cid, connection); + } + + public final int connectionCount() { + return connections.size(); + } + + // drop peer issued stateless tokes for the given connection + private void dropPeerIssuedResetTokensFor(QuicPacketReceiver connection) { + // remove references to this connection from the map which holds the peer issued + // reset tokens + peerIssuedResetTokens.values().removeIf(conn -> connection == conn); + } + + // remap peer issued stateless token from connection `from` to connection `to` + private void remapPeerIssuedResetToken(QuicPacketReceiver from, QuicPacketReceiver to) { + assert from != null; + assert to != null; + peerIssuedResetTokens.replaceAll((tok, c) -> c == from ? to : c); + } + + public void draining(final QuicPacketReceiver connection) { + // remap the connection to a DrainingConnection + if (closed) return; + connection.connectionIds().forEach((id) -> + connections.compute(id, this::remapDraining)); + assert !connections.containsValue(connection) : connection; + } + + private DrainingConnection remapDraining(QuicConnectionId id, QuicPacketReceiver conn) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + final var draining = closing.toDraining(); + remapPeerIssuedResetToken(closing, draining); + draining.startTimer(); + return draining; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + var draining = new DrainingConnection(conn.connectionIds(), idleTimeout); + // we can ignore stateless reset in the draining state. + remapPeerIssuedResetToken(impl, draining); + draining.startTimer(); + return draining; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to draining + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to draining", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + protected void closing(QuicConnectionImpl connection, ByteBuffer datagram) { + if (closed) return; + ByteBuffer closing = ByteBuffer.allocate(datagram.limit()); + closing.put(datagram.slice()); + closing.flip(); + connection.connectionIds().forEach((id) -> + connections.compute(id, (i, r) -> remapClosing(i, r, closing))); + assert !connections.containsValue(connection) : connection; + } + + private ClosedConnection remapClosing(QuicConnectionId id, QuicPacketReceiver conn, ByteBuffer datagram) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + // we already have a closing datagram, drop the new one + return closing; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to ClosingConnection", id); + var closing = new ClosingConnection(conn.connectionIds(), idleTimeout, datagram); + remapPeerIssuedResetToken(impl, closing); + closing.startTimer(); + return closing; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to closing + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to closing", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + public void registerNewConnection(QuicConnectionImpl quicConnection) throws IOException { + if (closed) throw new ClosedChannelException(); + quicConnection.connectionIds().forEach((id) -> putConnection(id, quicConnection)); + } + + /** + * A peer issues a stateless reset token which it can then send to close the connection. This + * method links the peer issued token against the connection that needs to be closed if/when + * that stateless reset token arrives in the packet. + * + * @param statelessResetToken the peer issued (16 byte) stateless reset token + * @param connection the connection to link the token against + */ + void associateStatelessResetToken(final byte[] statelessResetToken, final QuicPacketReceiver connection) { + Objects.requireNonNull(connection); + Objects.requireNonNull(statelessResetToken); + final int tokenLength = statelessResetToken.length; + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + tokenLength); + } + if (debug.on()) { + debug.log("associating stateless reset token with connection %s", connection); + } + this.peerIssuedResetTokens.put(makeToken(statelessResetToken), connection); + } + + /** + * Discard the stateless reset token that this endpoint might have previously + * {@link #associateStatelessResetToken(byte[], QuicPacketReceiver) associated any connection} + * with + * @param statelessResetToken The stateless reset token + */ + void forgetStatelessResetToken(final byte[] statelessResetToken) { + // just a tiny optimization - we know stateless reset token must be of 16 bytes, if the passed + // value isn't, then no point doing any more work + if (statelessResetToken.length != 16) { + return; + } + this.peerIssuedResetTokens.remove(makeToken(statelessResetToken)); + } + + /** + * {@return the timer queue associated with this endpoint} + */ + public QuicTimerQueue timer() { + return timerQueue; + } + + public boolean isChannelClosed() { + return !channel().isOpen(); + } + + /** + * {@return the time source associated with this endpoint} + * @apiNote + * There is a unique global {@linkplain TimeSource#source()} for the whole + * JVM, but this method can be overridden in tests to define an alternative + * timeline for the test. + */ + protected TimeLine timeSource() { + return TimeSource.source(); + } + + private void putConnection(QuicConnectionId id, QuicConnectionImpl quicConnection) { + // ideally we'd want to use an immutable byte buffer as a key here. + // but we don't have that. So we use the connection id instead. + var old = connections.put(id, quicConnection); + assert old == null : "%s already registered with %s (%s)" + .formatted(old, id, old == quicConnection ? "old == new" : "old != new"); + } + + + /** + * Represent a closing or draining quic connection: if we receive any packet + * for this connection we ignore them (if in draining state) or replay the + * closed packets in decreasing frequency: we reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + sealed abstract class ClosedConnection implements QuicPacketReceiver, QuicTimedEvent + permits QuicEndpoint.ClosingConnection, QuicEndpoint.DrainingConnection { + + // default time we keep the ClosedConnection alive while closing/draining - if + // PTO information is not available (if 0 is passed as idleTimeoutMs when creating + // an instance of this class) + final static long NO_IDLE_TIMEOUT = 2000; + final List localConnectionIds; + final long maxIdleTimeMs; + final long id; + int more = 1; + int waitformore; + volatile Deadline deadline; + volatile Deadline updatedDeadline; + + ClosedConnection(List localConnectionIds, long maxIdleTimeMs) { + this.id = QuicTimerQueue.newEventId(); + this.maxIdleTimeMs = maxIdleTimeMs == 0 ? NO_IDLE_TIMEOUT : maxIdleTimeMs; + this.deadline = Deadline.MAX; + this.updatedDeadline = Deadline.MAX; + this.localConnectionIds = List.copyOf(localConnectionIds); + } + + @Override + public List connectionIds() { + return localConnectionIds; + } + + @Override + public final void processIncoming(SocketAddress source, ByteBuffer destConnId, HeadersType headersType, ByteBuffer buffer) { + Deadline updated = updatedDeadline; + var waitformore = this.waitformore; + // Deadline.MIN will be set in case of write errors + if (updated != Deadline.MIN && waitformore == 0) { + var more = this.more; + this.waitformore = more; + this.more = more = more << 1; + if (more > 16) { + // the server doesn't seem to take into account our + // connection close frame. Just stop responding + updatedDeadline = Deadline.MIN; + } else { + updatedDeadline = updated.plusMillis(maxIdleTimeMs); + } + handleIncoming(source, destConnId, headersType, buffer); + } else { + this.waitformore = waitformore - 1; + dropIncoming(source, destConnId, headersType, buffer); + } + + timer().reschedule(this, updatedDeadline); + } + + protected void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + dropIncoming(source, idbytes, headersType, buffer); + } + + protected abstract void dropIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer); + + @Override + public final void onWriteError(Throwable t) { + if (debug.on()) + debug.log("failed to write close packet", t); + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public final void startTimer() { + deadline = updatedDeadline = timeSource().instant().plusMillis(maxIdleTimeMs); + timer().offer(this); + } + + @Override + public final Deadline deadline() { + return deadline; + } + + @Override + public final Deadline handle() { + removeConnection(this); + // Deadline.MAX means do not reschedule + return updatedDeadline = Deadline.MAX; + } + + @Override + public final Deadline refreshDeadline() { + // Returning Deadline.MIN here will cause handle() to + // be called and will remove this task from the timer queue. + return deadline = updatedDeadline; + } + + @Override + public final long eventId() { + return id; + } + + @Override + public final void processStatelessReset() { + // the peer has sent us a stateless reset: no need to + // replay CloseConnectionFrame. Just remove this connection. + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public void shutdown() { + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + } + + + /** + * Represent a closing quic connection: if we receive any packet for this + * connection we simply replay the packet(s) that contained the + * ConnectionCloseFrame frame. + * Packets are replayed in decreasing frequency. We reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + final class ClosingConnection extends ClosedConnection { + + final ByteBuffer closePacket; + + ClosingConnection(List localConnIdManager, long maxIdleTimeMs, + ByteBuffer closePacket) { + super(localConnIdManager, maxIdleTimeMs); + this.closePacket = Objects.requireNonNull(closePacket); + } + + @Override + public void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + if (isClosed() || isChannelClosed()) { + // don't respond with any more datagrams and instead just drop + // the incoming ones since the channel is closed + dropIncoming(source, idbytes, headersType, buffer); + return; + } + if (debug.on()) { + debug.log("ClosingConnection(%s): sending closed packets", localConnectionIds); + } + pushDatagram(this, source, closePacket.asReadOnlyBuffer()); + } + + @Override + protected void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("ClosingConnection(%s): dropping %s packet", localConnectionIds, headersType); + } + } + + private DrainingConnection toDraining() { + return new DrainingConnection(localConnectionIds, maxIdleTimeMs); + } + } + + /** + * Represent a draining quic connection: if we receive any packet for this + * connection we simply ignore them. + */ + final class DrainingConnection extends ClosedConnection { + + DrainingConnection(List localConnIdManager, long maxIdleTimeMs) { + super(localConnIdManager, maxIdleTimeMs); + } + + @Override + public void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("DrainingConnection(%s): dropping %s packet", + localConnectionIds, headersType); + } + } + + } + + private record StatelessResetToken (byte[] token) { + StatelessResetToken(final byte[] token) { + this.token = token.clone(); + } + @Override + public int hashCode() { + return Arrays.hashCode(token); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof StatelessResetToken other) { + return Arrays.equals(token, other.token); + } + return false; + } + } + + /** + * {@return a new {@link QuicEndpoint} of the given {@code endpointType}} + * @param endpointType the concrete endpoint type, one of {@link QuicSelectableEndpoint + * QuicSelectableEndpoint.class} or {@link QuicVirtualThreadedEndpoint + * QuicVirtualThreadedEndpoint.class}. + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @param the concrete endpoint type, one of {@link QuicSelectableEndpoint} + * or {@link QuicVirtualThreadedEndpoint} + * @throws IOException if an IOException occurs + * @throws IllegalArgumentException if the given endpoint type is not one of + * {@link QuicSelectableEndpoint QuicSelectableEndpoint.class} or + * {@link QuicVirtualThreadedEndpoint QuicVirtualThreadedEndpoint.class} + */ + private static T create(Class endpointType, + QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) throws IOException { + DatagramChannel channel = DatagramChannel.open(); + // avoid dependency on extnet + Optional> df = channel.supportedOptions().stream(). + filter(o -> "IP_DONTFRAGMENT".equals(o.name())).findFirst(); + if (df.isPresent()) { + // TODO on some platforms this doesn't work on dual stack sockets + // see Net#shouldSetBothIPv4AndIPv6Options + @SuppressWarnings("unchecked") + var option = (SocketOption) df.get(); + channel.setOption(option, true); + } + if (QuicSelectableEndpoint.class.isAssignableFrom(endpointType)) { + channel.configureBlocking(false); + } + Consumer logSink = Log.quic() ? Log::logQuic : null; + Utils.configureChannelBuffers(logSink, channel, + quicInstance.getReceiveBufferSize(), quicInstance.getSendBufferSize()); + channel.bind(bindAddress); // could do that on attach instead? + + if (endpointType.isAssignableFrom(QuicSelectableEndpoint.class)) { + return endpointType.cast(new QuicSelectableEndpoint(quicInstance, channel, name, timerQueue)); + } else if (endpointType.isAssignableFrom(QuicVirtualThreadedEndpoint.class)) { + return endpointType.cast(new QuicVirtualThreadedEndpoint(quicInstance, channel, name, timerQueue)); + } else { + throw new IllegalArgumentException(endpointType.getName()); + } + } + + public static final class QuicEndpointFactory { + + public QuicEndpointFactory() { + } + + /** + * {@return a new {@code QuicSelectableEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicSelectableEndpoint createSelectableEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicSelectableEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + + /** + * {@return a new {@code QuicVirtualThreadedEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicVirtualThreadedEndpoint createVirtualThreadedEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicVirtualThreadedEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + } + + /** + * Registers the given endpoint with the given selector. + *

      + * An endpoint of class {@link QuicSelectableEndpoint} is only + * compatible with a selector of type {@link QuicNioSelector}. + * An endpoint of tyoe {@link QuicVirtualThreadedEndpoint} is only + * compatible with a selector of type {@link QuicVirtualThreadPoller}. + *
      + * If the given endpoint implementation is not compatible with + * the given selector implementation an {@link IllegalStateException} + * is thrown. + * + * @param endpoint the endpoint + * @param selector the selector + * @param debug a logger for debugging + * + * @throws IOException if an IOException occurs + * @throws IllegalStateException if the endpoint and selector implementations + * are not compatible + */ + public static void registerWithSelector(QuicEndpoint endpoint, QuicSelector selector, Logger debug) + throws IOException { + if (selector instanceof QuicVirtualThreadPoller poller) { + var loopingEndpoint = (QuicVirtualThreadedEndpoint) endpoint; + poller.register(loopingEndpoint); + } else if (selector instanceof QuicNioSelector selectable) { + var selectableEndpoint = (QuicEndpoint.QuicSelectableEndpoint) endpoint; + selectable.register(selectableEndpoint); + } else { + throw new IllegalStateException("Incompatible selector and endpoint implementations: %s <-> %s" + .formatted(selector.getClass(), endpoint.getClass())); + } + if (debug.on()) debug.log("endpoint registered with selector"); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java new file mode 100644 index 00000000000..a963625d7f5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.HexFormat; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * A {@code QuicInstance} represents a common abstraction which is + * either a {@code QuicClient} or a {@code QuicServer}, or possibly + * both. It defines the subset of public methods that a + * {@code QuicEndpoint} and a {@code QuicSelector} need to operate + * with a quic client, or a quic server; + */ +public interface QuicInstance { + + /** + * The executor used by this quic instance when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + Executor executor(); + + /** + * {@return an endpoint to associate with a connection} + * @throws IOException + */ + QuicEndpoint getEndpoint() throws IOException; + + /** + * This method is called when a quic packet that couldn't be attributed + * to a registered connection is received. + * @param source the source address of the datagram + * @param type the packet type + * @param buffer A buffer positioned at the start of the quic packet + */ + void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer); + + /** + * {@return true if the passed version is available for use on this instance, false otherwise} + */ + boolean isVersionAvailable(QuicVersion quicVersion); + + /** + * {@return the versions that are available for use on this instance} + */ + List getAvailableVersions(); + + /** + * Instance ID used for debugging traces. + * @return A string uniquely identifying this instance. + */ + String instanceId(); + + /** + * Get the QuicTLSContext used by this quic instance. + * @return the QuicTLSContext used by this quic instance. + */ + QuicTLSContext getQuicTLSContext(); + + QuicTransportParameters getTransportParameters(); + + /** + * The {@link SSLParameters} for this Quic instance. + * May be {@code null} if no parameters have been specified. + * + * @implSpec + * The default implementation of this method returns {@code null}. + * + * @return The {@code SSLParameters} for this quic instance or {@code null}. + */ + default SSLParameters getSSLParameters() { return new SSLParameters(); } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_RCVBUF + * UDP receive buffer} size this instance should use} + */ + default int getReceiveBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.receiveBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_SNDBUF + * UDP send buffer} size this instance should use} + */ + default int getSendBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.sendBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return a string describing the given application error code} + * @param errorCode an application error code + * @implSpec By default, this method returns a generic + * string containing the hexadecimal value of the given errorCode. + * Subclasses built for supporting a given application protocol, + * such as HTTP/3, may override this method to return more + * specific names, such as for instance, {@code "H3_REQUEST_CANCELLED"} + * for {@code 0x010c}. + * @apiNote This method is typically used for logging and/or debugging + * purposes, to generate a more user-friendly log message. + */ + default String appErrorToString(long errorCode) { + return "ApplicationError(code=0x" + HexFormat.of().toHexDigits(errorCode) + ")"; + } + + default String name() { + return String.format("%s(%s)", this.getClass().getSimpleName(), instanceId()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java new file mode 100644 index 00000000000..6652b44de3a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Optional; + +/** + * The {@code QuicPacketReceiver} is an abstraction that defines the + * interface between a {@link QuicEndpoint} and a {@link QuicConnection}. + * This defines the minimum set of methods that the endpoint will need + * in order to be able to dispatch a received {@link jdk.internal.net.http.quic.packets.QuicPacket} + * to its destination. This abstraction is typically useful when dealing with + * {@linkplain QuicEndpoint.ClosedConnection + * closed connections, which need to remain alive for a certain time + * after being closed in order to satisfy the requirement of the quic + * protocol (typically for retransmitting the CLOSE_CONNECTION frame + * if needed). + */ +public interface QuicPacketReceiver { + + /** + * {@return a list of local connectionIds for this connection) + */ + List connectionIds(); + + /** + * {@return the initial connection id assigned by the peer} + * On the client side, this is always {@link Optional#empty()}. + * On the server side, it contains the initial connection id + * that was assigned by the client in the first INITIAL packet. + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + */ + default Optional initialConnectionId() { + return Optional.empty(); + } + + /** + * Called when an incoming datagram is received. + *

      + * The buffer is positioned at the start of the datagram to process. + * The buffer may contain more than one QUIC packet. + * + * @param source The peer address, as received from the UDP stack + * @param destConnId Destination connection id bytes included in the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the quic packet, + * not yet decrypted, and possibly containing coalesced + * packets. + */ + void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer); + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + void onWriteError(Throwable t); + + /** + * Called when a stateless reset token is received. + */ + void processStatelessReset(); + + /** + * Called to shut a closed connection down. + * This is the last step when closing a connection, and typically + * only release resources held by all packet spaces. + */ + void shutdown(); + + /** + * Called after a datagram has been written to the socket. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was sent + */ + default void datagramSent(QuicDatagram datagram) { } + + /** + * Called after a datagram has been discarded as a result of + * some error being raised, for instance, when an attempt + * to write it to the socket has failed, or if the encryption + * of a packet in the datagram has failed. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was discarded + */ + default void datagramDiscarded(QuicDatagram datagram) { } + + /** + * Called after a datagram has been dropped. Typically, this + * could happen if the datagram was only partly written, or if + * the connection was closed before the datagram could be sent. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was dropped + */ + default void datagramDropped(QuicDatagram datagram) { } + + /** + * {@return whether this receiver accepts packets from the given source} + * @param source the sender address + */ + default boolean accepts(SocketAddress source) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java new file mode 100644 index 00000000000..fde253740d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implementation of QUIC congestion controller based on RFC 9002. + * This is a QUIC variant of New Reno algorithm. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +class QuicRenoCongestionController implements QuicCongestionController { + // higher of 14720 and 2*maxDatagramSize; we use fixed maxDatagramSize + private static final int INITIAL_WINDOW = Math.max(14720, 2 * QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE); + private static final int MAX_BYTES_IN_FLIGHT = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxBytesInFlight", 1 << 24), + 1 << 14, 1 << 24); + private final TimeLine timeSource; + private final String dbgTag; + private final Lock lock = new ReentrantLock(); + private long congestionWindow = INITIAL_WINDOW; + private int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE; + private int minimumWindow = 2 * maxDatagramSize; + private long bytesInFlight; + // maximum bytes in flight seen since the last congestion event + private long maxBytesInFlight; + private Deadline congestionRecoveryStartTime; + private long ssThresh = Long.MAX_VALUE; + + public QuicRenoCongestionController(String dbgTag) { + this.dbgTag = dbgTag; + this.timeSource = TimeSource.source(); + } + + private boolean inCongestionRecovery(Deadline sentTime) { + return (congestionRecoveryStartTime != null && + !sentTime.isAfter(congestionRecoveryStartTime)); + } + + private void onCongestionEvent(Deadline sentTime) { + if (inCongestionRecovery(sentTime)) { + return; + } + congestionRecoveryStartTime = timeSource.instant(); + ssThresh = congestionWindow / 2; + congestionWindow = Math.max(minimumWindow, ssThresh); + maxBytesInFlight = 0; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + + private static boolean inFlight(QuicPacket packet) { + // packet is in flight if it contains anything other than a single ACK frame + // specifically, a packet containing padding is considered to be in flight. + return packet.frames().size() != 1 || + !(packet.frames().get(0) instanceof AckFrame); + } + + @Override + public boolean canSendPacket() { + lock.lock(); + try { + if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) { + return false; + } + var canSend = congestionWindow - bytesInFlight >= maxDatagramSize; + return canSend; + } finally { + lock.unlock(); + } + } + + @Override + public void updateMaxDatagramSize(int newSize) { + lock.lock(); + try { + if (minimumWindow != newSize * 2) { + minimumWindow = newSize * 2; + maxDatagramSize = newSize; + congestionWindow = Math.max(congestionWindow, minimumWindow); + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetSent(int packetBytes) { + lock.lock(); + try { + bytesInFlight += packetBytes; + if (bytesInFlight > maxBytesInFlight) { + maxBytesInFlight = bytesInFlight; + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { + lock.lock(); + try { + bytesInFlight -= packetBytes; + // RFC 9002 says we should not increase cwnd when application limited. + // The concept itself is poorly defined. + // Here we limit cwnd growth based on the maximum bytes in flight + // observed since the last congestion event + if (inCongestionRecovery(sentTime)) { + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Acked, in recovery: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } + return; + } + boolean isAppLimited; + if (congestionWindow < ssThresh) { + isAppLimited = congestionWindow >= 2 * maxBytesInFlight; + if (!isAppLimited) { + congestionWindow += packetBytes; + } + } else { + isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize; + if (!isAppLimited) { + congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L); + } + } + if (Log.quicCC()) { + if (isAppLimited) { + Log.logQuic(dbgTag+ " Acked, not blocked: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } else { + Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes + + ", in flight: " + bytesInFlight + + ", new cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { + lock.lock(); + try { + for (QuicPacket packet : lostPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + onCongestionEvent(sentTime); + if (persistent) { + congestionWindow = minimumWindow; + congestionRecoveryStartTime = null; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Persistent congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetDiscarded(Collection discardedPackets) { + lock.lock(); + try { + for (QuicPacket packet : discardedPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java new file mode 100644 index 00000000000..0e2f9401bc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Utils; + +/** + * Estimator for quic connection round trip time. + * Defined in + * RFC 9002 section 5. + * Takes RTT samples as input (max 1 sample per ACK frame) + * Produces: + * - minimum RTT over a period of time (minRtt) for internal use + * - exponentially weighted moving average (smoothedRtt) + * - mean deviation / variation in the observed samples (rttVar) + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicRttEstimator { + + // The property indicates the maximum number of retries. The constant holds the value + // 2^N where N is the value of the property (clamped between 2 and 20, default 8): + // 1=>2 2=>4 3=>8 4=>16 5=>32 6=>64 7=>128 8=>256 ... etc + public static final long MAX_PTO_BACKOFF = 1L << Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoff", 8), + 2, 20); + // The timeout calculated for PTO will stay clamped at MAX_PTO_BACKOFF_TIMEOUT if + // the calculated value exceeds MAX_PTO_BACKOFF_TIMEOUT + public static final Duration MAX_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoffTime", 240), + 1, 1200)); + // backoff will continue to be increased past MAX_PTO_BACKOFF if the timeout calculated + // for PTO is less than MIN_PTO_BACKOFF_TIMEOUT + public static final Duration MIN_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.minPtoBackoffTime", 15), + 0, 1200)); + + private static final long INITIAL_RTT = TimeUnit.MILLISECONDS.toMicros(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.initialRTT", 333), + 50, 1000)); + + // kGranularity, 1ms is recommended by RFC 9002 section 6.1.2 + private static final long GRANULARITY_MICROS = TimeUnit.MILLISECONDS.toMicros(1); + private Deadline firstSample; + private long latestRttMicros; + private long minRttMicros; + private long smoothedRttMicros = INITIAL_RTT; + private long rttVarMicros = INITIAL_RTT / 2; + private long ptoBackoffFactor = 1; + private long rttSampleCount = 0; + + public record QuicRttEstimatorState(long latestRttMicros, + long minRttMicros, + long smoothedRttMicros, + long rttVarMicros, + long rttSampleCount) {} + + public synchronized QuicRttEstimatorState state() { + return new QuicRttEstimatorState(latestRttMicros, minRttMicros, smoothedRttMicros, rttVarMicros, rttSampleCount); + } + + /** + * Update the estimator with latest RTT sample. + * Use only samples where: + * - the largest acknowledged PN is newly acknowledged + * - at least one of the newly acked packets is ack-eliciting + * @param latestRttMicros time between when packet was sent + * and ack was received, in microseconds + * @param ackDelayMicros ack delay received in ack frame, decoded to microseconds + * @param now time at which latestRttMicros was calculated + */ + public synchronized void consumeRttSample(long latestRttMicros, long ackDelayMicros, Deadline now) { + this.rttSampleCount += 1; + this.latestRttMicros = latestRttMicros; + if (firstSample == null) { + firstSample = now; + minRttMicros = latestRttMicros; + smoothedRttMicros = latestRttMicros; + rttVarMicros = latestRttMicros / 2; + } else { + minRttMicros = Math.min(minRttMicros, latestRttMicros); + long adjustedRtt; + if (latestRttMicros >= minRttMicros + ackDelayMicros) { + adjustedRtt = latestRttMicros - ackDelayMicros; + } else { + adjustedRtt = latestRttMicros; + } + rttVarMicros = (3 * rttVarMicros + Math.abs(smoothedRttMicros - adjustedRtt)) / 4; + smoothedRttMicros = (7 * smoothedRttMicros + adjustedRtt) / 8; + } + } + + /** + * {@return time threshold for time-based loss detection} + * See + * RFC 9002 section 6.1.2 + * + */ + public synchronized Duration getLossThreshold() { + // max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity) + long maxRttMicros = Math.max(smoothedRttMicros, latestRttMicros); + long lossThresholdMicros = Math.max(9*maxRttMicros / 8, GRANULARITY_MICROS); + return Duration.of(lossThresholdMicros, ChronoUnit.MICROS); + } + + /** + * {@return the amount of time to wait for acknowledgement of a sent packet, + * excluding max ack delay} + * See + * RFC 9002 section 6.1.2 + */ + public synchronized Duration getBasePtoDuration() { + // PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay + // max_ack_delay is applied by the caller + long basePtoMicros = smoothedRttMicros + + Math.max(4 * rttVarMicros, GRANULARITY_MICROS); + return Duration.of(basePtoMicros, ChronoUnit.MICROS); + } + + public synchronized boolean isMinBackoffTimeoutExceeded() { + return MIN_PTO_BACKOFF_TIMEOUT.compareTo(getBasePtoDuration().multipliedBy(ptoBackoffFactor)) < 0; + } + + public synchronized long getPtoBackoff() { + return ptoBackoffFactor; + } + + public synchronized long increasePtoBackoff() { + // limit to make sure we don't accidentally overflow + if (ptoBackoffFactor <= MAX_PTO_BACKOFF || !isMinBackoffTimeoutExceeded()) { + ptoBackoffFactor *= 2; // can go up to 2 * MAX_PTO_BACKOFF + } + return ptoBackoffFactor; + } + + public synchronized void resetPtoBackoff() { + ptoBackoffFactor = 1; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java new file mode 100644 index 00000000000..2bce3415b17 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicVirtualThreadedEndpoint; +import jdk.internal.net.http.quic.QuicEndpoint.QuicSelectableEndpoint; + + +/** + * A QUIC selector to select over one or several quic transport + * endpoints. + */ +public abstract sealed class QuicSelector implements Runnable, AutoCloseable + permits QuicSelector.QuicNioSelector, QuicSelector.QuicVirtualThreadPoller { + + /** + * The maximum timeout passed to Selector::select. + */ + public static final long IDLE_PERIOD_MS = 1500; + + private static final TimeLine source = TimeSource.source(); + final Logger debug = Utils.getDebugLogger(this::name); + + private final String name; + private volatile boolean done; + private final QuicInstance instance; + private final QuicSelectorThread thread; + private final QuicTimerQueue timerQueue; + + private QuicSelector(QuicInstance instance, String name) { + this.instance = instance; + this.name = name; + this.timerQueue = new QuicTimerQueue(this::wakeup, debug); + this.thread = new QuicSelectorThread(this); + } + + public String name() { + return name; + } + + // must be overridden by subclasses + public void register(T endpoint) throws ClosedChannelException { + if (debug.on()) debug.log("attaching %s", endpoint); + } + + // must be overridden by subclasses + public void wakeup() { + if (debug.on()) debug.log("waking up selector"); + } + + public QuicTimerQueue timer() { + return timerQueue; + } + + /** + * A {@link QuicSelector} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channels. + * This implementation is tied to {@link QuicVirtualThreadedEndpoint} instances. + */ + static final class QuicVirtualThreadPoller extends QuicSelector { + + static final boolean usePlatformThreads = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.poller.usePlatformThreads", false); + + static final class EndpointTask implements Runnable { + + final QuicVirtualThreadedEndpoint endpoint; + final ConcurrentLinkedQueue endpoints; + EndpointTask(QuicVirtualThreadedEndpoint endpoint, + ConcurrentLinkedQueue endpoints) { + this.endpoint = endpoint; + this.endpoints = endpoints; + } + + public void run() { + try { + endpoint.channelReadLoop(); + } finally { + endpoints.remove(this); + } + } + } + + private final Object waiter = new Object(); + private final ConcurrentLinkedQueue endpoints = new ConcurrentLinkedQueue<>(); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ExecutorService virtualThreadService; + + private volatile long wakeups; + + + private QuicVirtualThreadPoller(QuicInstance instance, String name) { + super(instance, name); + virtualThreadService = usePlatformThreads + ? Executors.newThreadPerTaskExecutor(Thread.ofPlatform() + .name(name + "-pt-worker", 1).factory()) + : Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name(name + "-vt-worker-", 1).factory()); + if (debug.on()) debug.log("created"); + } + + ExecutorService readLoopExecutor() { + return virtualThreadService; + } + + @Override + public void register(QuicVirtualThreadedEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(this); + } + + public Future startReading(QuicVirtualThreadedEndpoint endpoint) { + EndpointTask task; + stateLock.lock(); + try { + if (done()) throw new ClosedSelectorException(); + task = new EndpointTask(endpoint, endpoints); + endpoints.add(task); + return virtualThreadService.submit(task); + } finally { + stateLock.unlock(); + } + } + + void markDone() { + // use stateLock to prevent startReading + // to be called *after* shutdown. + stateLock.lock(); + try { + super.shutdown(); + } finally { + stateLock.unlock(); + } + } + + @Override + public void shutdown() { + markDone(); + try { + virtualThreadService.shutdown(); + } finally { + wakeup(); + } + } + + @Override + public void wakeup() { + super.wakeup(); + synchronized (waiter) { + wakeups++; + // there's only one thread that can be waiting + // on waiter - the thread that executes the run() + // method. + waiter.notify(); + } + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + long waited = 0; + while (!done()) { + var wakeups = this.wakeups; + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: wait(%s) wakeups:%s (+%s), waited:%s", + name(), timeout, this.wakeups, this.wakeups - wakeups, waited)); + } else if (debug.on()) { + debug.log("wait(%s) wakeups:%s (+%s), waited: %s", + timeout, this.wakeups, this.wakeups - wakeups, waited); + } + long wwaited = -1; + synchronized (waiter) { + if (done()) return; + if (wakeups == this.wakeups) { + var start = System.nanoTime(); + waiter.wait(timeout); + var stop = System.nanoTime(); + wwaited = waited = (stop - start) / 1000_000; + } else waited = 0; + } + if (wwaited != -1 && wwaited < timeout) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: waked up early: waited %s, timeout %s", + name(), waited, timeout)); + } + } + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicVirtualThreadPoller: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + if (!done()) markDone(); + timer().stop(); + endpoints.removeIf(this::close); + virtualThreadService.close(); + } + } + + boolean close(EndpointTask task) { + try { + task.endpoint.close(); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + boolean abort(EndpointTask task, Throwable error) { + try { + task.endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + @Override + public void abort(Throwable t) { + super.shutdown(); + endpoints.removeIf(task -> abort(task, t)); + super.abort(t); + } + } + + /** + * A {@link QuicSelector} implementation based on non-blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to {@link QuicSelectableEndpoint} instances. + */ + static final class QuicNioSelector extends QuicSelector { + final Selector selector; + + private QuicNioSelector(QuicInstance instance, Selector selector, String name) { + super(instance, name); + this.selector = selector; + if (debug.on()) debug.log("created"); + } + + + public void register(QuicSelectableEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(selector); + selector.wakeup(); + } + + public void wakeup() { + super.wakeup(); + selector.wakeup(); + } + + /** + * Shuts down the {@code QuicSelector} by marking the + * {@linkplain QuicSelector#shutdown() selector done}, + * and {@linkplain Selector#wakeup() waking up the + * selector thread}. + * Upon waking up, the selector thread will invoke + * {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + super.shutdown(); + selector.wakeup(); + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + while (!done()) { + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + // selected = 0 indicates that no key had its ready ops changed: + // it doesn't mean that no key is ready. Therefore - if a key + // was ready to read, and is again ready to read, it doesn't + // increment the selected count. + if (debug.on()) debug.log("select(%s)", timeout); + int selected = selector.select(timeout); + var selectedKeys = selector.selectedKeys(); + if (debug.on()) { + debug.log("Selected: changes=%s, keys=%s", selected, selectedKeys.size()); + } + + // We do not synchronize on selectedKeys: selectedKeys is only + // modified in this thread, whether directly, by calling selectedKeys.clear() below, + // or indirectly, by calling selector.close() below. + for (var key : selectedKeys) { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + if (debug.on()) { + debug.log("selected(%s): %s", Utils.readyOps(key), endpoint); + } + try { + endpoint.selected(key.readyOps()); + } catch (CancelledKeyException x) { + if (debug.on()) { + debug.log("Key for %s cancelled: %s", endpoint.name(), x); + } + } + } + // need to clear the selected keys. select won't do that. + selectedKeys.clear(); + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicNioSelector: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + timer().stop(); + + try { + selector.close(); + } catch (IOException io) { + if (debug.on()) debug.log("failed to close selector: " + io); + } + } + } + + boolean abort(SelectionKey key, Throwable error) { + try { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint associated with key %s: %s", key, error); + } + } + return true; + } + + @Override + public void abort(Throwable error) { + super.shutdown(); + try { + if (selector.isOpen()) { + for (var k : selector.keys()) { + abort(k, error); + } + } + } catch (ClosedSelectorException cse) { + // ignore + } finally { + super.abort(error); + } + } + } + + public long computeNextDeadLine() { + Deadline now = source.instant(); + Deadline deadline = timerQueue.processEventsAndReturnNextDeadline(now, instance.executor()); + if (deadline.equals(Deadline.MAX)) return IDLE_PERIOD_MS; + if (deadline.equals(Deadline.MIN)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", name, 1, "now")); + } + return 1; + } + now = source.instant(); + long millis = now.until(deadline, ChronoUnit.MILLIS); + // millis could be 0 if the next deadline is within 1ms of now. + // in that case, round up millis to 1ms since returning 0 + // means the selector would block indefinitely + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", + name, (millis <= 0L ? 1L : millis), deadline)); + } + return millis <= 0L ? 1L : millis; + } + + public void start() { + thread.start(); + } + + /** + * Shuts down the {@code QuicSelector} by invoking {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + if (debug.on()) debug.log("closing"); + done = true; + } + + boolean done() { + return done; + } + + /** + * Awaits termination of the selector thread, up until + * the given timeout has elapsed. + * If the current thread is the selector thread, returns + * immediately without waiting. + * + * @param timeout the maximum time to wait for termination + * @param unit the timeout unit + */ + public void awaitTermination(long timeout, TimeUnit unit) { + if (Thread.currentThread() == thread) { + return; + } + try { + thread.join(unit.toMillis(timeout)); + } catch (InterruptedException ie) { + if (debug.on()) debug.log("awaitTermination interrupted: " + ie); + Thread.currentThread().interrupt(); + } + } + + /** + * Closes this {@code QuicSelector}. + * This method calls {@link #shutdown()} and then {@linkplain + * #awaitTermination(long, TimeUnit) waits for the selector thread + * to terminate}, up to two {@link #IDLE_PERIOD_MS}. + */ + @Override + public void close() { + shutdown(); + awaitTermination(IDLE_PERIOD_MS * 2, TimeUnit.MILLISECONDS); + } + + @Override + public String toString() { + return name; + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + shutdown(); + } + + static class QuicSelectorThread extends Thread { + QuicSelectorThread(QuicSelector selector) { + super(null, selector, + "Thread(%s)".formatted(selector.name()), + 0, false); + this.setDaemon(true); + } + } + + /** + * {@return a new instance of {@code QuicNioSelector}} + *

      + * A {@code QuicNioSelector} is an implementation of {@link QuicSelector} + * based on non blocking {@linkplain DatagramChannel Datagram Channels} and + * using an underlying {@linkplain Selector NIO Selector}. + *

      + * The returned implementation can only be used with + * {@link QuicSelectableEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + * @throws IOException if an IOException occurs when creating the underlying {@link Selector} + */ + public static QuicSelector createQuicNioSelector(QuicInstance quicInstance, String name) + throws IOException { + Selector selector = Selector.open(); + return new QuicNioSelector(quicInstance, selector, name); + } + + /** + * {@return a new instance of {@code QuicVirtualThreadPoller}} + * A {@code QuicVirtualThreadPoller} is an implementation of + * {@link QuicSelector} based on blocking {@linkplain DatagramChannel + * Datagram Channels} and using {@linkplain Thread#ofVirtual() + * Virtual Threads} to poll the datagram channels. + *

      + * The returned implementation can only be used with + * {@link QuicVirtualThreadedEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + */ + public static QuicSelector createQuicVirtualThreadPoller(QuicInstance quicInstance, String name) { + return new QuicVirtualThreadPoller(quicInstance, name); + } +} diff --git a/src/hotspot/share/gc/shared/bufferNodeList.cpp b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java similarity index 60% rename from src/hotspot/share/gc/shared/bufferNodeList.cpp rename to src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java index 768f40e0985..e5802fef20c 100644 --- a/src/hotspot/share/gc/shared/bufferNodeList.cpp +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, 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. + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -19,20 +21,18 @@ * 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. - * */ +package jdk.internal.net.http.quic; -#include "gc/shared/bufferNodeList.hpp" -#include "utilities/debug.hpp" +/** + * Used internally to indicate Quic stream limit has been reached + */ +public final class QuicStreamLimitException extends Exception { -BufferNodeList::BufferNodeList() : - _head(nullptr), _tail(nullptr), _entry_count(0) {} + @java.io.Serial + private static final long serialVersionUID = 4181770819022847041L; -BufferNodeList::BufferNodeList(BufferNode* head, - BufferNode* tail, - size_t entry_count) : - _head(head), _tail(tail), _entry_count(entry_count) -{ - assert((_head == nullptr) == (_tail == nullptr), "invariant"); - assert((_head == nullptr) == (_entry_count == 0), "invariant"); + public QuicStreamLimitException(String message) { + super(message); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java new file mode 100644 index 00000000000..9269b12bf64 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; + +import jdk.internal.net.http.common.Deadline; + +/** + * Models an event which is triggered upon reaching a + * deadline. {@code QuicTimedEvent} instances are designed to be + * registered with a single {@link QuicTimerQueue}. + * + * @implSpec + * Implementations should make sure that each instance of + * {@code QuicTimedEvent} is only present once in a single + * {@link QuicTimerQueue} at any given time. It is however + * allowed to register the event again with the same {@link QuicTimerQueue} + * after it has been handled, or if it is no longer registered in any + * queue. + */ +sealed interface QuicTimedEvent + permits PacketSpaceManager.PacketTransmissionTask, + QuicTimerQueue.Marker, + QuicEndpoint.ClosedConnection, + IdleTimeoutManager.IdleTimeoutEvent, + IdleTimeoutManager.StreamDataBlockedEvent, + QuicConnectionImpl.MaxInitialTimer { + + /** + * {@return the deadline at which the event should be triggered, + * or {@link Deadline#MAX} if the event does not need + * to be scheduled} + * @implSpec + * Care should be taken to not change the deadline while the + * event is registered with a {@link QuicTimerQueue timer queue}. + * The only safe time when the deadline can be changed is: + *

        + *
      • when {@link #refreshDeadline()} method, since the event + * is not in any queue at that point,
      • + *
      • when the deadline is {@link Deadline#MAX}, since the + * event should not be in any queue if it has no + * deadline
      • + *
      + * + */ + Deadline deadline(); + + /** + * Handles the triggered event. + * Returns a new deadline, if the event needs to be + * rescheduled, or {@code Deadline.MAX} otherwise. + * + * @implSpec + * The {@link #deadline() deadline} should not be + * changed before {@link #refreshDeadline()} is called. + * + * @return a new deadline if the event should be + * rescheduled right away, {@code Deadline.MAX} + * otherwise. + */ + Deadline handle(); + + /** + * An event id, obtained at construction time from + * {@link QuicTimerQueue#newEventId()}. This is used + * to implement a total order among subclasses. + * @return this event's id. + */ + long eventId(); + + /** + * {@return true if this event's deadline is before the + * other's event deadline} + * + * @implSpec + * The default implementation of this method is to return {@code + * deadline().isBefore(other.deadline())}. + * + * @param other the other event + */ + default boolean isBefore(QuicTimedEvent other) { + return deadline().isBefore(other.deadline()); + } + + /** + * Compares this event's deadline with the other event's deadline. + * + * @implSpec + * The default implementation of this method compares deadlines in the same manner as + * {@link Deadline#compareTo(Deadline) this.deadline().compareTo(other.deadline())} would. + * + * @param other the other event + * + * @return {@code -1}, {@code 0}, or {@code 1} depending on whether this + * event's deadline is before, equals to, or after, the other event's + * deadline. + */ + default int compareDeadlines(QuicTimedEvent other) { return deadline().compareTo(other.deadline());} + + /** + * Called to cause an event to refresh its deadline. + * This method is called by the {@link QuicTimerQueue} + * when rescheduling an event. + * @apiNote + * The value returned by {@link #deadline()} can only be safely + * updated when this method is called. + */ + Deadline refreshDeadline(); + + /** + * Compares two instance of {@link QuicTimedEvent}. + * First compared their {@link #deadline()}, then their {@link #eventId()}. + * It is expected that two elements with same deadline and same event id + * must the same {@link QuicTimedEvent} instance. + * + * @param one a first QuicTimedEvent instance + * @param two a second QuicTimedEvent instance + * @return whether the first element is less, same, or greater than the + * second. + */ + static int compare(QuicTimedEvent one, QuicTimedEvent two) { + if (one == two) return 0; + int cmp = one.compareDeadlines(two); + cmp = cmp == 0 ? Long.compare(one.eventId(), two.eventId()) : cmp; + // ensure total ordering; + assert cmp != 0 || one.equals(two) && two.equals(one); + return cmp; + } + + /** + * A comparator that compares {@code QuicTimedEvent} instances by their deadline, in the same + * manner as {@link #compare(QuicTimedEvent, QuicTimedEvent) QuicTimedEvent::compare}. + */ + // public static final (are redundant) + Comparator COMPARATOR = QuicTimedEvent::compare; + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java new file mode 100644 index 00000000000..830415593cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A timer queue that can process events which are due, and possibly + * reschedule them if needed. An instance of a {@link QuicTimerQueue} + * is usually associated with an instance of {@link QuicSelector} which + * provides the timer/wakeup facility. + */ +public final class QuicTimerQueue { + + // A queue that contains scheduled events + private final ConcurrentSkipListSet scheduled = + new ConcurrentSkipListSet<>(QuicTimedEvent.COMPARATOR); + + // A queue that contains events which are due. The queue is + // filled by processAndReturnNextDeadline() + private final ConcurrentLinkedQueue due = + new ConcurrentLinkedQueue<>(); + + // A queue that contains events that need to be rescheduled. + // The event may already be in the scheduled queue - in which + // case it will be removed before being added back. + private final Set rescheduled = + ConcurrentHashMap.newKeySet(); + + // A callback to tell the timer thread to wake up + private final Runnable notifier; + + // A loop to process events which are due, or which need to + // be rescheduled. + private final SequentialScheduler processor = + SequentialScheduler.lockingScheduler(this::processDue); + + private final Logger debug; + private volatile boolean closed; + private volatile Deadline scheduledDeadline = Deadline.MAX; + private volatile Deadline returnedDeadline = Deadline.MAX; + + /** + * Creates a new timer queue with the given notifier. + * A notifier is used to notify the timer thread that + * new events have been added to the queue of scheduled + * event. The notifier should wake up the thread and + * trigger a call to either {@link + * #processEventsAndReturnNextDeadline(Deadline, Executor)} + * or {@link #nextDeadline()}. + * + * @param notifier A notifier to wake up the timer thread when + * new event have been added and the next + * deadline has changed. + */ + public QuicTimerQueue(Runnable notifier, Logger debug) { + this.notifier = notifier; + this.debug = debug; + } + + // For debug purposes only + private String d(Deadline deadline) { + return Utils.debugDeadline(debugNow(), deadline); + } + + // For debug purposes only + private String d(Deadline now, Deadline deadline) { + return Utils.debugDeadline(now, deadline); + } + + // For debug purposes only + private Deadline debugNow() { + return TimeSource.now(); + } + + /** + * Schedule the given event by adding it to the timer queue. + * + * @param event an event to be scheduled + */ + public void offer(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + Deadline deadline = event.deadline(); + scheduled.add(event); + scheduled(deadline); + if (debug.on()) debug.log("QuicTimerQueue: event %s offered", event); + if (notify(deadline)) { + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + /** + * The next deadline for this timer queue. This is only weakly + * consistent. If the queue is empty, {@link Deadline#MAX} is + * returned. + * + * @return The next deadline, or {@code Deadline.MAX}. + */ + public Deadline nextDeadline() { + var event = scheduled.ceiling(FLOOR); + return event == null ? Deadline.MAX : event.deadline(); + } + + public Deadline pendingScheduledDeadline() { + return scheduledDeadline; + } + + /** + * Process all events that were due before {@code now}, and + * returns the next deadline. The events are processed within + * an executor's thread, so this method may return before all + * events have been processed. The events are processed in + * order, with respect to their deadline. Processing an event + * involves invoking its {@link QuicTimedEvent#handle() handle} + * method. If that method returns a new deadline different from + * {@link Deadline#MAX} the processed event is rescheduled + * immediately. Otherwise, it will not be rescheduled. + * + * @param now The point in time before which events are + * considered to be due. Usually, that's now. + * @param executor An executor to process events which are due. + * + * @return the next unexpired deadline, or {@link Deadline#MAX} + * if the queue is empty. + */ + public Deadline processEventsAndReturnNextDeadline(Deadline now, Executor executor) { + QuicTimedEvent event; + int drained = 0; + int dues; + synchronized (this) { + scheduledDeadline = Deadline.MAX; + } + // moved scheduled / rescheduled tasks to due, until + // nothing else is due. Then process dues. + do { + dues = processRescheduled(now); + dues = dues + processScheduled(now); + drained += dues; + } while (dues > 0); + Deadline newDeadline = (event = scheduled.ceiling(FLOOR)) == null ? Deadline.MAX : event.deadline(); + assert event == null || newDeadline.isBefore(Deadline.MAX) : "Invalid deadline for " + event; + if (debug.on()) { + debug.log("QuicTimerQueue: newDeadline: " + d(now, newDeadline) + + (event == null ? "no event scheduled" : (" for " + event))); + } + Deadline next; + synchronized (this) { + var scheduled = scheduledDeadline; + scheduledDeadline = Deadline.MAX; + // if some task is being rescheduled with a deadline + // that is before any scheduled deadline, use that deadline. + next = returnedDeadline = min(newDeadline, scheduled); + } + if (next.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no deadline" + + " (scheduled: %s, rescheduled: %s, dues %s)", + Thread.currentThread().getName(), this.scheduled.size(), + this.rescheduled.size(), this.due.size())); + } + } + if (drained > 0) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: %s events to handle (%s in dues)", + Thread.currentThread().getName(), drained, this.due.size())); + } + processor.runOrSchedule(executor); + } + return next; + } + + // return the deadline which is before the other + private Deadline min(Deadline one, Deadline two) { + return one.isBefore(two) ? one : two; + } + + // walk through the rescheduled tasks and moves any + // that are due to `due`. Otherwise, move them to + // `scheduled` + private int processRescheduled(Deadline now) { + int drained = 0; + for (var it = rescheduled.iterator(); it.hasNext(); ) { + QuicTimedEvent event = it.next(); + it.remove(); // remove before processing to avoid race + scheduled.remove(event); + Deadline deadline = event.refreshDeadline(); + if (deadline.equals(Deadline.MAX)) { + continue; + } + if (deadline.isAfter(now)) { + scheduled.add(event); + } else { + due.add(event); + drained++; + } + } + if (drained > 0) { + if (debug.on()) { + debug.log("QuicTimerQueue: %s rescheduled tasks are due", drained); + } + } + return drained; + } + + // walk through the scheduled tasks and moves any + // that are due to `due`. + private int processScheduled(Deadline now) { + QuicTimedEvent event; + int drained = 0; + while ((event = scheduled.ceiling(FLOOR)) != null) { + Deadline deadline = event.deadline(); + if (!isDue(deadline, now)) { + break; + } + event = scheduled.pollFirst(); + if (event == null) { + break; + } + drained++; + due.add(event); + } + if (drained > 0 && debug.on()) { + debug.log("QuicTimerQueue: %s scheduled tasks are due", drained); + } + return drained; + } + + private static boolean isDue(final Deadline deadline, final Deadline now) { + return deadline.compareTo(now) <= 0; + } + + // process all due events in order + private void processDue() { + try { + QuicTimedEvent event; + if (closed) return; + if (debug.on()) debug.log("QuicTimerQueue: processDue"); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: process %s events", + Thread.currentThread().getName(), due.size())); + } + Deadline minDeadLine = Deadline.MAX; + while ((event = due.poll()) != null) { + if (closed) return; + Deadline nextDeadline = event.handle(); + if (Deadline.MAX.equals(nextDeadline)) continue; + rescheduled.add(event); + if (nextDeadline.isBefore(minDeadLine)) minDeadLine = nextDeadline; + } + + // record the minimal deadline that was rescheduled + scheduled(minDeadLine); + + // wake up the selector thread if necessary + if (notify(minDeadLine)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + notifier.run(); + } else if (!minDeadLine.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no need to notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + } + + } catch (Throwable t) { + if (!closed) { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Unexpected exception while processing due events: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Unexpected exception while processing due events", t); + } + throw t; + } else { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Ignoring exception while closing: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Ignoring exception while closing: " + t); + } + } + } + } + + // We do not need to notify the selector thread if the next scheduled + // deadline is before the given deadline, or if it is after + // the last returned deadline. + private boolean notify(Deadline deadline) { + synchronized (this) { + if (deadline.isBefore(nextDeadline()) + || deadline.isBefore(returnedDeadline)) { + return true; + } + } + return false; + } + + // Record a prospective attempt to reschedule an event at + // the given deadline + private Deadline scheduled(Deadline deadline) { + synchronized (this) { + var scheduled = scheduledDeadline; + if (deadline.isBefore(scheduled)) { + scheduledDeadline = deadline; + return deadline; + } + return scheduled; + } + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method is used if the prospective future deadline at which the event + * should be scheduled is not known by the caller. + * This may cause an idle wakeup in the selector thread owning this + * {@code QuicTimerQueue}. Use {@link #reschedule(QuicTimedEvent, Deadline)} + * to minimize idle wakeup. + * + * @param event an event to reschedule + */ + public void reschedule(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, returnedDeadline), + d(now, nextDeadline()))); + } + notifier.run(); + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method should be used in preference of {@link #reschedule(QuicTimedEvent)} + * if the prospective future deadline at which the event should be scheduled is + * already known by the caller. Using this method will minimize idle wakeup + * of the selector thread, in comparison of {@link #reschedule(QuicTimedEvent)}. + * + * @param event an event to reschedule + * @param deadline the prospective future deadline at which the event should + * be rescheduled + */ + public void reschedule(QuicTimedEvent event, Deadline deadline) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + scheduled(deadline); + // no need to wake up the selector thread if the next deadline + // is already before the new deadline + + if (notify(deadline)) { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } else if (debug.on()) { + debug.log("QuicTimerQueue: event %s will be rescheduled", event); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + private static final AtomicLong EVENTIDS = new AtomicLong(); + + /** + * {@return a unique id for a new {@link QuicTimedEvent}} + * Each new instance of {@link QuicTimedEvent} is created with a long + * ID returned by this method to ensure a total ordering of + * {@code QuicTimedEvent} instances, even when their deadlines + * are equal. + */ + public static long newEventId() { + return EVENTIDS.getAndIncrement(); + } + + // aliases + private static final Marker FLOOR = Marker.FLOOR; + private static final Marker CEILING = Marker.CEILING; + + /** + * Called to clean up the timer queue when it is no longer needed. + * Makes sure that all pending tasks are cleared from the various lists. + */ + public void stop() { + closed = true; + do { + processor.stop(); + due.clear(); + rescheduled.clear(); + scheduled.clear(); + } while (!due.isEmpty() || !rescheduled.isEmpty() || !scheduled.isEmpty()); + } + + // This class is used to work around the lack of a peek() method + // in ConcurrentSkipListSet. ConcurrentSkipListSet has a method + // called first(), but it throws NoSuchElementException if the + // set isEmpty() - whereas peek() would return {@code null}. + // The next best thing is to use ConcurrentSkipListSet::ceiling, + // but for that we need to define a minimum event which is lower + // than any other event: we do this by defining Marker.FLOOR + // which has deadline=Deadline.MIN and eventId=Long.MIN_VALUE; + // Note: it would be easier to use a record, but an enum ensures that we + // can only have the two instances FLOOR and CEILING. + enum Marker implements QuicTimedEvent { + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#ceiling(Object) + * ConcurrentSkipListSet::ceiling} in order to get the first event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
      {@code
      +         *       var head = scheduled.ceiling(FLOOR);
      +         * }
      + * + */ + FLOOR(Deadline.MIN, Long.MIN_VALUE), + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#floor(Object) + * ConcurrentSkipListSet::floor} in order to get the last event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
      {@code
      +         *       var head = scheduled.floor(CEILING);
      +         * }
      + * + */ + CEILING(Deadline.MAX, Long.MAX_VALUE); + private final Deadline deadline; + private final long eventId; + private Marker(Deadline deadline, long eventId) { + this.deadline = deadline; + this.eventId = eventId; + } + + @Override public Deadline deadline() { return deadline; } + @Override public Deadline refreshDeadline() {return Deadline.MAX;} + @Override public Deadline handle() { return Deadline.MAX; } + @Override public long eventId() { return eventId; } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java new file mode 100644 index 00000000000..36832575add --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java @@ -0,0 +1,1319 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.EnumMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class models a collection of Quic transport parameters. This class is mutable + * and not thread safe. + * + * A parameter is considered absent if {@link #getParameter(TransportParameterId)} + * yields {@code null}. The parameter is present otherwise. + * Parameters can be removed by calling {@link + * #setParameter(TransportParameterId, byte[]) setParameter(id, null)}. + * The methods {@link #getBooleanParameter(TransportParameterId)} and + * {@link #getIntParameter(TransportParameterId)} allow easy access to + * parameters whose type is boolean or int, respectively. + * When such a parameter is absent, its default value is returned by + * those methods. + + * From + * RFC 9000, section 18.2: + * + *
      + *
      {@code
      + * Many transport parameters listed here have integer values.
      + * Those transport parameters that are identified as integers use a
      + * variable-length integer encoding; see Section 16. Transport parameters
      + * have a default value of 0 if the transport parameter is absent, unless
      + * otherwise stated.
      + * }
      + * + *

      [...] + * + *

      {@code
      + * If present, transport parameters that set initial per-stream flow control limits
      + * (initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, and
      + * initial_max_stream_data_uni) are equivalent to sending a MAX_STREAM_DATA frame
      + * (Section 19.10) on every stream of the corresponding type immediately after opening.
      + * If the transport parameter is absent, streams of that type start with a flow control
      + * limit of 0.
      + *
      + * A client MUST NOT include any server-only transport parameter:
      + *        original_destination_connection_id,
      + *        preferred_address,
      + *        retry_source_connection_id, or
      + *        stateless_reset_token.
      + *
      + * A server MUST treat receipt of any of these transport parameters as a connection error
      + * of type TRANSPORT_PARAMETER_ERROR.
      + * }
      + *
      + * + * @see ParameterId + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class QuicTransportParameters { + + /** + * An interface to model a transport parameter ID. + * A transport parameter ID has a {@linkplain #name() name} (which is + * not transmitted) and an {@linkplain #idx() identifier}. + * Standard parameters are modeled by enum values in + * {@link ParameterId}. + */ + public sealed interface TransportParameterId { + /** + * {@return the transport parameter name} + * This a human-readable string. + */ + String name(); + + /** + * {@return the transport parameter identifier} + */ + int idx(); + + /** + * {@return the parameter id corresponding to the given identifier, if + * defined, an empty optional otherwise} + * @param idx a parameter identifier + */ + static Optional valueOf(long idx) { + return ParameterId.valueOf(idx); + } + } + + /** + * Standard Quic transport parameter names and ids. + * These are the transport parameters defined in IANA + * "QUIC Transport Parameters" registry. + * @see + * RFC 9000, Section 22.3 + */ + public enum ParameterId implements TransportParameterId { + /** + * original_destination_connection_id (0x00). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This parameter is the value of the Destination Connection ID field
      +         *     from the first Initial packet sent by the client; see Section 7.3.
      +         *     This transport parameter is only sent by a server.
      +         * }
      + * @see RFC 9000, Section 7.3 + */ + original_destination_connection_id(0x00), + + /** + * max_idle_timeout (0x01). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The maximum idle timeout is a value in milliseconds that is encoded
      +         *     as an integer; see (Section 10.1).
      +         *     Idle timeout is disabled when both endpoints omit this transport
      +         *     parameter or specify a value of 0.
      +         * }
      + * @see RFC 9000, Section 10.1 + */ + max_idle_timeout(0x01), + + /** + * stateless_reset_token (0x02). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     A stateless reset token is used in verifying a stateless reset;
      +         *     see Section 10.3.
      +         *     This parameter is a sequence of 16 bytes. This transport parameter MUST NOT
      +         *     be sent by a client but MAY be sent by a server. A server that does not send
      +         *     this transport parameter cannot use stateless reset (Section 10.3) for
      +         *     the connection ID negotiated during the handshake.
      +         * }
      + * @see RFC 9000, Section 10.3 + */ + stateless_reset_token(0x02), + + /** + * max_udp_payload_size (0x03). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The maximum UDP payload size parameter is an integer value that limits the
      +         *     size of UDP payloads that the endpoint is willing to receive. UDP datagrams
      +         *     with payloads larger than this limit are not likely to be processed by
      +         *     the receiver.
      +         *
      +         *     The default for this parameter is the maximum permitted UDP payload of 65527.
      +         *     Values below 1200 are invalid.
      +         *
      +         *     This limit does act as an additional constraint on datagram size
      +         *     in the same way as the path MTU, but it is a property of the endpoint
      +         *     and not the path; see Section 14.
      +         *     It is expected that this is the space an endpoint dedicates to
      +         *     holding incoming packets.
      +         * }
      + * @see RFC 9000, Section 14 + */ + max_udp_payload_size(0x03), + + /** + * initial_max_data (0x04). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The initial maximum data parameter is an integer value that contains
      +         *     the initial value for the maximum amount of data that can be sent on
      +         *     the connection. This is equivalent to sending a MAX_DATA (Section 19.9)
      +         *     for the connection immediately after completing the handshake.
      +         * }
      + * @see RFC 9000, Section 19.9 + */ + initial_max_data(0x04), + + /** + * initial_max_stream_data_bidi_local (0x05). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This parameter is an integer value specifying the initial flow control
      +         *     limit for locally initiated bidirectional streams. This limit applies to
      +         *     newly created bidirectional streams opened by the endpoint that
      +         *     sends the transport parameter.
      +         *     In client transport parameters, this applies to streams with an identifier
      +         *     with the least significant two bits set to 0x00;
      +         *     in server transport parameters, this applies to streams with the least
      +         *     significant two bits set to 0x01.
      +         * }
      + */ + initial_max_stream_data_bidi_local(0x05), + + /** + * initial_max_stream_data_bidi_remote (0x06). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This parameter is an integer value specifying the initial flow control
      +         *     limit for peer-initiated bidirectional streams. This limit applies to
      +         *     newly created bidirectional streams opened by the endpoint that receives
      +         *     the transport parameter. In client transport parameters, this applies to
      +         *     streams with an identifier with the least significant two bits set to 0x01;
      +         *     in server transport parameters, this applies to streams with the least
      +         *     significant two bits set to 0x00.
      +         * }
      + */ + initial_max_stream_data_bidi_remote(0x06), + + /** + * initial_max_stream_data_uni (0x07). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This parameter is an integer value specifying the initial flow control
      +         *     limit for unidirectional streams. This limit applies to newly created
      +         *     unidirectional streams opened by the endpoint that receives the transport
      +         *     parameter. In client transport parameters, this applies to streams with
      +         *     an identifier with the least significant two bits set to 0x03; in server
      +         *     transport parameters, this applies to streams with the least significant
      +         *     two bits set to 0x02.
      +         * }
      + */ + initial_max_stream_data_uni(0x07), + + /** + * initial_max_streams_bidi (0x08). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The initial maximum bidirectional streams parameter is an integer value
      +         *     that contains the initial maximum number of bidirectional streams the
      +         *     endpoint that receives this transport parameter is permitted to initiate.
      +         *     If this parameter is absent or zero, the peer cannot open bidirectional
      +         *     streams until a MAX_STREAMS frame is sent. Setting this parameter is equivalent
      +         *     to sending a MAX_STREAMS (Section 19.11) of the corresponding type with the
      +         *     same value.
      +         * }
      + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_bidi(0x08), + + /** + * initial_max_streams_uni (0x09). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The initial maximum unidirectional streams parameter is an integer value that
      +         *     contains the initial maximum number of unidirectional streams the endpoint
      +         *     that receives this transport parameter is permitted to initiate. If this parameter
      +         *     is absent or zero, the peer cannot open unidirectional streams until a MAX_STREAMS
      +         *     frame is sent. Setting this parameter is equivalent to sending a MAX_STREAMS
      +         *     (Section 19.11) of the corresponding type with the same value.
      +         * }
      + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_uni(0x09), + + /** + * ack_delay_exponent (0x0a). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The acknowledgment delay exponent is an integer value indicating an exponent
      +         *     used to decode the ACK Delay field in the ACK frame (Section 19.3). If this
      +         *     value is absent, a default value of 3 is assumed (indicating a multiplier of 8).
      +         *     Values above 20 are invalid.
      +         * }
      + * @see RFC 9000, Section 19.3 + */ + ack_delay_exponent(0x0a), + + /** + * max_ack_delay (0x0b). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The maximum acknowledgment delay is an integer value indicating the maximum
      +         *     amount of time in milliseconds by which the endpoint will delay sending acknowledgments.
      +         *     This value SHOULD include the receiver's expected delays in alarms firing. For example,
      +         *     if a receiver sets a timer for 5ms and alarms commonly fire up to 1ms late, then it
      +         *     should send a max_ack_delay of 6ms. If this value is absent, a default of 25
      +         *     milliseconds is assumed. Values of 2^14 or greater are invalid.
      +         * }
      + */ + max_ack_delay(0x0b), + + /** + * disable_active_migration (0x0c). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The disable active migration transport parameter is included if the endpoint does not
      +         *     support active connection migration (Section 9) on the address being used during the
      +         *     handshake. An endpoint that receives this transport parameter MUST NOT use a new local
      +         *     address when sending to the address that the peer used during the handshake. This transport
      +         *     parameter does not prohibit connection migration after a client has acted on a
      +         *     preferred_address transport parameter. This parameter is a zero-length value.
      +         * }
      + * @see RFC 9000, Section 9 + */ + disable_active_migration(0x0c), + + /** + * preferred_address (0x0d). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     The server's preferred address is used to effect a change in server address at the
      +         *     end of the handshake, as described in Section 9.6. This transport parameter is only
      +         *     sent by a server.
      +         *     Servers MAY choose to only send a preferred address of one address family
      +         *     by sending an all-zero address and port (0.0.0.0:0 or [::]:0) for the
      +         *     other family. IP addresses are encoded in network byte order.
      +         *
      +         *     The preferred_address transport parameter contains an address and port for both
      +         *     IPv4 and IPv6. The four-byte IPv4 Address field is followed by the associated
      +         *     two-byte IPv4 Port field. This is followed by a 16-byte IPv6 Address field and
      +         *     two-byte IPv6 Port field. After address and port pairs, a Connection ID Length
      +         *     field describes the length of the following Connection ID field.
      +         *     Finally, a 16-byte Stateless Reset Token field includes the stateless reset
      +         *     token associated with the connection ID. The format of this transport parameter
      +         *     is shown in Figure 22 below.
      +         *
      +         *     The Connection ID field and the Stateless Reset Token field contain an alternative
      +         *     connection ID that has a sequence number of 1; see Section 5.1.1. Having these values
      +         *     sent alongside the preferred address ensures that there will be at least one
      +         *     unused active connection ID when the client initiates migration to the preferred
      +         *     address.
      +         *
      +         *     The Connection ID and Stateless Reset Token fields of a preferred address are
      +         *     identical in syntax and semantics to the corresponding fields of a NEW_CONNECTION_ID
      +         *     frame (Section 19.15). A server that chooses a zero-length connection ID MUST NOT
      +         *     provide a preferred address. Similarly, a server MUST NOT include a zero-length
      +         *     connection ID in this transport parameter. A client MUST treat a violation of
      +         *     these requirements as a connection error of type TRANSPORT_PARAMETER_ERROR.
      +         *
      +         * Preferred Address {
      +         *   IPv4 Address (32),
      +         *   IPv4 Port (16),
      +         *   IPv6 Address (128),
      +         *   IPv6 Port (16),
      +         *   Connection ID Length (8),
      +         *   Connection ID (..),
      +         *   Stateless Reset Token (128),
      +         * }
      +         *
      +         * Figure 22: Preferred Address Format
      +         * }
      + * @see RFC 9000, Section 5.1.1 + * @see RFC 9000, Section 9.6 + * @see RFC 9000, Section 19.15 + */ + preferred_address(0x0d), + + /** + * active_connection_id_limit (0x0e). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This is an integer value specifying the maximum number of connection IDs from
      +         *     the peer that an endpoint is willing to store. This value includes the connection
      +         *     ID received during the handshake, that received in the preferred_address transport
      +         *     parameter, and those received in NEW_CONNECTION_ID frames. The value of the
      +         *     active_connection_id_limit parameter MUST be at least 2. An endpoint that receives
      +         *     a value less than 2 MUST close the connection with an error of type
      +         *     TRANSPORT_PARAMETER_ERROR. If this transport parameter is absent, a default of 2 is
      +         *     assumed. If an endpoint issues a zero-length connection ID, it will never send a
      +         *     NEW_CONNECTION_ID frame and therefore ignores the active_connection_id_limit value
      +         *     received from its peer.
      +         * }
      + */ + active_connection_id_limit(0x0e), + + /** + * initial_source_connection_id (0x0f). + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +         *     This is the value that the endpoint included in the Source Connection ID field of
      +         *     the first Initial packet it sends for the connection; see Section 7.3.
      +         * }
      + * @see RFC 9000, Section 7.3 + */ + initial_source_connection_id(0x0f), + + /** + * retry_source_connection_id (0x10). + *

      + * From + * RFC 9000, Section 18.2 + *

      {@code
      +         *     This is the value that the server included in the Source Connection ID field of a
      +         *     Retry packet; see Section 7.3. This transport parameter is only sent by a server.
      +         * }
      + * @see RFC 9000, Section 7.3 + */ + retry_source_connection_id(0x10), + + /** + * version_information (0x11). + *

      + * From + * RFC 9368, Section 3 + *

      {@code
      +         *     During the handshake, endpoints will exchange Version Information,
      +         *     which consists of a Chosen Version and a list of Available Versions.
      +         *     Any version of QUIC that supports this mechanism MUST provide a mechanism
      +         *     to exchange Version Information in both directions during the handshake,
      +         *     such that this data is authenticated.
      +         * }
      + */ + version_information(0x11); + + /* + * Reserved Transport Parameters (31 * N + 27 for int values of N) + *

      + * From + * RFC 9000, Section 18.1 + *

      {@code
      +         *     Transport parameters with an identifier of the form 31 * N + 27
      +         *     for integer values of N are reserved to exercise the requirement
      +         *     that unknown transport parameters be ignored. These transport
      +         *     parameters have no semantics and can carry arbitrary values.
      +         * }
      + */ + // No values are defined here, but these will be + // ignored if received (see + // sun.security.ssl.QuicTransportParametersExtension). + + /** + * The number of known transport parameters. + * This is also the number of enum values defined by the + * {@link ParameterId} enumeration. + */ + private static final int PARAMETERS_COUNT = ParameterId.values().length; + + ParameterId(int idx) { + // idx() and valueOf() assume that idx = ordinal; + // if that's no longer the case, update the implementation + // and remove this assert. + assert idx == ordinal(); + } + + @Override + public int idx() { + return ordinal(); + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + + private static Optional valueOf(long idx) { + if (idx < 0 || idx >= PARAMETERS_COUNT) return Optional.empty(); + return Optional.of(values()[(int)idx]); + } + } + + public record VersionInformation(int chosenVersion, int[] availableVersions) { } + + /** + * A map to store transport parameter values. + * Contains a byte array corresponding to the encoded value + * of the parameter. + */ + private final Map values; + + /** + * Constructs a new empty array of Quic transport parameters. + */ + public QuicTransportParameters() { + values = new EnumMap<>(ParameterId.class); + } + + /** + * Constructs a new collection of Quic transport parameters initialized + * from the specified collection. + * @param params the parameter collection used to initialize this object * + */ + public QuicTransportParameters(QuicTransportParameters params) { + values = new EnumMap<>(params.values); + } + + /** + * {@return true if the given parameter is present, false otherwise} + * @apiNote + * This is equivalent to {@link #getParameter(TransportParameterId) + * getParameter(id) != null}, but avoids cloning the parameter value. + * @param id the parameter id + */ + public boolean isPresent(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value != null; + } + + /** + * {@return the value of the given parameter, as a byte array, or + * {@code null} if the parameter is absent. + * @param id the parameter id + */ + public byte[] getParameter(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value == null ? null : value.clone(); + } + + /** + * {@return true if the value of the given parameter matches the given connection ID} + * @param id the transport parameter id + * @param connectionId the connection id to match against + */ + public boolean matches(TransportParameterId id, QuicConnectionId connectionId) { + byte[] value = values.get((ParameterId) id); + return connectionId.matches(ByteBuffer.wrap(value).asReadOnlyBuffer()); + } + + /** + * Sets the value of the given parameter. + * If the given value is {@code null}, the parameter is removed. + * @param id the parameter id + * @param value the new parameter value, or {@code null}. + * @throws IllegalArgumentException if the given value is invalid for + * the given parameter id + */ + public void setParameter(TransportParameterId id, byte[] value) { + ParameterId pid = checkParameterValue(id, value); + if (value != null) { + values.put(pid, value.clone()); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present its default value (as specified in the RFC) is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int + */ + public long getIntParameter(TransportParameterId id) { + return getIntParameter((ParameterId)id); + } + + private long getIntParameter(final ParameterId pid) { + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = switch (pid) { + case active_connection_id_limit -> 2; + case max_udp_payload_size -> 65527; + case ack_delay_exponent -> 3; + case max_ack_delay -> 25; + default -> 0; + }; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + + }; + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present then {@code defaultValue} is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int or if the {@code defaultValue} + * exceeds the maximum allowed value for variable length integer + */ + public long getIntParameter(TransportParameterId id, long defaultValue) { + if (defaultValue > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("default value " + defaultValue + + " exceeds maximum allowed variable length" + + " integer value " + VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + ParameterId pid = (ParameterId)id; + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = defaultValue; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + }; + } + + /** + * Sets the value of the given parameter, as an unsigned int. + * If a negative value is provided, the parameter is removed. + * + * @param id the parameter id + * @param value the new value of the parameter, or a negative value + * + * @throws IllegalArgumentException if the value of the given parameter is + * not an int, or if the provided value is out of range + */ + public void setIntParameter(TransportParameterId id, long value) { + ParameterId pid = (ParameterId)id; + switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] v = null; + if (value >= 0) { + int length = VariableLengthEncoder.getEncodedSize(value); + if (length <= 0) throw new IllegalArgumentException("failed to encode " + value); + int size = VariableLengthEncoder.encode(ByteBuffer.wrap(v = new byte[length]), value); + assert size == length; + checkParameterValue(pid, v); + } + setOrRemove(pid, v); + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + } + } + + /** + * {@return the value of the given parameter, as a boolean} + * If the parameter is not present its default value (false) + * is returned. + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a boolean + */ + public boolean getBooleanParameter(TransportParameterId id) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + return values.get(pid) != null; + } + + /** + * Sets the value of the given parameter, as a boolean. + * @apiNote + * It is not possible to distinguish between a boolean parameter + * whose value is absent and a parameter whose value is false. + * Both are represented by a {@code null} value in the parameter + * array. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a boolean + */ + public void setBooleanParameter(TransportParameterId id, boolean value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + setOrRemove(pid, value ? NOBYTES : null); + } + + private void setOrRemove(ParameterId pid, byte[] value) { + if (value != null) { + values.put(pid, value); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as {@link VersionInformation}} + * If the parameter is not present {@code null} is returned + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a version information + * @throws QuicTransportException if the parameter value has incorrect length, + * or if any version is equal to zero + */ + public VersionInformation getVersionInformationParameter(TransportParameterId id) + throws QuicTransportException { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = values.get(pid); + if (val == null) { + return null; + } + if (val.length < 4 || (val.length & 3) != 0) { + throw new QuicTransportException( + "Invalid version information length " + val.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + int chosen = bbval.getInt(); + if (chosen == 0) { + throw new QuicTransportException( + "[version_information] Chosen Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + int[] available = new int[bbval.remaining() / 4]; + for (int i = 0; i < available.length; i++) { + int version = bbval.getInt(); + if (version == 0) { + throw new QuicTransportException( + "[version_information] Available Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + available[i] = version; + } + return new VersionInformation(chosen, available); + } + + /** + * Sets the value of the given parameter, as {@link VersionInformation}. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a version information + */ + public void setVersionInformationParameter(TransportParameterId id, VersionInformation value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = new byte[value.availableVersions.length * 4 + 4]; + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + bbval.putInt(value.chosenVersion); + for (int available : value.availableVersions) { + bbval.putInt(available); + } + assert !bbval.hasRemaining(); + values.put(pid, val); + } + + /** + * {@return a {@link VersionInformation} object corresponding to the specified versions} + * @param chosenVersion chosen version + * @param availableVersions available versions + */ + public static VersionInformation buildVersionInformation( + QuicVersion chosenVersion, List availableVersions) { + int[] available = new int[availableVersions.size()]; + for (int i = 0; i < available.length; i++) { + available[i] = availableVersions.get(i).versionNumber(); + } + return new VersionInformation(chosenVersion.versionNumber(), available); + } + + /** + * Sets the value of a parameter whose format corresponds to the + * {@link ParameterId#preferred_address} parameter. + * @param id the parameter id + * @param ipv4 the preferred IPv4 address (or the IPv4 wildcard address) + * @param port4 the preferred IPv4 port (or 0) + * @param ipv6 the preferred IPv6 address (or the IPv6 wildcard address) + * @param port6 the preferred IPv6 port (or 0) + * @param connectionId the connection id bytes + * @param statelessToken the stateless token + * @throws IllegalArgumentException if any of the given parameters has an + * illegal value, or if the given parameter value is not of the + * {@link ParameterId#preferred_address} format + * @see ParameterId#preferred_address + */ + public void setPreferredAddressParameter(TransportParameterId id, + Inet4Address ipv4, int port4, + Inet6Address ipv6, int port6, + ByteBuffer connectionId, + ByteBuffer statelessToken) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.preferred_address) { + throw new IllegalArgumentException(String.valueOf(id)); + } + int cidlen = connectionId.remaining(); + if (cidlen == 0 || cidlen > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException( + "connection id len out of range [1..20]: " + cidlen); + } + int tklen = statelessToken.remaining(); + if (tklen != TOKEN_SIZE) { + throw new IllegalArgumentException("bad stateless token length: expected 16, found " + tklen); + } + if (port4 < 0 || port4 > MAX_PORT) + throw new IllegalArgumentException("IPv4 port out of range: " + port4); + if (port6 < 0 || port6 > MAX_PORT) + throw new IllegalArgumentException("IPv6 port out of range: " + port6); + int size = MIN_PREF_ADDR_SIZE + cidlen; + byte[] value = new byte[size]; + ByteBuffer buffer = ByteBuffer.wrap(value); + if (!ipv4.isAnyLocalAddress()) { + buffer.put(IPV4_ADDR_OFFSET, ipv4.getAddress()); + } + buffer.putShort(IPV4_PORT_OFFSET, (short) port4); + if (!ipv6.isAnyLocalAddress()) { + buffer.put(IPV6_ADDR_OFFSET, ipv6.getAddress()); + } + buffer.putShort(IPV6_PORT_OFFSET, (short)port6); + buffer.put(CID_LEN_OFFSET, (byte) cidlen); + buffer.put(CID_OFFSET, connectionId, connectionId.position(), cidlen); + assert size - CID_OFFSET - cidlen == TOKEN_SIZE : (size - CID_OFFSET - cidlen); + assert tklen == TOKEN_SIZE; + buffer.put(CID_OFFSET + cidlen, statelessToken, statelessToken.position(), tklen); + values.put(pid, value); + } + + /** + * {@return the size in bytes required to encode the parameter + * array} + */ + public int size() { + int size = 0; + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + assert value.length > 0 || i == ParameterId.disable_active_migration.idx(); + size += VariableLengthEncoder.getEncodedSize(i); + size += VariableLengthEncoder.getEncodedSize(value.length); + size += value.length; + } + return size; + } + + /** + * Encodes the transport parameters into the given byte buffer. + *

      + * From + * RFC 9000, Section 18.2: + *

      {@code
      +     * The extension_data field of the quic_transport_parameters
      +     * extension defined in [QUIC-TLS] contains the QUIC transport
      +     * parameters. They are encoded as a sequence of transport
      +     * parameters, as shown in Figure 20:
      +     *
      +     * Transport Parameters {
      +     *   Transport Parameter (..) ...,
      +     * }
      +     *
      +     * Figure 20: Sequence of Transport Parameters
      +     *
      +     * Each transport parameter is encoded as an (identifier, length,
      +     * value) tuple, as shown in Figure 21:
      +     *
      +     * Transport Parameter {
      +     *   Transport Parameter ID (i),
      +     *   Transport Parameter Length (i),
      +     *   Transport Parameter Value (..),
      +     * }
      +     * }
      + * + * @param buffer a byte buffer in which to encode the transport parameters + * @return the number of bytes written + * @throws BufferOverflowException if there is not enough space in the + * provided buffer + * @see jdk.internal.net.quic.QuicTLSEngine#setLocalQuicTransportParameters(ByteBuffer) + * @see + * RFC 9000, Section 18 + * @see + * RFC 9001 [QUIC-TLS] + */ + public int encode(ByteBuffer buffer) { + int start = buffer.position(); + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + + VariableLengthEncoder.encode(buffer, i); + VariableLengthEncoder.encode(buffer, value.length); + buffer.put(value); + } + var written = buffer.position() - start; + if (QuicTransportParameters.class.desiredAssertionStatus()) { + int size = size(); + assert written == size + : "unexpected number of bytes encoded: %d, expected %d" + .formatted(written, size); + } + return written; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set + // we just return the string representation of the param ids and don't include + // the encoded values + sb.append(param); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + // values for (variable length) integer params are decoded, for other params + // that are set, the value is printed as a hex string. + public String toStringWithValues() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set, so include it in the string representation + sb.append(param); + final String valAsString = valueToString(param); + sb.append("=").append(valAsString); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + private String valueToString(final ParameterId parameterId) { + assert this.values.get(parameterId) != null : "param " + parameterId + " not set"; + try { + return switch (parameterId) { + // int params + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, + initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> + String.valueOf(getIntParameter(parameterId)); + default -> + '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + }; + } catch (RuntimeException e) { + // if the value was a malformed integer, return the hex representation + return '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + } + } + + /** + * Decodes the quic transport parameters from the given buffer. + * Parameters which are not supported are silently discarded. + * + * @param buffer a byte buffer containing the transport parameters + * + * @return the decoded transport parameters + * @throws QuicTransportException if the parameters couldn't be decoded + * + * @see jdk.internal.net.quic.QuicTLSEngine#setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer) (ByteBuffer) + * @see jdk.internal.net.quic.QuicTransportParametersConsumer#accept(ByteBuffer) + * @see #encode(ByteBuffer) + * @see + * RFC 9000, Section 18 + */ + public static QuicTransportParameters decode(ByteBuffer buffer) + throws QuicTransportException { + QuicTransportParameters parameters = new QuicTransportParameters(); + while (buffer.hasRemaining()) { + final long id = VariableLengthEncoder.decode(buffer); + final ParameterId pid = TransportParameterId.valueOf(id) + .orElse(null); + final String name = pid == null ? String.valueOf(id) : pid.toString(); + long length = VariableLengthEncoder.decode(buffer); + if (length < 0) { + throw new QuicTransportException( + "Can't decode length for transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (length > buffer.remaining()) { + throw new QuicTransportException("Transport parameter truncated", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + byte[] value = new byte[(int) length]; + buffer.get(value); + if (pid == null) { + // RFC-9000, section 7.4.2: An endpoint MUST ignore transport parameters + // that it does not support. + if (Log.quicControl()) { + Log.logQuic("ignoring unsupported transport parameter: " + name); + } + continue; + } + try { + checkParameterValue(pid, value); + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + var oldValue = parameters.values.putIfAbsent(pid, value); + if (oldValue != null) { + throw new QuicTransportException( + "Duplicate transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + return parameters; + } + + /** + * Reads the preferred address encoded in the value + * of a parameter whose format corresponds to the {@link + * ParameterId#preferred_address} parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * Otherwise, the returned list contains + * at most one IPv4 address and/or one IPv6 address. + * + * @apiNote + * To obtain the list of addresses encoded in the + * {@link ParameterId#preferred_address} parameter, use + * {@link #getPreferredAddress(TransportParameterId, byte[]) + * getPreferredAddress(ParameterId.preferred_address,} + * {@link #getParameter(TransportParameterId) + * parameters.getParameter(ParameterId.preferred_address)}. + * + * @param id the parameter id + * @param value the value of the parameter + * @return a list of {@link InetSocketAddress}, or {@code null} if the + * given value is {@code null}. + * @see ParameterId#preferred_address + */ + public static List getPreferredAddress( + TransportParameterId id, byte[] value) { + if (value == null) return null; + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + ByteBuffer buffer = ByteBuffer.wrap(value); + int ipv4port = buffer.getShort(IPV4_PORT_OFFSET) & 0xFFFF; + int ipv6port = buffer.getShort(IPV6_PORT_OFFSET) & 0xFFFF; + + byte[] ipv4 = new byte[IPV4_SIZE]; + buffer.get(IPV4_ADDR_OFFSET, ipv4); + byte[] ipv6 = new byte[IPV6_SIZE]; + buffer.get(IPV6_ADDR_OFFSET, ipv6); + InetSocketAddress ipv4addr = new InetSocketAddress(getByAddress(id, ipv4), ipv4port); + InetSocketAddress ipv6addr = new InetSocketAddress(getByAddress(id, ipv6), ipv6port); + return Stream.of(ipv4addr, ipv6addr) + .filter((isa) -> !isa.getAddress().isAnyLocalAddress()) + .toList(); + } + + /** + * Reads the connection id bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the connection id bytes + * @see ParameterId#preferred_address + */ + public static ByteBuffer getPreferredConnectionId(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + return ByteBuffer.wrap(preferredAddressValue, CID_OFFSET, length); + } + + /** + * Reads the stateless token bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the stateless reset token bytes + * @see ParameterId#preferred_address + */ + public static byte[] getPreferredStatelessResetToken(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + final int offset = CID_OFFSET + length; + final byte[] statelessResetToken = new byte[TOKEN_SIZE]; + System.arraycopy(preferredAddressValue, offset, statelessResetToken, 0, TOKEN_SIZE); + return statelessResetToken; + } + + static final byte[] NOBYTES = new byte[0]; + static final int IPV6_SIZE = 16; + static final int IPV4_SIZE = 4; + static final int PORT_SIZE = 2; + static final int TOKEN_SIZE = 16; + static final int CIDLEN_SIZE = 1; + static final int IPV4_ADDR_OFFSET = 0; + static final int IPV4_PORT_OFFSET = IPV4_ADDR_OFFSET + IPV4_SIZE; + static final int IPV6_ADDR_OFFSET = IPV4_PORT_OFFSET + PORT_SIZE; + static final int IPV6_PORT_OFFSET = IPV6_ADDR_OFFSET + IPV6_SIZE; + static final int CID_LEN_OFFSET = IPV6_PORT_OFFSET + PORT_SIZE; + static final int CID_OFFSET = CID_LEN_OFFSET + CIDLEN_SIZE; + static final int MIN_PREF_ADDR_SIZE = CID_OFFSET + TOKEN_SIZE; + static final int MAX_PORT = 0xFFFF; + + private static int getPreferredConnectionIdLength(TransportParameterId id, byte[] value) { + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + int length = value[CID_LEN_OFFSET] & 0xFF; + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH || length == 0) { + throw new IllegalArgumentException(id + + ": invalid preferred connection ID length: " + length); + } + if (length != value.length - MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": invalid preferred address length: " + value.length + + ", expected: " + (MIN_PREF_ADDR_SIZE + length)); + } + return length; + } + + private static InetAddress getByAddress(TransportParameterId id, byte[] address) { + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException x) { + // should not happen + throw new IllegalArgumentException(id + + "Invalid address: " + HexFormat.of().formatHex(address)); + } + } + + /** + * verifies that the {@code value} is acceptable (as specified in the RFC) for the + * {@code tpid} + * + * @param tpid the transport parameter id + * @param value the value + * @return the corresponding parameter id if the value is acceptable, else throws a + * {@link IllegalArgumentException} + */ + private static ParameterId checkParameterValue(TransportParameterId tpid, byte[] value) { + ParameterId id = (ParameterId)tpid; + if (value != null) { + switch (id) { + case disable_active_migration -> { + if (value.length > 0) + throw new IllegalArgumentException(id + + ": value must be null or 0-length; found " + + value.length + " bytes"); + } + case stateless_reset_token -> { + if (value.length != 16) + throw new IllegalArgumentException(id + + ": value must be null or 16 bytes long; found " + + value.length + " bytes"); + } + case initial_source_connection_id, original_destination_connection_id, + retry_source_connection_id -> { + if (value.length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException(id + + ": value must not exceed " + + QuicConnectionId.MAX_CONNECTION_ID_LENGTH + + "bytes; found " + value.length + " bytes"); + } + } + case preferred_address -> getPreferredConnectionIdLength(id, value); + case version_information -> { + if (value.length < 4 || value.length % 4 != 0) { + throw new IllegalArgumentException(id + + ": value length must be a positive multiple of 4 " + + "bytes; found " + value.length + " bytes"); + } + } + default -> { + long intvalue; + try { + intvalue = decodeVLIntFully(id, ByteBuffer.wrap(value)); + } catch (IllegalArgumentException x) { + throw x; + } catch (Exception x) { + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer", x); + } + if (intvalue < 0) + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer"); + switch (id) { + case max_udp_payload_size -> { + if (intvalue < 1200 || intvalue > 65527) { + throw new IllegalArgumentException(id + + ": value out of range [1200, 65527]; found " + + intvalue); + } + } + case ack_delay_exponent -> { + if (intvalue > 20) { + throw new IllegalArgumentException(id + + ": value out of range [0, 20]; found " + + intvalue); + } + } + case max_ack_delay -> { + if (intvalue >= (1 << 14)) { + throw new IllegalArgumentException(id + + ": value out of range [0, 2^14); found " + + intvalue); + } + } + case active_connection_id_limit -> { + if (intvalue < 2) { + throw new IllegalArgumentException(id + + ": value out of range [2...]; found " + + intvalue); + } + } + case initial_max_streams_bidi, initial_max_streams_uni -> { + if (intvalue >= 1L << 60) { + throw new IllegalArgumentException(id + + ": value out of range [0,2^60); found " + + intvalue); + } + } + } + } + } + } + return id; + } + + private static long decodeVLIntFully(ParameterId id, ByteBuffer buffer) { + long value = VariableLengthEncoder.decode(buffer); + if (value < 0 || value > (1L << 62) - 1) { + throw new IllegalArgumentException(id + + ": failed to decode variable length integer"); + } + if (buffer.hasRemaining()) + throw new IllegalArgumentException(id + + ": extra bytes in provided value at index " + + buffer.position()); + return value; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java new file mode 100644 index 00000000000..9e441cf7873 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +// TODO: document this +public abstract sealed class TerminationCause { + private String logMsg; + private String peerVisibleReason; + private final long closeCode; + private final Throwable originalCause; + private final IOException reportedCause; + + private TerminationCause(final long closeCode, final Throwable closeCause) { + this.closeCode = closeCode; + this.originalCause = closeCause; + if (closeCause != null) { + this.logMsg = closeCause.toString(); + } + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + private TerminationCause(final long closeCode, final String loggedAs) { + this.closeCode = closeCode; + this.originalCause = null; + this.logMsg = loggedAs; + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + public final long getCloseCode() { + return this.closeCode; + } + + public final IOException getCloseCause() { + return this.reportedCause; + } + + public final String getLogMsg() { + return logMsg; + } + + public final TerminationCause loggedAs(final String logMsg) { + this.logMsg = logMsg; + return this; + } + + public final String getPeerVisibleReason() { + return this.peerVisibleReason; + } + + public final TerminationCause peerVisibleReason(final String reasonPhrase) { + this.peerVisibleReason = reasonPhrase; + return this; + } + + public abstract boolean isAppLayer(); + + public static TerminationCause forTransportError(final QuicTransportErrors err) { + return new TransportError(err); + } + + public static TerminationCause forTransportError(long errorCode, String loggedAs, long frameType) { + return new TransportError(errorCode, loggedAs, frameType); + } + + static SilentTermination forSilentTermination(final String loggedAs) { + return new SilentTermination(loggedAs); + } + + public static TerminationCause forException(final Throwable cause) { + Objects.requireNonNull(cause); + if (cause instanceof QuicTransportException qte) { + return new TransportError(qte); + } + return new InternalError(cause); + } + + // allows for higher (application) layer to inform the connection terminator + // that the higher layer had completed a graceful shutdown of the connection + // and the QUIC layer can now do an immediate close of the connection using + // the {@code closeCode} + public static TerminationCause appLayerClose(final long closeCode) { + return new AppLayerClose(closeCode, (Throwable)null); + } + + public static TerminationCause appLayerClose(final long closeCode, String loggedAs) { + return new AppLayerClose(closeCode, loggedAs); + } + + public static TerminationCause appLayerException(final long closeCode, + final Throwable cause) { + return new AppLayerClose(closeCode, cause); + } + + private static IOException toReportedCause(final Throwable original, + final String fallbackExceptionMsg) { + if (original == null) { + return fallbackExceptionMsg == null + ? new IOException("connection terminated") + : new IOException(fallbackExceptionMsg); + } else if (original instanceof QuicTransportException qte) { + return new IOException(qte.getMessage()); + } else if (original instanceof IOException ioe) { + return ioe; + } else { + return new IOException(original); + } + } + + + static final class TransportError extends TerminationCause { + final long frameType; + final QuicTLSEngine.KeySpace keySpace; + + private TransportError(final QuicTransportErrors err) { + super(err.code(), err.name()); + this.frameType = 0; // unknown frame type + this.keySpace = null; + } + + private TransportError(final QuicTransportException exception) { + super(exception.getErrorCode(), exception); + this.frameType = exception.getFrameType(); + this.keySpace = exception.getKeySpace(); + peerVisibleReason(exception.getReason()); + } + + public TransportError(long errorCode, String loggedAs, long frameType) { + super(errorCode, loggedAs); + this.frameType = frameType; + keySpace = null; + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class InternalError extends TerminationCause { + + private InternalError(final Throwable cause) { + super(QuicTransportErrors.INTERNAL_ERROR.code(), cause); + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class AppLayerClose extends TerminationCause { + private AppLayerClose(final long closeCode, String loggedAs) { + super(closeCode, loggedAs); + } + + // TODO: allow optionally to specify "name" of the close code for app layer + // like "H3_GENERAL_PROTOCOL_ERROR" (helpful in logging) + private AppLayerClose(final long closeCode, final Throwable cause) { + super(closeCode, cause); + } + + @Override + public boolean isAppLayer() { + return true; + } + } + + static final class SilentTermination extends TerminationCause { + + private SilentTermination(final String loggedAs) { + // the error code won't play any role, since silent termination + // doesn't cause any packets to be generated or sent to the peer + super(NO_ERROR.code(), loggedAs); + } + + @Override + public boolean isAppLayer() { + return false; // doesn't play a role in context of silent termination + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java new file mode 100644 index 00000000000..91380fcfca4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; + +/** + * QUIC packets and frames commonly use a variable-length encoding for + * non-negative values. This encoding ensures that smaller values will use less + * in the packet or frame. + * + *

      The QUIC variable-length encoding reserves the two most significant bits + * of the first byte to encode the size of the length value as a base 2 logarithm + * value. The length itself is then encoded on the remaining bits, in network + * byte order. This means that the length values will be encoded on 1, 2, 4, or + * 8 bytes and can encode 6-, 14-, 30-, or 62-bit values + * respectively, or a value within the range of 0 to 4611686018427387903 + * inclusive. + * + * @spec https://www.rfc-editor.org/rfc/rfc9000.html#integer-encoding + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class VariableLengthEncoder { + + /** + * The maximum number of bytes on which a variable length + * integer can be encoded. + */ + public static final int MAX_INTEGER_LENGTH = 8; + + /** + * The maximum value a variable length integer can + * take. + */ + public static final long MAX_ENCODED_INTEGER = (1L << 62) - 1; + + static { + assert MAX_ENCODED_INTEGER == 4611686018427387903L; + } + + private VariableLengthEncoder() { + throw new InternalError("should not come here"); + } + + /** + * Decode a variable length value from {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte read. + * + * @param buffer the {@code ByteBuffer} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(ByteBuffer buffer) { + return decode(BuffersReader.single(buffer)); + } + + /** + * Decode a variable length value from {@code BuffersReader}. This method assumes that the + * position of {@code buffers} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte. + * + * @param buffers the {@code BuffersReader} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(BuffersReader buffers) { + if (!buffers.hasRemaining()) + return -1; + + long pos = buffers.position(); + int lenByte = buffers.get(pos) & 0xFF; + pos++; + // read size of length from leading two bits + int prefix = lenByte >> 6; + int len = 1 << prefix; + // retrieve remaining bits that constitute the length + long result = lenByte & 0x3F; + long idx = 0, lim = buffers.limit(); + if (lim - pos < len - 1) return -1; + while (idx++ < len - 1) { + assert pos < lim; + result = ((result << Byte.SIZE) + (buffers.get(pos) & 0xFF)); + pos++; + } + // Set position of ByteBuffer to next byte following length + assert pos == buffers.position() + len; + assert pos <= buffers.limit(); + buffers.position(pos); + + assert (result >= 0) && (result < (1L << 62)); + return result; + } + + /** + * Encode (a variable length) value into {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last length byte. + * + * @param buffer the {@code ByteBuffer} that the length will be encoded into + * @param value the variable length value + * + * @throws IllegalArgumentException + * if value supplied falls outside of acceptable bounds [0, 2^62-1], + * or if the given buffer doesn't contain enough space to encode the + * value + * + * @return the {@code position} of the buffer + */ + public static int encode(ByteBuffer buffer, long value) throws IllegalArgumentException { + // check for valid parameters + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException( + "value supplied falls outside of acceptable bounds"); + if (!buffer.hasRemaining()) + throw new IllegalArgumentException( + "buffer does not contain enough bytes to store length"); + + // set length prefix to indicate size of length + int lengthPrefix = getVariableLengthPrefix(value); + assert lengthPrefix >= 0 && lengthPrefix <= 3; + lengthPrefix <<= (Byte.SIZE - 2); + + int lengthSize = getEncodedSize(value); + assert lengthSize > 0; + assert lengthSize <= 8; + + var limit = buffer.limit(); + var pos = buffer.position(); + + // check that it's possible to add length to buffer + if (lengthSize > limit - pos) + throw new IllegalArgumentException("buffer does not contain enough bytes to store length"); + + // create mask to use in isolating byte to transfer to buffer + long mask = 255L << (Byte.SIZE * (lengthSize - 1)); + // convert length to bytes and add to buffer + boolean isFirstByte = true; + for (int i = lengthSize; i > 0; i--) { + assert buffer.hasRemaining() : "no space left at " + (lengthSize - i); + assert mask != 0; + assert mask == (255L << ((i - 1) * 8)) + : "mask: %x, expected %x".formatted(mask, (255L << ((i - 1) * 8))); + + long b = value & mask; + for (int j = i - 1; j > 0; j--) { + b >>= Byte.SIZE; + } + + assert b == (value & mask) >> (8 * (i - 1)); + + if (isFirstByte) { + assert (b & 0xC0) == 0; + buffer.put((byte) (b | lengthPrefix)); + isFirstByte = false; + } else { + buffer.put((byte) b); + } + // move mask over to next byte - avoid carrying sign bit + mask = (mask >>> Byte.SIZE); + } + var bytes = buffer.position() - pos; + assert bytes == lengthSize; + return lengthSize; + } + + /** + * Returns the variable length prefix. + * The variable length prefix is the base 2 logarithm of + * the number of bytes required to encode + * a positive value as a variable length integer: + * [0, 1, 2, 3] for [1, 2, 4, 8] bytes. + * + * @param value the value to encode + * + * @throws IllegalArgumentException + * if the supplied value falls outside the acceptable bounds [0, 2^62-1] + * + * @return the base 2 logarithm of the number of bytes required to encode + * the value as a variable length integer. + */ + public static int getVariableLengthPrefix(long value) throws IllegalArgumentException { + if ((value > MAX_ENCODED_INTEGER) || (value < 0)) + throw new IllegalArgumentException("invalid length"); + + int lengthPrefix; + if (value > (1L << 30) - 1) + lengthPrefix = 3; // 8 bytes + else if (value > (1L << 14) - 1) + lengthPrefix = 2; // 4 bytes + else if (value > (1L << 6) - 1) + lengthPrefix = 1; // 2 bytes + else + lengthPrefix = 0; // 1 byte + + return lengthPrefix; + } + + /** + * Returns the number of bytes needed to encode + * the given value as a variable length integer. + * This a number between 1 and 8. + * + * @param value the value to encode + * + * @return the number of bytes needed to encode + * the given value as a variable length integer. + * + * @throws IllegalArgumentException + * if the value supplied falls outside of acceptable bounds [0, 2^62-1] + */ + public static int getEncodedSize(long value) throws IllegalArgumentException { + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException("invalid variable length integer: " + value); + return 1 << getVariableLengthPrefix(value); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + *

      This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(ByteBuffer buffer, int offset) { + return peekEncodedValue(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + * This method doesn't advance the buffer position. + * + * @param buffers the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(BuffersReader buffers, long offset) { + + // figure out on how many bytes the length is encoded. + int size = peekEncodedValueSize(buffers, offset); + if (size <= 0) return -1L; + assert size > 0 && size <= 8; + + // check that we have enough bytes in the buffer + long limit = buffers.limit(); + long pos = offset; + if (limit - size < pos) return -1L; + + // peek at the variable length: + // - read first byte + int first = buffers.get(pos++); + long res = first & 0x3F; + if (size == 1) return res; + + // - read the rest of the bytes + size -= 1; + assert size > 0; + for (int i=0 ; i < size; i++) { + if (limit <= pos) return -1L; + res = (res << 8) | (long) (buffers.get(pos++) & 0xFF); + } + return res; + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(ByteBuffer buffer, int offset) { + return peekEncodedValueSize(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffers the buffers to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(BuffersReader buffers, long offset) { + long limit = buffers.limit(); + long pos = offset; + if (limit <= pos) return -1; + int first = buffers.get(pos); + int prefix = (first & 0xC0) >>> 6; + int size = 1 << prefix; + assert size > 0 && size <= 8; + return size; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java new file mode 100644 index 00000000000..7983d1be4f0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java @@ -0,0 +1,931 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.LongConsumer; +import java.util.stream.LongStream; +import java.util.stream.StreamSupport; + +/** + * An ACK Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class AckFrame extends QuicFrame { + + private final long largestAcknowledged; + private final long ackDelay; + private final int ackRangeCount; + private final List ackRanges; + + private final boolean countsPresent; + private final long ect0Count; + private final long ect1Count; + private final long ecnCECount; + private final int size; + + private static final int COUNTS_PRESENT = 0x1; + + /** + * Reads an {@code AckFrame} from the given buffer. When entering + * this method the buffer position is supposed to be just past + * after the frame type. That, is the frame type has already + * been read. This method moves the position of the buffer to the + * first byte after the read ACK frame. + * @param buffer a buffer containing the ACK frame + * @param type the frame type read from the buffer + * @throws QuicTransportException if the ACK frame was malformed + */ + AckFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(ACK); + int pos = buffer.position(); + largestAcknowledged = decodeVLField(buffer, "largestAcknowledged"); + ackDelay = decodeVLField(buffer, "ackDelay"); + ackRangeCount = decodeVLFieldAsInt(buffer, "ackRangeCount"); + long firstAckRange = decodeVLField(buffer, "firstAckRange"); + long smallestAcknowledged = largestAcknowledged - firstAckRange; + if (smallestAcknowledged < 0) { + throw new QuicTransportException("Negative PN acknowledged", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + var ackRanges = new ArrayList(ackRangeCount + 1); + AckRange first = AckRange.of(0, firstAckRange); + ackRanges.add(0, first); + for (int i=1; i <= ackRangeCount; i++) { + long gap = decodeVLField(buffer, "gap"); + long len = decodeVLField(buffer, "range length"); + ackRanges.add(i, AckRange.of(gap, len)); + smallestAcknowledged -= gap + len + 2; + if (smallestAcknowledged < 0) { + // verify after each range to avoid wrap around + throw new QuicTransportException("Negative PN acknowledged", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + this.ackRanges = List.copyOf(ackRanges); + if (type % 2 == 1) { + // packet contains ECN counts + countsPresent = true; + ect0Count = decodeVLField(buffer, "ect0Count"); + ect1Count = decodeVLField(buffer, "ect1Count"); + ecnCECount = decodeVLField(buffer, "ecnCECount"); + } else { + countsPresent = false; + ect0Count = -1; + ect1Count = -1; + ecnCECount = -1; + } + size = computeSize(); + int wireSize = buffer.position() - pos + getVLFieldLengthFor(getTypeField()); + assert size <= wireSize : "parsed: %s, computed size: %s" + .formatted(wireSize, size); + } + + /** + * Creates the short formed ACK frame with no count totals + */ + public AckFrame(long largestAcknowledged, long ackDelay, List ackRanges) + { + this(largestAcknowledged, ackDelay, ackRanges, -1, -1, -1); + } + + /** + * Creates the long formed ACK frame with count totals + */ + public AckFrame( + long largestAcknowledged, + long ackDelay, + List ackRanges, + long ect0Count, + long ect1Count, + long ecnCECount) + { + super(ACK); + this.largestAcknowledged = requireVLRange(largestAcknowledged, "largestAcknowledged"); + this.ackDelay = requireVLRange(ackDelay, "ackDelay"); + if (ackRanges.size() < 1) { + throw new IllegalArgumentException("insufficient ackRanges"); + } + if (ackRanges.get(0).gap() != 0) { + throw new IllegalArgumentException("first range must have zero gap"); + } + this.ackRanges = List.copyOf(ackRanges); + this.ackRangeCount = ackRanges.size() - 1; + this.countsPresent = ect0Count != -1 || ect1Count != -1 || ecnCECount != -1; + if (countsPresent) { + this.ect0Count = requireVLRange(ect0Count,"ect0Count"); + this.ect1Count = requireVLRange(ect1Count, "ect1Count"); + this.ecnCECount = requireVLRange(ecnCECount, "ecnCECount"); + } else { + this.ect0Count = ect0Count; + this.ect1Count = ect1Count; + this.ecnCECount = ecnCECount; + } + this.size = computeSize(); + } + + @Override + public long getTypeField() { + return ACK | (countsPresent ? COUNTS_PRESENT : 0); + } + + @Override + public boolean isAckEliciting() { return false; } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, largestAcknowledged, "largestAcknowledged"); + encodeVLField(buffer, ackDelay, "ackDelay"); + encodeVLField(buffer, ackRangeCount, "ackRangeCount"); + encodeVLField(buffer, ackRanges.get(0).range(), "firstAckRange"); + for (int i=1; i <= ackRangeCount; i++) { + AckRange ar = ackRanges.get(i); + encodeVLField(buffer, ar.gap(), "gap"); + encodeVLField(buffer, ar.range(), "range"); + } + if (countsPresent) { + // encode the counts + encodeVLField(buffer, ect0Count, "ect0Count"); + encodeVLField(buffer, ect1Count, "ect1Count"); + encodeVLField(buffer, ecnCECount, "ecnCECount"); + } + assert buffer.position() - pos == size(); + } + + private int computeSize() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(largestAcknowledged) + + getVLFieldLengthFor(ackDelay) + + getVLFieldLengthFor(ackRangeCount) + + getVLFieldLengthFor(ackRanges.get(0).range()) + + ackRanges.stream().skip(1).mapToInt(AckRange::size).sum(); + if (countsPresent) { + size = size + getVLFieldLengthFor(ect0Count) + + getVLFieldLengthFor(ect1Count) + + getVLFieldLengthFor(ecnCECount); + } + return size; + } + + @Override + public int size() { return size; } + + /** + * {@return largest packet number acknowledged by this frame} + */ + public long largestAcknowledged() { + return largestAcknowledged; + } + + /** + * The ACK delay + */ + public long ackDelay() { + return ackDelay; + } + + /** + * {@return the number of ack ranges} + * This corresponds to {@link #ackRanges() ackRange.size() -1}. + */ + public long ackRangeCount() { + return ackRangeCount; + } + + /** + * {@return a new {@code AckFrame} identical to this one, but + * with the given {@code ackDelay}}; + * @param ackDelay + */ + public AckFrame withAckDelay(long ackDelay) { + if (ackDelay == this.ackDelay) return this; + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + /** + * An ACK range, composed of a gap and a range. + */ + public record AckRange(long gap, long range) { + public static final AckRange INITIAL = new AckRange(0, 0); + public AckRange { + requireVLRange(gap, "gap"); + requireVLRange(range, "range"); + } + public int size() { + return getVLFieldLengthFor(gap) + getVLFieldLengthFor(range); + } + public static AckRange of(long gap, long range) { + if (gap == 0 && range == 0) return INITIAL; + return new AckRange(gap, range); + } + } + + /** + * The ack ranges. First element is an actual range relative + * to highest acknowledged packet number. Second (if present) + * is a gap and a range following that gap, and so on until the last. + * @return the list of {@code AckRange} where the first ack range + * has a gap of {@code 0} and a range corresponding to + * the {@code First ACK Range}. + */ + public List ackRanges() { + return ackRanges; + } + + /** + * {@return the ECT0 count from this frame or -1 if not present} + */ + public long ect0Count() { + return ect0Count; + } + + /** + * {@return the ECT1 count from this frame or -1 if not present} + */ + public long ect1Count() { + return ect1Count; + } + + /** + * {@return the ECN-CE count from this frame or -1 if not present} + */ + public long ecnCECount() { + return ecnCECount; + } + + /** + * {@return true if this frame contains an acknowledgment for the + * given packet number} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + return isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return true if the given range is acknowledged by this frame} + * @param first the first packet in the range, inclusive + * @param last the last packet in the range, inclusive + */ + public boolean isRangeAcknowledged(long first, long last) { + return isRangeAcknowledged(largestAcknowledged, ackRanges, first, last); + } + + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + return smallestAcknowledged(largestAcknowledged, ackRanges); + } + + /** + * @return a stream of packet numbers acknowledged by this frame + */ + public LongStream acknowledged() { + return StreamSupport.longStream(new AckFrameSpliterator(this), false); + } + + + private static class AckFrameSpliterator implements Spliterator.OfLong { + + final AckFrame ackFrame; + + AckFrameSpliterator(AckFrame ackFrame) { + this.ackFrame = ackFrame; + this.largest = ackFrame.largestAcknowledged(); + this.smallest = largest + 2; + this.ackRangeIterator = ackFrame.ackRanges.iterator(); + } + + @Override + public long estimateSize() { + // It is costly to compute an estimate, so we just + // return Long.MAX_VALUE instead + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + // NONNULL - nulls are not expected to be returned by this long spliterator + // IMMUTABLE - ackFrame.ackRanges() returns unmodifiable list, which cannot be + // structurally modified + return NONNULL | IMMUTABLE; + } + + @Override + public OfLong trySplit() { + // null - this spliterator cannot be split + return null; + } + private final Iterator ackRangeIterator; + private volatile long largest; + private volatile long smallest; + private volatile long pn; // the current packet number + + // The stream returns packet number in decreasing order + // (largest packet number is returned first) + private boolean ackAndDecId(LongConsumer action) { + assert ackFrame.isAcknowledging(pn) + : "%s is not acknowledging %s".formatted(ackFrame, pn); + action.accept(pn--); + return true; + } + + @Override + public boolean tryAdvance(LongConsumer action) { + // First call will see pn == 0 and smallest >= 2, + // which guarantees we will not enter the if below + // before pn has been initialized from the + // first ackRange value + if (pn >= smallest) { + return ackAndDecId(action); + } + if (ackRangeIterator.hasNext()) { + var ackRange = ackRangeIterator.next(); + largest = smallest - ackRange.gap() - 2; + smallest = largest - ackRange.range; + pn = largest; + return ackAndDecId(action); + } + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + return o instanceof AckFrame ackFrame + && largestAcknowledged == ackFrame.largestAcknowledged + && ackDelay == ackFrame.ackDelay + && ackRangeCount == ackFrame.ackRangeCount + && countsPresent == ackFrame.countsPresent + && ect0Count == ackFrame.ect0Count + && ect1Count == ackFrame.ect1Count + && ecnCECount == ackFrame.ecnCECount + && ackRanges.equals(ackFrame.ackRanges); + } + + @Override + public int hashCode() { + return Objects.hash(largestAcknowledged, ackDelay, + ackRanges, ect0Count, ect1Count, ecnCECount); + } + + @Override + public String toString() { + String res = "AckFrame(" + + "largestAcknowledged=" + largestAcknowledged + + ", ackDelay=" + ackDelay + + ", ackRanges=[" + prettyRanges() + "]"; + if (countsPresent) res = res + + ", ect0Count=" + ect0Count + + ", ect1Count=" + ect1Count + + ", ecnCECount=" + ecnCECount; + res += ")"; + return res; + } + + private String prettyRanges() { + String result = null; + long largest; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + result = smallest + ".." + largest + (result != null ? ", "+result : ""); + } + return result; + } + + /** + * {@return the largest packet acknowledged by an + * {@link QuicFrame#ACK ACK} frame contained in the + * given packet, or {@code -1L} if the packet + * contains no {@code ACK} frame} + * @param packet a packet that may contain an {@code ACK} frame + */ + public static long largestAcknowledgedInPacket(QuicPacket packet) { + return packet.frames().stream() + .filter(AckFrame.class::isInstance) + .map(AckFrame.class::cast) + .mapToLong(AckFrame::largestAcknowledged) + .max().orElse(-1L); + } + + /** + * A builder that allows to incrementally build the AckFrame + * that will need to be sent, as new packets are received. + * This class is not MT-thread safe. + */ + public static final class AckFrameBuilder { + long largestAckAcked = -1; + long largestAcknowledged = -1; + long ackDelay = 0; + List ackRanges = new ArrayList<>(); + long ect0Count = -1; + long ect1Count = -1; + long ecnCECount = -1; + + /** + * An empty builder. + */ + public AckFrameBuilder() {} + + /** + * A builder initialize from the content of an AckFrame. + * @param frame the {@code AckFrame} to initialize this builder with. + * Must not be {@code null}. + */ + public AckFrameBuilder(AckFrame frame) { + largestAckAcked = -1; + largestAcknowledged = frame.largestAcknowledged; + ackDelay = frame.ackDelay; + ackRanges.addAll(frame.ackRanges); + ect0Count = frame.ect0Count; + ect1Count = frame.ect1Count; + ecnCECount = frame.ecnCECount; + } + + public long getLargestAckAcked() { + return largestAckAcked; + } + + /** + * Drops all acks for packet whose number is smaller + * than the given {@code largestAckAcked}. + * @param largestAckAcked the smallest packet number that + * should be acknowledged by this + * {@link AckFrame}. + * @return this builder + */ + public AckFrameBuilder dropAcksBefore(long largestAckAcked) { + if (largestAckAcked > this.largestAckAcked) { + this.largestAckAcked = largestAckAcked; + return dropIfSmallerThan(largestAckAcked); + } else { + this.largestAckAcked = largestAckAcked; + } + return this; + } + + /** + * Drops all instances of {@link AckRange} after the given + * index in the {@linkplain #ackRanges() Ack Range List}, and compute + * the new smallest packet number now acknowledged by this + * {@link AckFrame}: this computed packet number will then be + * returned by {@link #getLargestAckAcked()}. + * This is a no-op if index is greater or equal to + * {@code ackRanges().size() -1}. + * @param index the index after which ranges should be dropped. + * @return this builder + */ + public AckFrameBuilder dropAckRangesAfter(int index) { + if (index < 0) { + throw new IllegalArgumentException("invalid index %s for size %s" + .formatted(index, ackRanges.size())); + } + if (index >= ackRanges.size() - 1) return this; + long newLargestAckAcked = dropRangesIfAfter(index); + assert newLargestAckAcked > largestAckAcked; + largestAckAcked = newLargestAckAcked; + return this; + } + + /** + * Sets the ack delay. + * @param ackDelay the ack delay. + * @return this builder. + */ + public AckFrameBuilder ackDelay(long ackDelay) { + this.ackDelay = ackDelay; + return this; + } + + /** + * Sets the ect0Count. Passing -1 unsets the ectOcount. + * @param ect0Count the ect0Count + * @return this builder. + */ + public AckFrameBuilder ect0Count(long ect0Count) { + this.ect0Count = ect0Count; + return this; + } + + /** + * Sets the ect1Count. Passing -1 unsets the ect1count. + * @param ect1Count the ect1Count + * @return this builder. + */ + public AckFrameBuilder ect1Count(long ect1Count) { + this.ect1Count = ect1Count; + return this; + } + + /** + * Sets the ecnCECount. Passing -1 unsets the ecnCEOcount. + * @param ecnCECount the ecnCECount + * @return this builder. + */ + public AckFrameBuilder ecnCECount(long ecnCECount) { + this.ecnCECount = ecnCECount; + return this; + } + + /** + * Adds the given packet number to the list of ack ranges. + * If the packet is already being acknowledged by this frame, + * do nothing. + * @param packetNumber the packet number + * @return this builder + */ + public AckFrameBuilder addAck(long packetNumber) { + // check if we need to acknowledge this packet + if (packetNumber <= largestAckAcked) return this; + // System.out.println("adding " + packetNumber); + if (ackRanges.isEmpty()) { + // easy case: we only have one packet to acknowledge! + return acknowledgeFirstPacket(packetNumber); + } else if (packetNumber > largestAcknowledged) { + return acknowledgeLargerPacket(packetNumber); + } else if (packetNumber < largestAcknowledged) { + // now is the complex case: we need to find out: + // - whether this packet is already acknowledged, in which case, + // there is nothing to do (great) + // - or whether we can extend an existing range + // - or whether we need to create a new range (if the packet falls + // within a gap whose value is > 0). + // - or whether we should merge two ranges if the packet falls + // on a gap whose value is 0 + ListIterator iterator = ackRanges.listIterator(); + long largest = largestAcknowledged; + long smallest = largest + 2; + int index = -1; + while (iterator.hasNext()) { + var ackRange = iterator.next(); + // index of the current ackRange element + index++; + // largest packet number acknowledged by this ackRange + largest = smallest - ackRange.gap - 2; + // smallest packet number acknowledged by this ackRange + smallest = largest - ackRange.range; + + // if the packet number we want to acknowledge is greater + // than the largest packet acknowledged by this ackRange + // there are two cases: + if (packetNumber > largest) { + // the packet number is just above the largest packet + if (packetNumber -1 == largest) { + // the current ackRange must have a gap, and we can simply + // reduce that gap by 1, and extend the range by 1. + // the case where the current ackrange doesn't have a gap + // and the packet number is the largest + 1 should have + // been handled when processing the previous ackRange. + assert ackRange.gap > 0; + var gap = ackRange.gap - 1; + var range = ackRange.range + 1; + var replaced = AckRange.of(gap, range); + ackRanges.set(index, replaced); + return this; + } else { + // the packet falls within the gap of this ack range. + // we need to split the ackRange in two... + // + // in the case where we have + // [31,31] [27,27] -> 31, AckRange[g=0, r=0], AckRange[g=2, r=0] + // and we want to acknowledge 29. + // we should end up with: + // [31,31] [29,29] [27,27] -> + // 31, AckRange[g=0, r=0], AckRange[g=0, r=0], AckRange[g=0, r=0] + assert ackRange.gap > 0 : "%s at index (prev:%s, next:%s)" + .formatted(ackRanges, iterator.previousIndex(), iterator.nextIndex()); + assert packetNumber - ackRange.gap -2 <= largest; + + // compute the smallest packet that was acknowledged by the + // previous ackRange. This should be: + var previousSmallest = largest + ackRange.gap + 2; + + // System.out.printf("ack: %s, largest:%s, previousSmallest:%d%n", + // ackRange, largest, previousSmallest); + + // compute the point at which we should split the current ackRange + // the current ackRange will be split in two: first, and second + // - first will replace the current ackRange + // - second will be inserted after first + var firstgap = previousSmallest - packetNumber -2; + AckRange first = AckRange.of(firstgap, 0); + AckRange second = AckRange.of(ackRange.gap - firstgap -2, ackRange.range); + ackRanges.set(index, first); + iterator.add(second); + return this; + } + } else if (packetNumber < smallest) { + // otherwise, if the packet number is smaller than + // the smallest packet acknowledged by the current ackRange, + // there are two cases: + + // If the current ackRange is the last: it's simple! + // But there are again two cases: + if (!iterator.hasNext()) { + // If the packet number we want to acknowledge is just below + // the smallest packet number acknowledge by the current + // ackRange, there is no gap between the packet number and + // the current range, so we can simply extend the current range + // Otherwise, we need to append a new ackRange. + if (packetNumber == smallest - 1) { + // no gap: we can extend the current range + AckRange replaced = AckRange + .of(ackRange.gap, ackRange.range + 1); + ackRanges.set(index, replaced); + } else { + // gap: we need to add a new AckRange + AckRange last = AckRange.of(smallest - packetNumber - 2, 0); + iterator.add(last); + } + return this; + } else if (packetNumber == smallest - 1) { + // Otherwise, if the packet number to be acknowledged is + // just below the smallest packet ackowledged by the current + // range, there are again two cases, depending on + // whether the next ackRange has a gap that can be reduced, + // or not + assert iterator.hasNext(); + AckRange next = ackRanges.get(index + 1); + // if the gap of the next packet can be reduced, that's great! + // just do it! We need to reduce that gap by one, and extend + // the range of the current ackRange + if (next.gap > 0) { + // reduce the gap in the next ackrange, and increase + // the range in the current ackrange. + // System.out.printf("ack: %s, next: %s%n", ackRange, next); + AckRange first = AckRange.of(ackRange.gap, ackRange.range + 1); + AckRange second = AckRange.of(next.gap - 1, next.range); + // System.out.printf("first: %s, second: %s%n", first, second); + ackRanges.set(index, first); + ackRanges.set(index + 1, second); + return this; + } else { + // Otherwise, that's the complex case again. + // we have a gap of 1 packet between 2 ackranges. + // our packet number falls exactly in that gap. + // We need to merge the two ranges! + // merge with next ackRange: remove the current ackRange, + // the ackRange at the current index is now the next ackRange, + // replace it with a merged ACK range. + var mergedRanges = ackRange.range + next.range + 2; + iterator.remove(); + ackRanges.set(index, AckRange.of(ackRange.gap, mergedRanges)); + return this; + } + } + } else { + // Otherwise, the packet is already acknowledged! + // nothing to do. + assert packetNumber <= largest && packetNumber >= smallest; + return this; + } + } + } else { + // already acknowledged! + assert packetNumber == largestAcknowledged; + return this; + } + return this; + } + + /** + * {@return true if this builder contains no ACK yet} + */ + public boolean isEmpty() { + return ackRanges.isEmpty(); + } + + /** + * {@return the number of ACK ranges in this builder, including the fake + * first ACK range} + */ + public int length() { + return ackRanges.size(); + } + + /** + * {@return true if the given packet number is already acknowledged + * by this builder} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + if (isEmpty()) return false; + return AckFrame.isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + if (largestAcknowledged == -1L) return -1L; + return AckFrame.smallestAcknowledged(largestAcknowledged, ackRanges); + } + + // drop acknowledgement of all packet numbers acknowledged + // by AckRange instances coming after the given index, and + // return the smallest packet number now acked by this + // AckFrame. + private long dropRangesIfAfter(int ackIndex) { + assert ackIndex > 0 && ackIndex < ackRanges.size(); + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + long newLargestAckAcked = -1; + while (iterator.hasNext()) { + if (index == ackIndex) { + newLargestAckAcked = smallest; + removeRemainings = true; + } + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return newLargestAckAcked; + } + + + // drop acknowledgement of all packet numbers less or equal + // to `largestAckAcked; + private AckFrameBuilder dropIfSmallerThan(long largestAckAcked) { + if (largestAckAcked >= largestAcknowledged) { + largestAcknowledged = -1; + ackRanges.clear(); + return this; + } + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + while (iterator.hasNext()) { + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + if (largest <= largestAckAcked) { + iterator.remove(); + removeRemainings = true; + } else if (smallest <= largestAckAcked) { + long removed = largestAckAcked - smallest + 1; + long gap = ackRange.gap; + long range = ackRange.range - removed; + assert gap >= 0; + assert range >= 0; + ackRanges.set(index, new AckRange(gap, range)); + removeRemainings = true; + } + } + return this; + } + + /** + * Builds an {@code AckFrame} from this builder's content. + * @return a new {@code AckFrame}. + */ + public AckFrame build() { + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + private AckFrameBuilder acknowledgeFirstPacket(long packetNumber) { + assert ackRanges.isEmpty(); + largestAcknowledged = packetNumber; + ackRanges.add(AckRange.INITIAL); + return this; + } + + private AckFrameBuilder acknowledgeLargerPacket(long largerThanLargest) { + var packetNumber = largerThanLargest; + // the new packet is larger than the largest acknowledged + var firstAckRange = ackRanges.get(0); + if (largestAcknowledged == packetNumber -1) { + // if packetNumber is largestAcknowledged + 1, we can simply + // extend the first ack range by 1 + firstAckRange = AckRange.of(0, firstAckRange.range + 1); + ackRanges.set(0, firstAckRange); + } else { + // otherwise - we have a gap - we need to acknowledge the new packetNumber, + // and then add the gap that separate it from the previous largestAcknowledged... + ackRanges.add(0, AckRange.INITIAL); // acknowledge packetNumber only + long gap = packetNumber - largestAcknowledged -2; + var secondAckRange = AckRange.of(gap, firstAckRange.range); + ackRanges.set(1, secondAckRange); // add the gap + } + largestAcknowledged = packetNumber; + return this; + } + + public static AckFrameBuilder ofNullable(AckFrame frame) { + return frame == null ? new AckFrameBuilder() : new AckFrameBuilder(frame); + } + + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static boolean isAcknowledging(long largestAcknowledged, + List ackRanges, + long packetNumber) { + if (packetNumber > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (packetNumber > largest) return false; + smallest = largest - ackRange.range; + if (packetNumber >= smallest) return true; + } + return false; + } + + private static boolean isRangeAcknowledged(long largestAcknowledged, + List ackRanges, + long first, + long last) { + assert last >= first; + if (last > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (last > largest) return false; + smallest = largest - ackRange.range; + if (first >= smallest) return true; + } + return false; + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static long smallestAcknowledged(long largestAcknowledged, + List ackRanges) { + long largest = largestAcknowledged; + long smallest = largest + 2; + assert !ackRanges.isEmpty(); + for (AckRange ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return smallest; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java new file mode 100644 index 00000000000..5e0f2abd8df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * A CONNECTION_CLOSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ConnectionCloseFrame extends QuicFrame { + + /** + * This variant indicates an error originating from the higher + * level protocol, for instance, HTTP/3. + */ + public static final int CONNECTION_CLOSE_VARIANT = 0x1d; + private final long errorCode; + private final long errorFrameType; + private final boolean variant; + private final byte[] reason; + private String cachedToString; + private String cachedReason; + + /** + * An immutable ConnectionCloseFrame of type 0x1c with no reason phrase + * and an error of type APPLICATION_ERROR. + * @apiNote + * From + * RFC 9000 - section 10.2.3: + *

      + * A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + * of type 0x1c when sending the frame in Initial or Handshake packets. + * Otherwise, information about the application state might be revealed. + * Endpoints MUST clear the value of the Reason Phrase field and SHOULD + * use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + * of type 0x1c. + *
      + */ + public static final ConnectionCloseFrame APPLICATION_ERROR = + new ConnectionCloseFrame(QuicTransportErrors.APPLICATION_ERROR.code(), 0,""); + + /** + * Incoming CONNECTION_CLOSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + ConnectionCloseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CONNECTION_CLOSE); + errorCode = decodeVLField(buffer, "errorCode"); + if (type == CONNECTION_CLOSE) { + variant = false; + errorFrameType = decodeVLField(buffer, "errorFrameType"); + } else { + assert type == CONNECTION_CLOSE_VARIANT; + errorFrameType = -1; + variant = true; + } + int reasonLength = decodeVLFieldAsInt(buffer, "reasonLength"); + validateRemainingLength(buffer, reasonLength, type); + reason = new byte[reasonLength]; + buffer.get(reason, 0, reasonLength); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant with errorFrameType - 0x1c). + * This indicates a {@linkplain jdk.internal.net.quic.QuicTransportErrors + * quic transport error}. + */ + public ConnectionCloseFrame(long errorCode, long errorFrameType, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = requireVLRange(errorFrameType, "errorFrameType"); + this.variant = false; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant without errorFrameType). + * This indicates an error originating from the higher level protocol, + * for instance {@linkplain jdk.internal.net.http.http3.Http3Error HTTP/3}. + */ + public ConnectionCloseFrame(long errorCode, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = -1; + this.variant = true; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + private static byte[] getReasonBytes(String reason) { + return reason != null ? + reason.getBytes(StandardCharsets.UTF_8) : + new byte[0]; + } + + /** + * {@return a ConnectionCloseFrame suitable for inclusion in + * an Initial or Handshake packet} + */ + public ConnectionCloseFrame clearApplicationState() { + return this.variant ? APPLICATION_ERROR : this; + } + + @Override + public long getTypeField() { + return variant ? CONNECTION_CLOSE_VARIANT : CONNECTION_CLOSE; + } + + @Override + public boolean isAckEliciting() { + return false; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, errorCode, "errorCode"); + if (!variant) { + encodeVLField(buffer, errorFrameType, "errorFrameType"); + } + encodeVLField(buffer, reason.length, "reasonLength"); + if (reason.length > 0) { + buffer.put(reason); + } + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(errorCode) + + (variant ? 0 : getVLFieldLengthFor(errorFrameType)) + + getVLFieldLengthFor(reason.length) + + reason.length; + } + + public long errorCode() { + return errorCode; + } + + public long errorFrameType() { + return errorFrameType; + } + + public boolean variant() { + return variant; + } + + public boolean isQuicTransportCode() { + return !variant; + } + + public boolean isApplicationCode() { + return variant; + } + + public byte[] reason() { + return reason; + } + + public String reasonString() { + if (cachedReason != null) return cachedReason; + if (reason == null) return null; + if (reason.length == 0) return ""; + return cachedReason = new String(reason, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + if (cachedToString == null) { + final StringBuilder sb = new StringBuilder("ConnectionCloseFrame[type=0x"); + final long type = getTypeField(); + sb.append(Long.toHexString(type)) + .append(", errorCode=0x").append(Long.toHexString(errorCode)); + // CRYPTO_ERROR codes ranging 0x0100-0x01ff + if (type == 0x1c) { + if (errorCode >= 0x0100 && errorCode <= 0x01ff) { + // this represents a CRYPTO_ERROR which as per RFC-9001, section 4.8: + // A TLS alert is converted into a QUIC connection error. The AlertDescription + // value is added to 0x0100 to produce a QUIC error code from the range reserved for + // CRYPTO_ERROR; ... The resulting value is sent in a QUIC CONNECTION_CLOSE + // frame of type 0x1c + + // find the tls alert code from the error code, by substracting 0x0100 from + // the error code + sb.append(", tlsAlertDescription=").append(errorCode - 0x0100); + } + sb.append(", errorFrameType=0x").append(Long.toHexString(errorFrameType)); + } + if (cachedReason == null) { + cachedReason = new String(reason, StandardCharsets.UTF_8); + } + sb.append(", reason=").append(cachedReason).append("]"); + + cachedToString = sb.toString(); + } + return cachedToString; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java new file mode 100644 index 00000000000..7b606527193 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A CRYPTO Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class CryptoFrame extends QuicFrame { + + private final long offset; + private final int length; + private final ByteBuffer cryptoData; + + CryptoFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CRYPTO); + offset = decodeVLField(buffer, "offset"); + length = decodeVLFieldAsInt(buffer, "length"); + if (offset + length > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new QuicTransportException("Maximum crypto offset exceeded", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + // The buffer is the datagram: we will make a copy if the datagram + // is larger than the crypto frame by 64 bytes. + cryptoData = Utils.sliceOrCopy(buffer, pos, length, 64); + buffer.position(pos + length); + } + + /** + * Creates CryptoFrame + */ + public CryptoFrame(long offset, int length, ByteBuffer cryptoData) { + this(offset, length, cryptoData, true); + } + + private CryptoFrame(long offset, int length, ByteBuffer cryptoData, boolean slice) + { + super(CRYPTO); + this.offset = requireVLRange(offset, "offset"); + if (length != cryptoData.remaining()) + throw new IllegalArgumentException("bad length: " + length); + this.length = length; + this.cryptoData = slice + ? cryptoData.slice(cryptoData.position(), length) + : cryptoData; + } + + /** + * Creates a new CryptoFrame which is a slice of this crypto frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current crypto frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this crypto frame + */ + public CryptoFrame slice(long offset, int length) { + long offsetdiff = offset - offset(); + long oldlen = length(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = cryptoData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + ByteBuffer slice = Utils.sliceOrCopy(cryptoData, newpos, length); + return new CryptoFrame(offset, length, slice, false); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, CRYPTO, "type"); + encodeVLField(dest, offset, "offset"); + encodeVLField(dest, length, "length"); + assert cryptoData.remaining() == length; + putByteBuffer(dest, cryptoData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(CRYPTO) + + getVLFieldLengthFor(offset) + + getVLFieldLengthFor(length) + + length; + } + + /** + * {@return the frame offset} + */ + public long offset() { + return offset; + } + + public int length() { + return length; + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return cryptoData.slice(); + } + + @Override + public String toString() { + return "CryptoFrame(" + + "offset=" + offset + + ", length=" + length + + ')'; + } + + public static int compareOffsets(CryptoFrame cf1, CryptoFrame cf2) { + return Long.compare(cf1.offset, cf2.offset); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java new file mode 100644 index 00000000000..b008e643cda --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class DataBlockedFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + DataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(DATA_BLOCKED); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing DATA_BLOCKED frame + */ + public DataBlockedFrame(long maxData) { + super(DATA_BLOCKED); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, DATA_BLOCKED, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(DATA_BLOCKED) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "DataBlockedFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java new file mode 100644 index 00000000000..ffe6aff2f0d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A HANDSHAKE_DONE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class HandshakeDoneFrame extends QuicFrame { + + /** + * Incoming HANDSHAKE_DONE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + */ + HandshakeDoneFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(HANDSHAKE_DONE); + } + + /** + * Outgoing HANDSHAKE_DONE frame + */ + public HandshakeDoneFrame() { + super(HANDSHAKE_DONE); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, HANDSHAKE_DONE, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(HANDSHAKE_DONE); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java new file mode 100644 index 00000000000..26720c34494 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxDataFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming MAX_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_DATA); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_DATA frame + */ + public MaxDataFrame(long maxData) { + super(MAX_DATA); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_DATA, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_DATA) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "MaxDataFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java new file mode 100644 index 00000000000..3fff70a377c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM_DATA Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamDataFrame extends QuicFrame { + + private final long streamID; + private final long maxStreamData; + + /** + * Incoming MAX_STREAM_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAM_DATA); + streamID = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_STREAM_DATA frame + */ + public MaxStreamDataFrame(long streamID, long maxStreamData) { + super(MAX_STREAM_DATA); + this.streamID = requireVLRange(streamID, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_STREAM_DATA, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamID() { + return streamID; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAM_DATA) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "MaxStreamDataFrame(" + + "streamId=" + streamID + + ", maxStreamData=" + maxStreamData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java new file mode 100644 index 00000000000..e35b16195a6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamsFrame extends QuicFrame { + static final long MAX_VALUE = 1L << 60; + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming MAX_STREAMS frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamsFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAMS); + bidi = (type == MAX_STREAMS); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing MAX_STREAMS frame + */ + public MaxStreamsFrame(boolean bidi, long maxStreams) { + super(MAX_STREAMS); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return MAX_STREAMS + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAMS) + + getVLFieldLengthFor(maxStreams); + } + + @Override + public String toString() { + return "MaxStreamsFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java new file mode 100644 index 00000000000..87e91b21e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A NEW_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + private final long retirePriorTo; + private final ByteBuffer connectionId; + private final ByteBuffer statelessResetToken; + + /** + * Incoming NEW_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + retirePriorTo = decodeVLField(buffer, "retirePriorTo"); + if (retirePriorTo > sequenceNumber) { + throw new QuicTransportException("Invalid retirePriorTo", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, 17, type); + int length = Byte.toUnsignedInt(buffer.get()); + if (length < 1 || length > 20) { + throw new QuicTransportException("Invalid connection ID", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length + 16, type); + int position = buffer.position(); + connectionId = buffer.slice(position, length); + position += length; + statelessResetToken = buffer.slice(position, 16); + position += 16; + buffer.position(position); + } + + /** + * Outgoing NEW_CONNECTION_ID frame + */ + public NewConnectionIDFrame(long sequenceNumber, long retirePriorTo, ByteBuffer connectionId, ByteBuffer statelessResetToken) { + super(NEW_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + this.retirePriorTo = requireVLRange(retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + if (length < 1 || length > 20) + throw new IllegalArgumentException("invalid length"); + this.connectionId = connectionId.slice(); + if (statelessResetToken.remaining() != 16) + throw new IllegalArgumentException("stateless reset token must be 16 bytes"); + this.statelessResetToken = statelessResetToken.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + encodeVLField(buffer, retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + buffer.put((byte)length); + putByteBuffer(buffer, connectionId); + putByteBuffer(buffer, statelessResetToken); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(NEW_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber) + + getVLFieldLengthFor(retirePriorTo) + + 1 // connection length + + connectionId.remaining() + + statelessResetToken.remaining(); + } + + public long sequenceNumber() { + return sequenceNumber; + } + + public long retirePriorTo() { + return retirePriorTo; + } + + public ByteBuffer connectionId() { + return connectionId; + } + + public ByteBuffer statelessResetToken() { + return statelessResetToken; + } + + @Override + public String toString() { + return "NewConnectionIDFrame(seqNumber=" + sequenceNumber + + ", retirePriorTo=" + retirePriorTo + + ", connIdLength=" + connectionId.remaining() + + ")"; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java new file mode 100644 index 00000000000..08bc72ff1c2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A NEW_TOKEN frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewTokenFrame extends QuicFrame { + private final byte[] token; + + /** + * Incoming NEW_TOKEN frame + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewTokenFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_TOKEN); + int length = decodeVLFieldAsInt(buffer, "token length"); + if (length == 0) { + throw new QuicTransportException("Empty token", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + final byte[] t = new byte[length]; + buffer.get(t); + this.token = t; + } + + /** + * Outgoing NEW_TOKEN frame whose token is the given ByteBuffer + * (position to limit) + */ + public NewTokenFrame(final ByteBuffer tokenBuf) { + super(NEW_TOKEN); + Objects.requireNonNull(tokenBuf); + final int length = tokenBuf.remaining(); + if (length <= 0) { + throw new IllegalArgumentException("Invalid token length"); + } + final byte[] t = new byte[length]; + tokenBuf.get(t); + this.token = t; + } + + @Override + public void encode(final ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_TOKEN, "type"); + encodeVLField(buffer, token.length, "token length"); + buffer.put(token); + assert buffer.position() - pos == size(); + } + + public byte[] token() { + return this.token; + } + + @Override + public int size() { + final int tokenLength = token.length; + return getVLFieldLengthFor(NEW_TOKEN) + + getVLFieldLengthFor(tokenLength) + + tokenLength; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java new file mode 100644 index 00000000000..d0258aee84e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * PaddingFrames. Since padding frames comprise a single zero byte + * this class actually represents sequences of PaddingFrames. + * When decoding, the class consumes all the zero bytes that are + * available and when encoding, the number of required padding bytes + * is specified. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PaddingFrame extends QuicFrame { + + private final int size; + + /** + * Incoming + */ + PaddingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PADDING); + int count = 1; + while (buffer.hasRemaining()) { + if (buffer.get() == 0) { + count++; + } else { + int pos = buffer.position(); + buffer.position(pos - 1); + break; + } + } + size = count; + } + + /** + * Outgoing + * @param size the number of padding frames that should be written + * to the buffer. Each frame is one byte long. + */ + public PaddingFrame(int size) { + super(PADDING); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than zero"); + } + this.size = size; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + for (int i=0; i buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_CHALLENGE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_CHALLENGE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java new file mode 100644 index 00000000000..2b1edcfe731 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PATH_RESPONSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PathResponseFrame extends QuicFrame { + + public static final int LENGTH = 8; + private final ByteBuffer data; + + /** + * Incoming PATH_RESPONSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + PathResponseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PATH_RESPONSE); + validateRemainingLength(buffer, LENGTH, type); + int position = buffer.position(); + data = buffer.slice(position, LENGTH); + buffer.position(position + LENGTH); + } + + /** + * Outgoing PATH_RESPONSE frame + */ + public PathResponseFrame(ByteBuffer data) { + super(PATH_RESPONSE); + if (data.remaining() != LENGTH) + throw new IllegalArgumentException("response data must be 8 bytes"); + this.data = data.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_RESPONSE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_RESPONSE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java new file mode 100644 index 00000000000..9717a3c31d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PING frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PingFrame extends QuicFrame { + + PingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PING); + } + + /** + */ + public PingFrame() { + super(PING); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PING, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(PING); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java new file mode 100644 index 00000000000..f4a230452d4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A QUIC Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public abstract sealed class QuicFrame permits + AckFrame, + DataBlockedFrame, + ConnectionCloseFrame, CryptoFrame, + HandshakeDoneFrame, + MaxDataFrame, MaxStreamDataFrame, MaxStreamsFrame, + NewConnectionIDFrame, NewTokenFrame, + PaddingFrame, PathChallengeFrame, PathResponseFrame, PingFrame, + ResetStreamFrame, RetireConnectionIDFrame, + StreamsBlockedFrame, StreamDataBlockedFrame, StreamFrame, StopSendingFrame { + + public static final long MAX_VL_INTEGER = (1L << 62) - 1; + /** + * Frame types + */ + public static final int PADDING=0x00; + public static final int PING=0x01; + public static final int ACK=0x02; + public static final int RESET_STREAM=0x04; + public static final int STOP_SENDING=0x05; + public static final int CRYPTO=0x06; + public static final int NEW_TOKEN=0x07; + public static final int STREAM=0x08; + public static final int MAX_DATA=0x10; + public static final int MAX_STREAM_DATA=0x11; + public static final int MAX_STREAMS=0x12; + public static final int DATA_BLOCKED=0x14; + public static final int STREAM_DATA_BLOCKED=0x15; + public static final int STREAMS_BLOCKED=0x16; + public static final int NEW_CONNECTION_ID=0x18; + public static final int RETIRE_CONNECTION_ID=0x19; + public static final int PATH_CHALLENGE=0x1a; + public static final int PATH_RESPONSE=0x1b; + public static final int CONNECTION_CLOSE=0x1c; + public static final int HANDSHAKE_DONE=0x1e; + private static final int MAX_KNOWN_FRAME_TYPE = HANDSHAKE_DONE; + private final int frameType; + + /** + * Concrete Frame types normally have two constructors which call this + * + * XXXFrame(ByteBuffer, int firstByte) which is called for incoming frames + * after reading the first byte to determine the type. The firstByte is also + * supplied to the constructor because it can contain additional state information + * + * XXXFrame(...) which is called to instantiate outgoing frames + * @param type the first byte of the frame, which encodes the frame type. + */ + QuicFrame(int type) { + frameType = type; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * decode given ByteBuffer and return a QUICFrame + */ + public static QuicFrame decode(ByteBuffer buffer) throws QuicTransportException { + long frameTypeLong = VariableLengthEncoder.decode(buffer); + if (frameTypeLong < 0) { + throw new QuicTransportException("Error decoding frame type", + null, 0, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + if (frameTypeLong > Integer.MAX_VALUE) { + throw new QuicTransportException("Unrecognized frame", + null, frameTypeLong, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + int frameType = (int)frameTypeLong; + var frame = switch (maskType(frameType)) { + case ACK -> new AckFrame(buffer, frameType); + case STREAM -> new StreamFrame(buffer, frameType); + case RESET_STREAM -> new ResetStreamFrame(buffer, frameType); + case PADDING -> new PaddingFrame(buffer, frameType); + case PING -> new PingFrame(buffer, frameType); + case STOP_SENDING -> new StopSendingFrame(buffer, frameType); + case CRYPTO -> new CryptoFrame(buffer, frameType); + case NEW_TOKEN -> new NewTokenFrame(buffer, frameType); + case DATA_BLOCKED -> new DataBlockedFrame(buffer, frameType); + case MAX_DATA -> new MaxDataFrame(buffer, frameType); + case MAX_STREAMS -> new MaxStreamsFrame(buffer, frameType); + case MAX_STREAM_DATA -> new MaxStreamDataFrame(buffer, frameType); + case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(buffer, frameType); + case STREAMS_BLOCKED -> new StreamsBlockedFrame(buffer, frameType); + case NEW_CONNECTION_ID -> new NewConnectionIDFrame(buffer, frameType); + case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(buffer, frameType); + case PATH_CHALLENGE -> new PathChallengeFrame(buffer, frameType); + case PATH_RESPONSE -> new PathResponseFrame(buffer, frameType); + case CONNECTION_CLOSE -> new ConnectionCloseFrame(buffer, frameType); + case HANDSHAKE_DONE -> new HandshakeDoneFrame(buffer, frameType); + default -> throw new QuicTransportException("Unrecognized frame", + null, frameType, QuicTransportErrors.FRAME_ENCODING_ERROR); + }; + assert frameClassOf(maskType(frameType)) == frame.getClass(); + assert frameTypeOf(frame.getClass()) == maskType(frameType); + assert frame.getTypeField() == frameType : "frame type mismatch: " + + frameType + "!=" + frame.getTypeField() + + " for frame: " + frame; + return frame; + } + + public static Class frameClassOf(int frameType) { + return switch (maskType(frameType)) { + case ACK -> AckFrame.class; + case STREAM -> StreamFrame.class; + case RESET_STREAM -> ResetStreamFrame.class; + case PADDING -> PaddingFrame.class; + case PING -> PingFrame.class; + case STOP_SENDING -> StopSendingFrame.class; + case CRYPTO -> CryptoFrame.class; + case NEW_TOKEN -> NewTokenFrame.class; + case DATA_BLOCKED -> DataBlockedFrame.class; + case MAX_DATA -> MaxDataFrame.class; + case MAX_STREAMS -> MaxStreamsFrame.class; + case MAX_STREAM_DATA -> MaxStreamDataFrame.class; + case STREAM_DATA_BLOCKED -> StreamDataBlockedFrame.class; + case STREAMS_BLOCKED -> StreamsBlockedFrame.class; + case NEW_CONNECTION_ID -> NewConnectionIDFrame.class; + case RETIRE_CONNECTION_ID -> RetireConnectionIDFrame.class; + case PATH_CHALLENGE -> PathChallengeFrame.class; + case PATH_RESPONSE -> PathResponseFrame.class; + case CONNECTION_CLOSE -> ConnectionCloseFrame.class; + case HANDSHAKE_DONE -> HandshakeDoneFrame.class; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + public static int frameTypeOf(Class frameClass) { + // we don't have class pattern matching yet - so switch + // on the class name instead + return switch (frameClass.getSimpleName()) { + case "AckFrame" -> ACK; + case "StreamFrame" -> STREAM; + case "ResetStreamFrame" -> RESET_STREAM; + case "PaddingFrame" -> PADDING; + case "PingFrame" -> PING; + case "StopSendingFrame" -> STOP_SENDING; + case "CryptoFrame" -> CRYPTO; + case "NewTokenFrame" -> NEW_TOKEN; + case "DataBlockedFrame" -> DATA_BLOCKED; + case "MaxDataFrame" -> MAX_DATA; + case "MaxStreamsFrame" -> MAX_STREAMS; + case "MaxStreamDataFrame" -> MAX_STREAM_DATA; + case "StreamDataBlockedFrame" -> STREAM_DATA_BLOCKED; + case "StreamsBlockedFrame" -> STREAMS_BLOCKED; + case "NewConnectionIDFrame" -> NEW_CONNECTION_ID; + case "RetireConnectionIDFrame" -> RETIRE_CONNECTION_ID; + case "PathChallengeFrame" -> PATH_CHALLENGE; + case "PathResponseFrame" -> PATH_RESPONSE; + case "ConnectionCloseFrame" -> CONNECTION_CLOSE; + case "HandshakeDoneFrame" -> HANDSHAKE_DONE; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + /** + * Writes src to dest, preserving position in src + */ + protected static void putByteBuffer(ByteBuffer dest, ByteBuffer src) { + dest.put(src.asReadOnlyBuffer()); + } + + /** + * Throws a QuicTransportException if the given buffer does not have enough bytes + * to finish decoding the frame + * + * @param buffer source buffer + * @param expected minimum number of bytes required + * @param type frame type to include in exception + * @throws QuicTransportException if the buffer is shorter than {@code expected} + */ + protected static void validateRemainingLength(ByteBuffer buffer, int expected, long type) + throws QuicTransportException + { + if (buffer.remaining() < expected) { + throw new QuicTransportException("Error decoding frame", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * depending on the frame type, additional bits can be encoded + * in frameType(). This masks them out to return a unique value + * for each frame type. + */ + private static int maskType(int type) { + if (type >= ACK && type < RESET_STREAM) + return ACK; + if (type >= STREAM && type < MAX_DATA) + return STREAM; + if (type >= MAX_STREAMS && type < DATA_BLOCKED) + return MAX_STREAMS; + if (type >= STREAMS_BLOCKED && type < NEW_CONNECTION_ID) + return STREAMS_BLOCKED; + if (type >= CONNECTION_CLOSE && type < HANDSHAKE_DONE) + return CONNECTION_CLOSE; + // all others are unique + return type; + } + + /** + * {@return true if this frame is ACK-eliciting} + * A frame is ACK-eliciting if it is anything + * other than {@link QuicFrame#ACK}, + * {@link QuicFrame#PADDING} or + * {@link QuicFrame#CONNECTION_CLOSE} + * (or its variant). + */ + public boolean isAckEliciting() { return true; } + + /** + * {@return the minimum number of bytes needed to encode this frame} + */ + public abstract int size(); + + protected final long decodeVLField(ByteBuffer buffer, String name) throws QuicTransportException { + long v = VariableLengthEncoder.decode(buffer); + if (v < 0) { + throw new QuicTransportException("Error decoding field: " + name, + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return v; + } + + protected final int decodeVLFieldAsInt(ByteBuffer buffer, String name) throws QuicTransportException { + long l = decodeVLField(buffer, name); + int intval = (int)l; + if (((long)intval) != l) { + throw new QuicTransportException(name + ":field too long", + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return intval; + } + + protected static int requireVLRange(int val, String message) { + if (val < 0) { + throw new IllegalArgumentException(message + " " + val + " not in range"); + } + return val; + } + + protected static long requireVLRange(long val, String fieldName) { + if (val < 0 || val > MAX_VL_INTEGER) { + throw new IllegalArgumentException( + String.format("%s not in VL range: %s", fieldName, val)); + } + return val; + } + + protected static void encodeVLField(ByteBuffer buffer, long val, String name) { + try { + VariableLengthEncoder.encode(buffer, val); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error encoding " + name, e); + } + } + + protected static int getVLFieldLengthFor(long val) { + return VariableLengthEncoder.getEncodedSize(val); + } + + /** + * The type of this frame, ie. one of values above, which means it + * excludes the additional information that is encoded into the first field + * of some QUIC frames. That additional info has to be maintained by the sub + * class and used by its encode() method to generate the first field for outgoing frames. + * + * see maskType() below + */ + public int frameType() { + return frameType; + } + + /** + * Encode this QUIC Frame into given ByteBuffer + */ + public abstract void encode(ByteBuffer buffer); + + /** + * {@return the type field that was / should be encoded} + * This is the {@linkplain #frameType() frame type} with + * possibly some additional bits set, depending on the + * frame. + * @implSpec + * The default implementation of this method is to return + * {@link #frameType()}. + */ + public long getTypeField() { return frameType(); } + + /** + * Tells whether this particular frame is valid in the given + * packet type. + * + *

      From + * RFC 9000, section 12.5. Frames and Number Spaces: + *

      + * Some frames are prohibited in different packet number space + * The rules here generalize those of TLS, in that frames associated + * with establishing the connection can usually appear in packets + * in any packet number space, whereas those associated with transferring + * data can only appear in the application data packet number space: + * + *
        + *
      • PADDING, PING, and CRYPTO frames MAY appear in any packet number + * space.
      • + *
      • CONNECTION_CLOSE frames signaling errors at the QUIC layer (type 0x1c) + * MAY appear in any packet number space.
      • + *
      • CONNECTION_CLOSE frames signaling application errors (type 0x1d) + * MUST only appear in the application data packet number space. + *
      • ACK frames MAY appear in any packet number space but can only + * acknowledge packets that appeared in that packet number space. + * However, as noted below, 0-RTT packets cannot contain ACK frames.
      • + *
      • All other frame types MUST only be sent in the application data + * packet number space.
      • + *
      + * + * Note that it is not possible to send the following frames in 0-RTT + * packets for various reasons: ACK, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, + * PATH_RESPONSE, and RETIRE_CONNECTION_ID. A server MAY treat receipt + * of these frames in 0-RTT packets as a connection error of + * type PROTOCOL_VIOLATION. + *
      + * + * @param packetType the packet type + * @return true if the frame can be embedded in a packet of that type + */ + public boolean isValidIn(QuicPacket.PacketType packetType) { + return switch (frameType) { + case PADDING, PING -> true; + case ACK, CRYPTO -> switch (packetType) { + case VERSIONS, ZERORTT -> false; + default -> true; + }; + case CONNECTION_CLOSE -> { + if ((getTypeField() & 0x1D) == 0x1C) yield true; + yield QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + } + case HANDSHAKE_DONE, NEW_TOKEN, PATH_RESPONSE, + RETIRE_CONNECTION_ID -> switch (packetType) { + case ZERORTT -> false; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java new file mode 100644 index 00000000000..7a3579fc831 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ResetStreamFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + private final long finalSize; + + ResetStreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RESET_STREAM); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + finalSize = decodeVLField(buffer, "finalSize"); + } + + /** + */ + public ResetStreamFrame( + long streamID, + long errorCode, + long finalSize) + { + super(RESET_STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.finalSize = requireVLRange(finalSize, "finalSize"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RESET_STREAM, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + encodeVLField(buffer, finalSize, "finalSize"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamId() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + /** + */ + public long finalSize() { + return finalSize; + } + + @Override + public int size() { + return getVLFieldLengthFor(RESET_STREAM) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode) + + getVLFieldLengthFor(finalSize); + } + + @Override + public String toString() { + return "ResetStreamFrame(stream=" + streamID + + ", errorCode=" + errorCode + + ", finalSize=" + finalSize + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java new file mode 100644 index 00000000000..bf448f6a301 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RETIRE_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class RetireConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + + /** + * Incoming RETIRE_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + RetireConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RETIRE_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + } + + /** + * Outgoing RETIRE_CONNECTION_ID frame + */ + public RetireConnectionIDFrame(long sequenceNumber) { + super(RETIRE_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RETIRE_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long sequenceNumber() { + return sequenceNumber; + } + + @Override + public int size() { + return getVLFieldLengthFor(RETIRE_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber); + } + + @Override + public String toString() { + return "RetireConnectionIDFrame(" + + "sequenceNumber=" + sequenceNumber + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java new file mode 100644 index 00000000000..4a7d6525685 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STOP_SENDING Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StopSendingFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + + StopSendingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STOP_SENDING); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + } + + /** + */ + public StopSendingFrame(long streamID, long errorCode) { + super(STOP_SENDING); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STOP_SENDING, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamID() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + @Override + public int size() { + return getVLFieldLengthFor(STOP_SENDING) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode); + } + + @Override + public String toString() { + return "StopSendingFrame(stream=" + streamID + + ", errorCode=" + errorCode + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java new file mode 100644 index 00000000000..7dd95e2278b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAM_DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamDataBlockedFrame extends QuicFrame { + + private final long streamId; + private final long maxStreamData; + + /** + * Incoming STREAM_DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamDataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM_DATA_BLOCKED); + assert type == STREAM_DATA_BLOCKED : "STREAM_DATA_BLOCKED, unexpected frame type 0x" + + Integer.toHexString(type); + streamId = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing STREAM_DATA_BLOCKED frame + */ + public StreamDataBlockedFrame(long streamId, long maxStreamData) { + super(STREAM_DATA_BLOCKED); + this.streamId = requireVLRange(streamId, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STREAM_DATA_BLOCKED, "type"); + encodeVLField(buffer, streamId, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamId() { + return streamId; + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAM_DATA_BLOCKED) + + getVLFieldLengthFor(streamId) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "StreamDataBlockedFrame(" + + "streamId=" + streamId + + ", maxStreamData=" + maxStreamData + + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StreamDataBlockedFrame that)) return false; + if (streamId != that.streamId) return false; + return maxStreamData == that.maxStreamData; + } + + @Override + public int hashCode() { + int result = (int) (streamId ^ (streamId >>> 32)); + result = 31 * result + (int) (maxStreamData ^ (maxStreamData >>> 32)); + return result; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java new file mode 100644 index 00000000000..b597e53c06c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamFrame extends QuicFrame { + + // Flags in frameType() + private static final int OFF = 0x4; + private static final int LEN = 0x2; + private static final int FIN = 0x1; + + private final long streamID; + // true if the OFF bit in the type field has been set + private final boolean typeFieldHasOFF; + private final long offset; + private final int length; // -1 means consume all data in packet + private final int dataLength; + private final ByteBuffer streamData; + private final boolean fin; + + StreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM); + streamID = decodeVLField(buffer, "streamID"); + if ((type & OFF) > 0) { + typeFieldHasOFF = true; + offset = decodeVLField(buffer, "offset"); + } else { + typeFieldHasOFF = false; + offset = 0; + } + if ((type & LEN) > 0) { + length = decodeVLFieldAsInt(buffer, "length"); + } else { + length = -1; + } + if (length == -1) { + int remaining = buffer.remaining(); + streamData = Utils.sliceOrCopy(buffer, buffer.position(), remaining); + buffer.position(buffer.limit()); + dataLength = remaining; + } else { + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + streamData = Utils.sliceOrCopy(buffer, pos, length); + buffer.position(pos + length); + dataLength = length; + } + fin = (type & FIN) == 1; + } + + /** + * Creates StreamFrame (length == -1 means no length specified in frame + * and is assumed to occupy the remainder of the Quic/UDP packet. + * If a length is specified then it must correspond with the remaining bytes + * in streamData + */ + // It would be interesting to have a version of this constructor that can take + // a list of ByteBuffer. + public StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData) { + this(streamID, offset, length, fin, streamData, true); + } + + private StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData, boolean slice) + { + super(STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.offset = requireVLRange(offset, "offset"); + // if offset is non-zero then we mark that the type field has OFF bit set + // to allow for that bit to be set when encoding this frame + this.typeFieldHasOFF = this.offset != 0; + if (length != -1 && length != streamData.remaining()) { + throw new IllegalArgumentException("bad length"); + } + this.length = length; + this.dataLength = streamData.remaining(); + this.fin = fin; + this.streamData = slice + ? streamData.slice(streamData.position(), dataLength) + : streamData; + } + + /** + * Creates a new StreamFrame which is a slice of this stream frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current stream frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this stream frame + */ + public StreamFrame slice(long offset, int length) { + long oldoffset = offset(); + long offsetdiff = offset - oldoffset; + long oldlen = dataLength(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = streamData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + // preserves the FIN bit if set + boolean fin = this.fin && offset + length == oldoffset + oldlen; + ByteBuffer slice = Utils.sliceOrCopy(streamData, newpos, length); + return new StreamFrame(streamID, offset, length, fin, slice, false); + } + + /** + * {@return the stream id} + */ + public long streamId() { + return streamID; + } + + /** + * {@return whether this frame has a length} + * A frame that doesn't have a length must be the last + * frame in the packet. + */ + public boolean hasLength() { + return length != -1; + } + + /** + * {@return true if this is the last frame in the stream} + * The last frame has the FIN bit set. + */ + public boolean isLast() { return fin; } + + @Override + public long getTypeField() { + return STREAM | (hasLength() ? LEN : 0) + | (typeFieldHasOFF ? OFF : 0) + | (fin ? FIN : 0); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, getTypeField(), "type"); + encodeVLField(dest, streamID, "streamID"); + if (typeFieldHasOFF) { + encodeVLField(dest, offset, "offset"); + } + if (hasLength()) { + encodeVLField(dest, length, "length"); + assert streamData.remaining() == length; + } + putByteBuffer(dest, streamData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(streamID); + if (typeFieldHasOFF) { + size += getVLFieldLengthFor(offset); + } + if (hasLength()) { + return size + getVLFieldLengthFor(length) + length; + } else { + return size + streamData.remaining(); + } + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return streamData.slice(); + } + + /** + * {@return the frame offset} + */ + public long offset() { return offset; } + + /** + * {@return the number of data bytes in the frame} + * @apiNote + * This is equivalent to calling {@code payload().remaining()}. + */ + public int dataLength() { + return dataLength; + } + + public static int compareOffsets(StreamFrame sf1, StreamFrame sf2) { + return Long.compare(sf1.offset, sf2.offset); + } + + /** + * Computes the header size that would be required to encode a frame with + * the given streamId, offset, and length. + * @apiNote + * This method is useful to figure out how many bytes can be allocated for + * the frame data, given a size constraint imposed by the space available + * for the whole datagram payload. + * @param encoder the {@code QuicPacketEncoder} - which can be used in case + * some part of the computation is Quic-version dependent. + * @param streamId the stream id + * @param offset the stream offset + * @param length the estimated length of the frame, typically this will be + * the min between the data available in the stream with respect + * to flow control, and the maximum remaining size for the datagram + * payload + * @return the estimated size of the header for a {@code StreamFrame} that would + * be created with the given parameters. + */ + public static int headerSize(QuicPacketEncoder encoder, long streamId, long offset, long length) { + // the header length is the size needed to encode the frame type, + // plus the size needed to encode the streamId, plus the size needed + // to encode the offset (if not 0) and the size needed to encode the + // length (if present) + int headerLength = getVLFieldLengthFor(STREAM | OFF | LEN | FIN) + + getVLFieldLengthFor(streamId); + if (offset != 0) headerLength += getVLFieldLengthFor(offset); + if (length >= 0) headerLength += getVLFieldLengthFor(length); + return headerLength; + } + + @Override + public String toString() { + return "StreamFrame(stream=" + streamID + + ", offset=" + offset + + ", length=" + length + + ", fin=" + fin + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java new file mode 100644 index 00000000000..69292ffbbc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAMS_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamsBlockedFrame extends QuicFrame { + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming STREAMS_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamsBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAMS_BLOCKED); + bidi = (type == STREAMS_BLOCKED); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MaxStreamsFrame.MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing STREAMS_BLOCKED frame + */ + public StreamsBlockedFrame(boolean bidi, long maxStreams) { + super(STREAMS_BLOCKED); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return STREAMS_BLOCKED + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAMS_BLOCKED) + + getVLFieldLengthFor(maxStreams); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public String toString() { + return "StreamsBlockedFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java new file mode 100644 index 00000000000..dcdd040c0ed --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic; + +/** + *

      Internal classes for the Quic protocol implementation

      + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java new file mode 100644 index 00000000000..a037f2e9387 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Handshake Packets, as defined by + * RFC 9000, Section 17.2.4: + * + *
      {@code
      + *    A Handshake packet uses long headers with a type value of 0x02, followed
      + *    by the Length and Packet Number fields; see Section 17.2. The first byte
      + *    contains the Reserved and Packet Number Length bits; see Section 17.2.
      + *    It is used to carry cryptographic handshake messages and acknowledgments
      + *    from the server and client.
      + *
      + *    Handshake Packet {
      + *      Header Form (1) = 1,
      + *      Fixed Bit (1) = 1,
      + *      Long Packet Type (2) = 2,
      + *      Reserved Bits (2),
      + *      Packet Number Length (2),
      + *      Version (32),
      + *      Destination Connection ID Length (8),
      + *      Destination Connection ID (0..160),
      + *      Source Connection ID Length (8),
      + *      Source Connection ID (0..160),
      + *      Length (i),
      + *      Packet Number (8..32),
      + *      Packet Payload (..),
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Handshake Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface HandshakePacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.HANDSHAKE; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.HANDSHAKE; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java new file mode 100644 index 00000000000..7be864c8b9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Initial Packets, as defined by + * RFC 9000, Section 17.2.2: + * + *

      {@code
      + *    An Initial packet uses long headers with a type value of 0x00.
      + *    It carries the first CRYPTO frames sent by the client and server to perform
      + *    key exchange, and it carries ACK frames in either direction.
      + *
      + *    Initial Packet {
      + *      Header Form (1) = 1,
      + *      Fixed Bit (1) = 1,
      + *      Long Packet Type (2) = 0,
      + *      Reserved Bits (2),         # Protected
      + *      Packet Number Length (2),  # Protected
      + *      Version (32),
      + *      DCID Len (8),
      + *      Destination Connection ID (0..160),
      + *      SCID Len (8),
      + *      Source Connection ID (0..160),
      + *      Token Length (i),
      + *      Token (..),
      + *      Length (i),
      + *      Packet Number (8..32),     # Protected
      + *      # Protected Packet Payload (..)
      + *      Protected Payload (0..24), # Skipped Part
      + *      Protected Payload (128),   # Sampled Part
      + *      Protected Payload (..)     # Remainder
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Initial Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface InitialPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.INITIAL; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.INITIAL; + } + + @Override + default boolean hasLength() { return true; } + + /** + * {@return the length of the token field, if present, 0 if not} + */ + int tokenLength(); + + /** + * {@return the token bytes, if present, {@code null} if not} + * + * From + * RFC 9000, Section 17.2.2: + * + *

      {@code
      +     *    The value of the token that was previously provided
      +     *    in a Retry packet or NEW_TOKEN frame; see Section 8.1.
      +     * }
      + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] token(); + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, token[%s], frames:%s)", packetType(), packetNumber(), + size(), tokenLength(), frames()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java new file mode 100644 index 00000000000..9c9a6e6ef31 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packet header, as defined by + * RFC 8999, Section 5.1: + * + *
      {@code
      + *    Long Header Packet {
      + *       Header Form (1) = 1,
      + *       Version-Specific Bits (7),
      + *       Version (32),
      + *       Destination Connection ID Length (8),
      + *       Destination Connection ID (0..2040),
      + *       Source Connection ID Length (8),
      + *       Source Connection ID (0..2040),
      + *       Version-Specific Data (..),
      + *    }
      + * }
      + * + * @param version version + * @param destinationId Destination Connection ID + * @param sourceId Source Connection ID + * @param headerLength length in bytes of the packet header + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + */ +public record LongHeader(int version, + QuicConnectionId destinationId, + QuicConnectionId sourceId, + int headerLength) { +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java new file mode 100644 index 00000000000..960aef6530b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packets, as defined by + * RFC 8999, Section 5.1: + * + *
      {@code
      + *    Long Header Packet {
      + *       Header Form (1) = 1,
      + *       Version-Specific Bits (7),
      + *       Version (32),
      + *       Destination Connection ID Length (8),
      + *       Destination Connection ID (0..2040),
      + *       Source Connection ID Length (8),
      + *       Source Connection ID (0..2040),
      + *       Version-Specific Data (..),
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface LongHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.LONG; } + + /** + * {@return the packet's source connection ID} + */ + QuicConnectionId sourceId(); + + /** + * {@return the Quic version of the packet} + */ + int version(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java new file mode 100644 index 00000000000..7df9f904a82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 1-RTT packets, as defined by + * RFC 9000, Section 17.3.1: + * + *

      {@code
      + *    A 1-RTT packet uses a short packet header. It is used after the
      + *    version and 1-RTT keys are negotiated.
      + *
      + *    1-RTT Packet {
      + *      Header Form (1) = 0,
      + *      Fixed Bit (1) = 1,
      + *      Spin Bit (1),
      + *      Reserved Bits (2),         # Protected
      + *      Key Phase (1),             # Protected
      + *      Packet Number Length (2),  # Protected
      + *      Destination Connection ID (0..160),
      + *      Packet Number (8..32),     # Protected
      + *      # Protected Packet Payload:
      + *      Protected Payload (0..24), # Skipped Part
      + *      Protected Payload (128),   # Sampled Part
      + *      Protected Payload (..),    # Remainder
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Quic Version 2 uses the same 1-RTT packet structure than + * Quic Version 1. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface OneRttPacket extends ShortHeaderPacket { + + @Override + List frames(); + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default PacketType packetType() { + return PacketType.ONERTT; + } + + /** + * Returns the packet's Key Phase Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *

      {@code
      +     *     Bit (0x04) of byte 0 indicates the key phase, which allows a recipient
      +     *     of a packet to identify the packet protection keys that are used to
      +     *     protect the packet. See [QUIC-TLS] for details.
      +     *     This bit is protected using header protection; see Section 5.4 of [QUIC-TLS].
      +     * }
      + * + * @return the packet's Key Phase Bit + * + * @see RFC 9001, [QUIC-TLS] + * @see RFC 9001, Section 5.4, [QUIC-TLS] + */ + default int keyPhase() { + return -1; + } + + /** + * Returns the packet's Latency Spin Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *
      {@code
      +     *     The third most significant bit (0x20) of byte 0 is the latency spin
      +     *     bit, set as described in Section 17.4.
      +     * }
      + * + * @return the packet's Latency Spin Bit + * + * @see RFC 9000, Section 17.4 + */ + default int spin() { + return -1; + } + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, phase:%s, spin:%s, frames:%s)", packetType(), packetNumber(), + size(), keyPhase(), spin(), frames()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java new file mode 100644 index 00000000000..cea0854e0e5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.PacketSpaceManager; +import jdk.internal.net.quic.QuicTransportException; + +/** + * An interface implemented by classes which keep track of packet + * numbers for a given packet number space. + */ +public sealed interface PacketSpace permits PacketSpaceManager { + + /** + * called on application packet space to record peer's transport parameters + * @param peerDelay max_ack_delay + * @param ackDelayExponent ack_delay_exponent + */ + void updatePeerTransportParameters(long peerDelay, long ackDelayExponent); + + /** + * {@return the packet number space managed by this class} + */ + PacketNumberSpace packetNumberSpace(); + + /** + * The largest processed PN is used to compute + * the packet number of an incoming Quic packet. + * + * @return the largest incoming packet number that + * was successfully processed in this space. + */ + long getLargestProcessedPN(); + + /** + * The largest received acked PN is used to compute the + * packet number that we include in an outgoing Quic packet. + * + * @return the largest packet number that was acknowledged by + * the peer in this space. + */ + long getLargestPeerAckedPN(); + + /** + * {@return the largest packet number that we have acknowledged in this + * space} + * + * @apiNote This is necessarily greater or equal to the packet number + * returned by {@linkplain #getMinPNThreshold()}. + */ + long getLargestSentAckedPN(); + + /** + * {@return the packet number threshold below which packets should be + * discarded without being processed in this space} + * + * @apiNote + * This corresponds to the largest acknowledged packet number + * carried in an outgoing ACK frame whose packet number has + * been acknowledged by the peer. In other words, the largest + * packet number sent by the peer for which we know that the + * peer has received an acknowledgement. + *

      + * Note that we need to track the ACK of outgoing packets that + * contain ACK frames in order to figure out whether a peer + * knows that a particular packet number has been received and + * avoid retransmission. However - we don't want ACK frames to grow + * too big and therefore we can drop some of the information, + * based on the largestSentAckedPN - see RFC 9000 Section 13.2 + * + */ + long getMinPNThreshold(); + + /** + * {@return a new packet number atomically allocated in this space} + */ + long allocateNextPN(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + void versionChanged(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful processing of retry packet. + * In that case we should treat all previously sent packets as lost. + */ + void retry(); + + /** + * {@return a lock used by the transmission task}. + * Used to ensure that the transmission task does not observe partial changes + * during processing of incoming Versions and Retry packets. + */ + ReentrantLock getTransmitLock(); + /** + * Called when a packet is received. Causes the next ack frame to be + * updated. If a packet contains an {@link AckFrame}, the caller is + * expected to also later call {@link #processAckFrame(AckFrame)} + * when processing the packet payload. + * + * @param packet the received packet + * @param packetNumber the received packet number + * @param isAckEliciting whether this packet is ack eliciting + */ + void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting); + + /** + * Signals that a packet has been sent. + * This method is called by {@link QuicConnectionImpl} when a packet has been + * pushed to the endpoint for sending. + *

      The retransmitted packet is taken out the pendingRetransmission list and + * the new packet is inserted in the pendingAcknowledgement list. + * + * @param packet the new packet being retransmitted + * @param previousPacketNumber the packet number of the previous packet that was not acknowledged, + * or -1 if this is not a retransmission + * @param packetNumber the new packet number under which this packet is being retransmitted + * @throws IllegalArgumentException If {@code newPacketNumber} is lesser than 0 + */ + void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber); + + /** + * Processes a received ACK frame. + * This method is called by {@link QuicConnectionImpl}. + * + * @param frame the ACK frame received. + */ + void processAckFrame(AckFrame frame) throws QuicTransportException; + + /** + * Signals that the peer confirmed the handshake. Application space only. + */ + void confirmHandshake(); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - the ack frame size doesn't exceed {@code maxSize} + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @param maxSize + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize); + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + * + * @apiNote The returned completable future is actually completed + * if any packet whose packet number is greater than the packet number + * that contained the ping frame is acknowledged. + */ + CompletableFuture requestSendPing(); + + /** + * Stops retransmission for this packet space. + */ + void close(); + + /** + * {@return true if this packet space is closed} + */ + boolean isClosed(); + + /** + * Triggers immediate run of transmit loop. + * + * This method is called by {@link QuicConnectionImpl} when new data may be + * available for sending, for example: + * - new stream data is available + * - new receive credit is available + * - stream is forcibly closed + */ + void runTransmitter(); + + /** + * {@return true if a packet with that packet number + * is already being acknowledged (will be, or has been + * acknowledged)} + * @param packetNumber the packet number + */ + boolean isAcknowledged(long packetNumber); + + /** + * Immediately retransmit one unacknowledged initial packet + * @spec https://www.rfc-editor.org/rfc/rfc9002#name-speeding-up-handshake-compl + * RFC 9002 6.2.3. Speeding up Handshake Completion + */ + void fastRetransmit(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java new file mode 100644 index 00000000000..fa04cb3c947 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; +import java.util.Optional; + +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * A super-interface for all specific Quic packet implementation + * classes. + */ +public interface QuicPacket { + + /** + * {@return the packet's Destination Connection ID} + * + * @see + * RFC 9000, Section 7.2 + */ + QuicConnectionId destinationId(); + + /** + * The packet number space. + * NONE is for packets that don't have a packet number, + * such as Stateless Reset. + */ + enum PacketNumberSpace { + INITIAL, HANDSHAKE, APPLICATION, NONE; + + /** + * Maps a {@code PacketType} to the corresponding + * packet number space. + *

      + * For {@link PacketType#RETRY}, {@link PacketType#VERSIONS}, and + * {@link PacketType#NONE}, {@link PacketNumberSpace#NONE} is returned. + * + * @param packetType a packet type + * + * @return the packet number space that corresponds to the + * given packet type + */ + public static PacketNumberSpace of(PacketType packetType) { + return switch (packetType) { + case ONERTT, ZERORTT -> APPLICATION; + case INITIAL -> INITIAL; + case HANDSHAKE -> HANDSHAKE; + case RETRY, VERSIONS, NONE -> NONE; + }; + } + + /** + * Maps a {@code KeySpace} to the corresponding + * packet number space. + *

      + * For {@link KeySpace#RETRY}, {@link PacketNumberSpace#NONE} + * is returned. + * + * @param keySpace a key space + * + * @return the packet number space that corresponds to the given + * key space. + */ + public static PacketNumberSpace of(KeySpace keySpace) { + return switch (keySpace) { + case ONE_RTT, ZERO_RTT -> APPLICATION; + case HANDSHAKE -> HANDSHAKE; + case INITIAL -> INITIAL; + case RETRY -> NONE; + }; + } + } + + /** + * The packet type for Quic packets. + */ + enum PacketType { + NONE, INITIAL, VERSIONS, ZERORTT, HANDSHAKE, RETRY, ONERTT; + public boolean isLongHeaderType() { + return switch (this) { + case ONERTT, NONE, VERSIONS -> false; + default -> true; + }; + } + + /** + * {@return true if packets of this type are short-header packets} + */ + public boolean isShortHeaderType() { + return this == ONERTT; + } + + /** + * {@return the QUIC-TLS key space corresponding to this packet type} + * Some packet types, such as {@link #VERSIONS}, do not have an associated + * key space. + */ + public Optional keySpace() { + return switch (this) { + case INITIAL -> Optional.of(KeySpace.INITIAL); + case HANDSHAKE -> Optional.of(KeySpace.HANDSHAKE); + case RETRY -> Optional.of(KeySpace.RETRY); + case ZERORTT -> Optional.of(KeySpace.ZERO_RTT); + case ONERTT -> Optional.of(KeySpace.ONE_RTT); + case VERSIONS -> Optional.empty(); + case NONE -> Optional.empty(); + }; + } + } + + /** + * The Headers Type of the packet. + * This is either SHORT or LONG, or NONE when it can't be + * determined, or when we know that the packet is a stateless + * reset packet. A stateless reset packet is indistinguishable + * from a short header packet, so we only know that a packet + * is a stateless reset if we built it. In that case, the packet + * may advertise its header's type as NONE. + */ + enum HeadersType { NONE, SHORT, LONG} + + /** + * {@return this packet's number space} + */ + PacketNumberSpace numberSpace(); + + /** + * This packet size. + * @return the number of bytes needed to encode the packet. + * @see #payloadSize() + * @see #length() + */ + int size(); + + /** + * {@return true if this packet is ACK-eliciting} + * A packet is ACK-eliciting if it contains any + * {@linkplain QuicFrame#isAckEliciting() + * ACK-eliciting frame}. + */ + default boolean isAckEliciting() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream().anyMatch(QuicFrame::isAckEliciting); + } + + /** + * Whether this packet has a length field whose value can be read + * from the packet bytes. + * @return whether this packet has a length. + */ + default boolean hasLength() { + return switch (packetType()) { + case INITIAL, ZERORTT, HANDSHAKE -> true; + default -> false; + }; + } + + /** + * Returns the length of the payload and packet number. Includes encryption tag. + * + * This is the value stored in the {@code Length} field in Initial, + * Handshake and 0-RTT packets. + * @return the length of the payload and packet number. + * @throws UnsupportedOperationException if this packet type does not have + * the {@code Length} field. + * @see #hasLength() + * @see #size() + * @see #payloadSize() + */ + default int length() { + throw new UnsupportedOperationException(); + } + + /** + * This packet header's type. Either SHORT or LONG. + * @return this packet's header's type. + */ + HeadersType headersType(); + + /** + * {@return this packet's type} + */ + PacketType packetType(); + + /** + * {@return this packet's packet number, if applicable, {@code -1L} otherwise} + */ + default long packetNumber() { + return -1L; + } + + /** + * {@return this packet's frames} + */ + default List frames() { + return List.of(); + } + + /** + * {@return the packet's payload size} + * This is the number of bytes needed to encode the packet's + * {@linkplain #frames() frames}. + * @see #size() + * @see #length() + */ + default int payloadSize() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return 0; + return frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + } + + default String prettyPrint() { + long pn = packetNumber(); + if (pn >= 0) { + return String.format("%s(pn:%s, size=%s, frames:%s)", packetType(), pn, size(), frames()); + } else { + return String.format("%s(size=%s)", packetType(), size()); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java new file mode 100644 index 00000000000..233a1a35778 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java @@ -0,0 +1,1748 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.Objects; +import java.util.List; +import java.nio.BufferUnderflowException; + +/** + * A {@code QuicPacketDecoder} encapsulates the logic to decode a + * quic packet. A {@code QuicPacketDecoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketDecoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketDecoder"); + + private final QuicVersion quicVersion; + private QuicPacketDecoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Reads the headers type from the given byte. + * @param first the first byte of a quic packet + * @return the headers type encoded in the given byte. + */ + private static QuicPacket.HeadersType headersType(byte first) { + int type = first & 0x80; + return type == 0 ? QuicPacket.HeadersType.SHORT : QuicPacket.HeadersType.LONG; + } + + /** + * Peeks at the headers type in the given byte buffer. + * Does not advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit.The provided offset must be less than the buffer + * limit in order for this method to read the header + * bytes. + * + * @param buffer the byte buffer containing a packet. + * @param offset the offset at which the packet starts. + * + * @return the header's type of the packet contained in this + * byte buffer. NONE if the header's type cannot be determined. + */ + public static QuicPacket.HeadersType peekHeaderType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return QuicPacket.HeadersType.NONE; + return headersType(buffer.get(offset)); + } + + /** + * Reads a connection ID length from the connection ID length + * byte. + * @param length the connection ID length byte. + * @return the connection ID length + */ + private static int connectionIdLength(byte length) { + // length is represented by an unsigned byte. + return length & 0xFF; + } + + /** + * Peeks at the connection id in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long headers packet. + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekLongConnectionId(ByteBuffer buffer) { + // the connection id length starts at index 5 (1 byte for headers, + // 4 bytes for version) + var pos = buffer.position(); + var remaining = buffer.remaining(); + if (remaining < 6) return null; + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length > remaining - 6) return null; + return buffer.slice(pos + 6, length); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long header packet. + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer) { + return peekLongHeader(buffer, buffer.position()); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * + * @param buffer the buffer containing a long header packet. + * @param offset the position of the start of the packet + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer, int offset) { + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 7) return null; + if ((buffer.get(offset) & 0x80) == 0) { + // short header + return null; + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int version = buffer.getInt(offset+1); + + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(offset + 5)); + if (length < 0 || length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + QuicConnectionId destinationId = new PeerConnectionId(buffer.slice(offset + 6, length), null); + + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + if (length > remaining - 7) return null; + int srcPos = offset + 6 + length; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(srcPos)); + if (srclength < 0 || srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the SCID + if (srclength > limit - srcPos - 1) return null; + QuicConnectionId sourceId = new PeerConnectionId(buffer.slice(srcPos + 1, srclength), null); + int headerLength = 7 + length + srclength; + + // Return the SCID as a buffer slice. + // The SCID begins at pos + 1 and has srclength bytes. + return new LongHeader(version, destinationId, sourceId, headerLength); + } + + /** + * Returns a bytebuffer containing the token from initial packet. + * This method doesn't advance the cursor. + * The buffer position must be at the start of an initial packet. + * @apiNote + * If the initial packet doesn't contain any token, an empty + * {@code ByteBuffer} is returned. + * @param buffer the buffer containing an initial packet. + * @return token or null if packet is malformed + */ + public static ByteBuffer peekInitialPacketToken(ByteBuffer buffer) { + + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var pos = buffer.position(); + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 6) return null; + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length < 0) return null; + + // skip the DCID, and read the SCID length + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + pos = pos + 6 + length; + if (pos > limit - 1) return null; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(pos)); + if (srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (srclength < 0) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the token + if (srclength > limit - pos - 1) return null; + + //skip the SCID, and read the token length + pos = pos + srclength + 1; + + // read the token length + int tokenLengthLength = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + assert tokenLengthLength <= 8; + if (pos > limit - tokenLengthLength -1) return null; + long tokenLength = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tokenLength < 0 || tokenLength > Integer.MAX_VALUE) return null; + if (tokenLength > limit - pos - tokenLengthLength) return null; + + // return the token + return buffer.slice(pos + tokenLengthLength, (int)tokenLength); + } + + /** + * Peeks at the connection id in the short header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the short header packet. + * + * @param buffer the buffer containing a short headers packet. + * @param length the connection id length. + * + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekShortConnectionId(ByteBuffer buffer, int length) { + int pos = buffer.position(); + int limit = buffer.limit(); + assert pos >= 0; + assert length <= QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + if (limit - pos < length + 1) return null; + return buffer.slice(pos+1, length); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * The packet is expected to start at the buffer's current position. + * + * @implNote + * This is equivalent to calling: + * {@code peekVersion(buffer, buffer.position())}. + * + * @param buffer the buffer containing the packet. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer) { + return peekVersion(buffer, buffer.position()); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. The buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + assert offset >= 0; + if (limit - offset < 5) return 0; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1); + } + return 0; + } + + /** + * Returns true if the first packet in the buffer is a version + * negotiation packet. + * This method doesn't advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. If the packet is a long header packet, + * the buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return true if the first packet in the buffer is a version + * negotiation packet. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + private static boolean isVersionNegotiation(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + if (limit - offset < 5) return false; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1) == 0; + } + return false; + } + + public abstract static class IncomingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected IncomingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + } + + private abstract static class IncomingLongHeaderPacket + extends IncomingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + IncomingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + } + + private abstract static class IncomingShortHeaderPacket + extends IncomingQuicPacket implements ShortHeaderPacket { + + IncomingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private static final class IncomingRetryPacket + extends IncomingLongHeaderPacket implements RetryPacket { + final int size; + final byte[] retryToken; + + private IncomingRetryPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int size, byte[] retryToken) { + super(sourceId, destinationId, version); + this.size = size; + this.retryToken = retryToken; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingRetryPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingRetryPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingRetryPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicTransportException { + try { + reader.verifyRetry(); + } catch (AEADBadTagException e) { + throw new IOException("Bad integrity tag", e); + } + + int size = reader.remaining(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Retry Token + byte[] retryToken = reader.readRetryToken(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryToken(%d), %s)", + retryToken.length, reader); + } + + // Retry Integrity Tag + assert reader.remaining() == 16; + byte[] retryIntegrityTag = reader.readRetryIntegrityTag(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryIntegrityTag(%d), %s)", + retryIntegrityTag.length, reader); + } + assert size == reader.bytesRead(); + + return new IncomingRetryPacket(sourceID, destinationID, version, + size, retryToken); + } + } + + private static final class IncomingHandshakePacket + extends IncomingLongHeaderPacket implements HandshakePacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingHandshakePacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingHandshakePacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingHandshakePacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingHandshakePacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingHandshakePacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(length(%d), %s)", + packetLength, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt HANDSHAKE packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.HANDSHAKE, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert size == reader.position() - reader.offset(); + + assert packetLength == (int)packetLength; + return new IncomingHandshakePacket(sourceID, destinationID, + version, (int)packetLength, packetNumber, frames, size); + } + } + + private static final class IncomingZeroRttPacket + extends IncomingLongHeaderPacket implements ZeroRttPacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingZeroRttPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingZeroRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingZeroRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingZeroRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingZeroRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var length = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(length(%d), %s)", + length, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(length); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (length - packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ZERORTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ZERO_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + assert length == (int)length; + return new IncomingZeroRttPacket(sourceID, destinationID, + version, (int)length, packetNumber, frames, size); + } + } + + private static final class IncomingOneRttPacket + extends IncomingShortHeaderPacket implements OneRttPacket { + + final int size; + final long packetNumber; + final List frames; + final int keyPhase; + final int spin; + + IncomingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, List frames, + int spin, int keyPhase, int size) { + super(destinationId); + this.keyPhase = keyPhase; + this.spin = spin; + this.size = size; + this.packetNumber = packetNumber; + this.frames = frames; + } + + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public int keyPhase() { + return keyPhase; + } + + @Override + public int spin() { + return spin; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingOneRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingOneRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingOneRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingOneRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(headers(%x), %s)", + headers, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readShortConnectionId(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + + // Remove protection before reading packet number + reader.unprotectShort(); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = reader.remaining(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + final int keyPhase = (headers & 0x04) >> 2; + // keyphase is a 1 bit structure, so only 0 or 1 are valid values + assert keyPhase == 0 || keyPhase == 1 : "unexpected key phase: " + keyPhase; + final int spin = (headers & 0x20) >> 5; + assert spin == 0 || spin == 1 : "unexpected spin bit: " + spin; + + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, keyPhase); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ONERTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.3.1 + if ((headers & 0x18) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ONE_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + return new IncomingOneRttPacket(destinationID, packetNumber, frames, spin, keyPhase, size); + } + } + + private static final class IncomingInitialPacket + extends IncomingLongHeaderPacket implements InitialPacket { + + final int size; + final int length; + final int tokenLength; + final long packetNumber; + final byte[] token; + final List frames; + + IncomingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, int version, + int tokenLength, byte[] token, int length, + long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.tokenLength = tokenLength; + this.token = token; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int tokenLength() { return tokenLength; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + @Override + public int size() { return size; } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingInitialPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingInitialPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingInitialPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingInitialPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([protected]headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get number of bytes needed to store the length of the token + var tokenLength = (int) reader.readTokenLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token-length(%d), %s)", + tokenLength, reader); + } + var token = reader.readToken(tokenLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token(%d), %s)", + token == null ? 0 : token.length, reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(packetLength(%d), %s)", + packetLength, reader); + } + assert packetLength == (int)packetLength; + if (packetLength > reader.remaining()) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid length(%d/%d), %s)", + packetLength, reader.remaining(), reader); + } + throw new BufferUnderflowException(); + } + + + // get the size (in bytes) of new packet + int size = reader.bytesRead() + (int)packetLength; + + if (!context.verifyToken(destinationID, token)) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid token(%s), %s)", + token == null ? "null" : HexFormat.of().formatHex(token), + reader); + } + return null; + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + int packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt INITIAL packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.INITIAL, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + assert size == reader.bytesRead() : size - reader.bytesRead(); + + return new IncomingInitialPacket(sourceID, destinationID, + version, tokenLength, token, (int)packetLength, packetNumber, frames, size); + } + + } + + private static final class IncomingVersionNegotiationPacket + extends IncomingLongHeaderPacket + implements VersionNegotiationPacket { + + final int size; + final int[] versions; + + IncomingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, int[] versions, + int size) { + super(sourceId, destinationId, version); + this.size = size; + this.versions = Objects.requireNonNull(versions); + } + + @Override + public int size() { return size; } + + @Override + public List frames() { return List.of(); } + + @Override + public int payloadSize() { return versions.length << 2; } + + @Override + public int[] supportedVersions() { + return versions; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingVersionNegotiationPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingVersionNegotiationPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingVersionNegotiationPacket decode(PacketReader reader, CodingContext context) + throws IOException { + + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + // The long header bit should be set. We should ignore the other 7 bits + assert QuicPacketDecoder.headersType(headers) == HeadersType.LONG || (headers & 0x80) == 0x80; + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Calculate payload length and retrieve payload + final int payloadLen = reader.remaining(); + final int versionsCount = payloadLen >> 2; + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + int[] versions = reader.readSupportedVersions(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert !reader.hasRemaining() : "%s superfluous bytes in buffer" + .formatted(reader.remaining()); + + // sanity checks: + var msg = "Bad version negotiation packet"; + if (payloadLen != versionsCount << 2) { + throw new IOException("%s: %s bytes after %s versions" + .formatted(msg, payloadLen % 4, versionsCount)); + } + if (versionsCount == 0) { + throw new IOException("%s: no supported versions in packet" + .formatted(msg)); + } + + return new IncomingVersionNegotiationPacket(sourceID, destinationID, + version, versions, size); + } + } + + /** + * Decode the contents of the given {@code ByteBuffer} and, depending on the + * {@link PacketType}, return a {@link QuicPacket} with the corresponding type. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer as appropriate. + * + *

      If successful, an {@code IncomingQuicPacket} instance is returned. + * The position of the buffer is moved to the first byte following the last + * decoded byte. The buffer limit is unchanged. + * + *

      Otherwise, an exception is thrown. The position of the buffer is unspecified, + * but is usually set at the place where the error occurred. + * + * @apiNote If successful, and the limit was not reached, this method should be + * called again to decode the next packet contained in the buffer. Otherwise, if + * an exception occurs, the remaining bytes in the buffer should be dropped, since + * the position of the next packet in the buffer cannot be determined with + * certainty. + * + * @param buffer the buffer with the bytes to be decoded + * @param context the decoding context + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ + public IncomingQuicPacket decode(ByteBuffer buffer, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + Objects.requireNonNull(buffer); + + assert buffer.order() == ByteOrder.BIG_ENDIAN; + PacketType type = peekPacketType(buffer); + PacketReader packetReader = new PacketReader(buffer, context, type); + + QuicTLSEngine.KeySpace keySpace = type.keySpace().orElse(null); + if (keySpace != null && !context.getTLSEngine().keysAvailable(keySpace)) { + if (debug.on()) { + debug.log("QuicPacketDecoder.decode(%s): no keys, skipping", packetReader); + } + return null; + } + + return switch (type) { + case RETRY -> IncomingRetryPacket.decode(packetReader, context); + case ONERTT -> IncomingOneRttPacket.decode(packetReader, context); + case ZERORTT -> IncomingZeroRttPacket.decode(packetReader, context); + case HANDSHAKE -> IncomingHandshakePacket.decode(packetReader, context); + case INITIAL -> IncomingInitialPacket.decode(packetReader, context); + case VERSIONS -> IncomingVersionNegotiationPacket.decode(packetReader, context); + case NONE -> throw new IOException("Unknown type: " + type); // if junk received + default -> throw new IOException("Not implemented: " + type); // if has type but not recognised + }; + } + + private static QuicConnectionId decodeConnectionID(ByteBuffer buffer) { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + int len = buffer.get() & 0xFF; + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + /** + * Peek at the size of the first packet present in the buffer. + * The position of the buffer must be at the first byte of the + * first packet. This method doesn't advance the buffer position. + * @param buffer A byte buffer containing quic packets + * @return the size of the first packet present in the buffer. + */ + public int peekPacketSize(ByteBuffer buffer) { + int pos = buffer.position(); + int limit = buffer.limit(); + int available = limit - pos; + assert available >= 0 : available; + if (available <= 0) return available; + PacketType type = peekPacketType(buffer); + return switch (type) { + case HANDSHAKE, INITIAL, ZERORTT -> { + assert peekVersion(buffer, pos) == quicVersion.versionNumber(); + int end = peekPacketEnd(type, buffer); + assert end <= limit; + yield end - pos; + } + // ONERTT, RETRY, VERSIONS, NONE: + default -> available; + }; + } + + /** + * Reads the Quic V1 packet type from the given byte. + * + * @param headerByte the first byte of a quic packet + * @return the packet type encoded in the given byte. + */ + private PacketType packetType(byte headerByte) { + int htype = headerByte & 0xC0; + int ptype = headerByte & 0xF0; + return switch (htype) { + case 0xC0 -> switch (quicVersion) { + case QUIC_V1 -> switch (ptype) { + case 0xC0 -> PacketType.INITIAL; + case 0xD0 -> PacketType.ZERORTT; + case 0xE0 -> PacketType.HANDSHAKE; + case 0xF0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + case QUIC_V2 -> switch (ptype) { + case 0xD0 -> PacketType.INITIAL; + case 0xE0 -> PacketType.ZERORTT; + case 0xF0 -> PacketType.HANDSHAKE; + case 0xC0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + }; + case 0x40 -> PacketType.ONERTT; // may be a stateless reset too + default -> PacketType.NONE; + }; + } + + public PacketType peekPacketType(ByteBuffer buffer) { + int offset = buffer.position(); + return peekPacketType(buffer, offset); + } + + public PacketType peekPacketType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return PacketType.NONE; + var headers = buffer.get(offset); + var headersType = headersType(headers); + if (headersType == QuicPacket.HeadersType.LONG) { + if (isVersionNegotiation(buffer, offset)) { + return PacketType.VERSIONS; + } + var version = peekVersion(buffer, offset); + if (version != quicVersion.versionNumber()) { + return PacketType.NONE; + } + } + return packetType(headers); + } + + /** + * Returns the position just after the first packet present in the buffer. + * @param type the first packet type. Must be INITIAL, HANDSHAKE, or ZERORTT. + * @param buffer the byte buffer containing the packet + * @return the position just after the first packet present in the buffer. + */ + private int peekPacketEnd(PacketType type, ByteBuffer buffer) { + // Store initial position to calculate size of packet decoded + int initialPosition = buffer.position(); + int limit = buffer.limit(); + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert type == PacketType.HANDSHAKE + || type == PacketType.INITIAL + || type == PacketType.ZERORTT : type; + // This case should have been handled by the caller + assert buffer.getInt(initialPosition + 1) != 0 : "version is 0"; + + int pos = initialPosition; // header bits + pos = pos + 4; // version + pos = pos + 1; // dcid length + if (pos <= 0 || pos >= limit) return limit; + int dcidlen = buffer.get(pos) & 0xFF; + pos = pos + dcidlen + 1; // scid length + if (pos <= 0 || pos >= limit) return limit; + int scidlen = buffer.get(pos) & 0xFF; + pos = pos + scidlen + 1; // token length or packet length + if (pos <= 0 || pos >= limit) return limit; + + if (type == PacketType.INITIAL) { + int tksize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (tksize <= 0 || tksize > 8) return limit; + if (limit - tksize < pos) return limit; + long tklen = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tklen < 0 || tklen > limit - pos) return limit; + pos = pos + tksize + (int)tklen; // packet length + if (pos <= 0 || pos >= limit) return limit; + } + + int lensize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (lensize <= 0 || lensize > 8) return limit; + long len = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (len < 0 || len > limit - pos) return limit; + pos = pos + lensize + (int)len; // end of packet + if (pos <= 0 || pos >= limit) return limit; + return pos; + } + + /** + * Find the length of the next packet in the buffer, and return + * the next packet bytes as a slice of the original packet. + * Advances the original buffer position to after the returned + * packet. + * @param buffer a buffer containing coalesced packets + * @param offset the offset at which the next packet starts + * @return the next packet. + */ + public ByteBuffer nextPacketSlice(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + ByteBuffer next = null; + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("next packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + next = buffer.slice(offset, size); + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + next = buffer; + } + return next; + } + + /** + * Advance the bytebuffer position to the end of the packet + * @param buffer A byte buffer containing quic packets + * @param offset The offset at which the packet starts + */ + public void skipPacket(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("dropping packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + } + } + + /** + * Returns a decoder for the given Quic version. + * Returns {@code null} if no decoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return a decoder for the given Quic version or {@code null} + */ + public static QuicPacketDecoder of(QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Decoders.QUIC_V1_DECODER; + case QUIC_V2 -> Decoders.QUIC_V2_DECODER; + default -> throw new IllegalArgumentException("No packet decoder for Quic version " + quicVersion); + }; + } + + /** + * Returns a {@code QuicPacketDecoder} to decode the packet + * starting at the specified offset in the buffer. + * This method will attempt to read the quic version in the + * packet in order to return the proper decoder. + * If the version is 0, then the decoder for Quic Version 1 + * is returned. + * + * @param buffer A buffer containing a Quic packet + * @param offset The offset at which the packet starts + * @return A {@code QuicPacketDecoder} instance to decode the + * packet starting at the given offset. + */ + public static QuicPacketDecoder of(ByteBuffer buffer, int offset) { + var version = peekVersion(buffer, offset); + final QuicVersion quicVersion = version == 0 ? QuicVersion.QUIC_V1 + : QuicVersion.of(version).orElse(null); + if (quicVersion == null) { + return null; + } + return of(quicVersion); + } + + /** + * A {@code PacketReader} to read a Quic packet. + * A {@code PacketReader} may have version specific code, and therefore + * has an implicit pointer to a {@code QuicPacketDecoder} instance. + *

      + * A {@code PacketReader} offers high level helper methods to read + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code decode} method of the appropriate + * {@code IncomingQuicPacket} type. + *

      + * A {@code PacketReader} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is reading. + * + */ + class PacketReader { + private static final int PACKET_NUMBER_MASK = 0x03; + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketReader(ByteBuffer buffer, CodingContext context) { + this(buffer, context, peekPacketType(buffer)); + } + + PacketReader(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesRead() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public int packetNumberLength() { + return (headers() & PACKET_NUMBER_MASK) + 1; + } + + public byte readHeaders() { + return buffer.get(); + } + + public int readVersion() { + return buffer.getInt(); + } + + public int[] readSupportedVersions() { + // Calculate payload length and retrieve payload + final int payloadLen = buffer.remaining(); + final int versionsCount = payloadLen >> 2; + + int[] versions = new int[versionsCount]; + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + if (packetLength > remaining()) { + throw new BufferUnderflowException(); + } + return packetLength; + } + + public long readTokenLength() { + return readVariableLength(); + } + + public byte[] readToken(int tokenLength) { + // Check to ensure that tokenLength is within valid range + if (tokenLength < 0 || tokenLength > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] token = tokenLength > 0 ? new byte[tokenLength] : null; + if (tokenLength > 0) { + buffer.get(token); + } + return token; + } + + public long readVariableLength() { + return VariableLengthEncoder.decode(buffer); + } + + public void maskPacketNumber(int packetNumberLength, ByteBuffer mask) { + int pos = buffer.position(); + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(pos + i, (byte)(buffer.get(pos + i) ^ mask.get())); + } + } + + public long readPacketNumber(int packetNumberLength) { + var packetNumberSpace = PacketNumberSpace.of(packetType); + var largestProcessedPN = context.largestProcessedPN(packetNumberSpace); + return QuicPacketNumbers.decodePacketNumber(largestProcessedPN, buffer, packetNumberLength); + } + + public long readPacketNumber() { + return readPacketNumber(packetNumberLength()); + } + + private ByteBuffer peekPayloadSlice(int relativeOffset, int length) { + int payloadStart = buffer.position() + relativeOffset; + return buffer.slice(payloadStart, length); + } + + private ByteBuffer decryptPayload(long packetNumber, int payloadLen, int keyPhase) + throws AEADBadTagException, QuicKeyUnavailableException, QuicTransportException { + // Calculate payload length and retrieve payload + ByteBuffer output = buffer.slice(); + // output's position is on the first byte of encrypted data + output.mark(); + int payloadStart = buffer.position(); + buffer.position(offset); + buffer.limit(payloadStart + payloadLen); + // buffer's position and limit are set to the boundaries of the encrypted packet + try { + context.getTLSEngine().decryptPacket(packetType.keySpace().get(), packetNumber, keyPhase, + buffer, payloadStart - offset, output); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + // buffer's position and limit are both at end of the packet + output.limit(output.position()); + output.reset(); + // output's position and limit are set to the boundaries of decrypted frame data + buffer.limit(initialLimit); + return output; + } + + public List parsePayloadSlice(ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new QuicTransportException("Packet with no frames", + packetType().keySpace().get(), 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + try { + List frames = new ArrayList<>(); + while (payload.hasRemaining()) { + int start = payload.position(); + frames.add(QuicFrame.decode(payload)); + int end = payload.position(); + assert start < end : "bytes remaining at offset %s: %s" + .formatted(start, payload.remaining()); + } + return frames; + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + packetType().keySpace().get(), 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + byte[] readRetryToken() { + var tokenLength = buffer.limit() - buffer.position() - 16; + assert tokenLength > 0; + byte[] retryToken = new byte[tokenLength]; + buffer.get(retryToken); + return retryToken; + } + + byte[] readRetryIntegrityTag() { + // The 16 last bytes in the datagram payload + assert remaining() == 16; + byte[] retryIntegrityTag = new byte[16]; + buffer.get(retryIntegrityTag); + return retryIntegrityTag; + } + + public void verifyRetry() throws AEADBadTagException, QuicTransportException { + // assume the buffer position and limit are set to packet boundaries + QuicTLSEngine tlsEngine = context.getTLSEngine(); + tlsEngine.verifyRetryPacket(quicVersion, + context.originalServerConnId().asReadOnlyBuffer(), buffer.asReadOnlyBuffer()); + } + + public QuicConnectionId readLongConnectionId() { + return decodeConnectionID(buffer); + } + + public QuicConnectionId readShortConnectionId() { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + // Retrieve connection ID length from endpoint via context + int len = context.connectionIdLength(); + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + @Override + public String toString() { + return "PacketReader(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void unprotectLong(long packetLength) + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(packetLength, (byte) 0x0f); + } + + public void unprotectShort() + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(buffer.remaining(), (byte) 0x1f); + } + + private void unprotect(long packetLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + if (packetLength > buffer.remaining() || packetLength < sampleSize + 4) { + throw new BufferUnderflowException(); + } + ByteBuffer sample = peekPayloadSlice(4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), true, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberLength(), encryptedSample); + } + } + + + private static final class Decoders { + static final QuicPacketDecoder QUIC_V1_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V1); + static final QuicPacketDecoder QUIC_V2_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java new file mode 100644 index 00000000000..890c1a63a35 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java @@ -0,0 +1,1746 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.IntFunction; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.ShortBufferException; + +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.encodePacketNumber; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A {@code QuicPacketEncoder} encapsulates the logic to encode a + * quic packet. A {@code QuicPacketEncoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketEncoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketEncoder"); + + private final QuicVersion quicVersion; + private QuicPacketEncoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Computes the packet's header byte, which also encodes + * the packetNumber length. + * + * @param packetTypeTag quic-dependent packet type encoding + * @param pnsize the number of bytes needed to encode the packet number + * @return the packet's header byte + */ + private static byte headers(byte packetTypeTag, int pnsize) { + int pnprefix = pnsize - 1; + assert pnprefix >= 0; + assert pnprefix <= 3; + return (byte)(packetTypeTag | pnprefix); + } + + /** + * Returns the headers tag for the given packet type. + * Returns 0 if the packet type is NONE or unknown. + *

      + * For version negotiations packet, this method returns 0x80. + * The other 7 bits must be ignored by a client. + * When emitting a version negotiation packet the server should + * also set the fix bit (0x40) to 1. + * What distinguishes a version negotiation packet from other + * long header packet types is not the packet type found in the + * header's byte, but the fact that a. it is a long header and + * b. the version number in the packet (the 4 bytes following + * the header) is 0. + * @param packetType the packet type + * @return the headers tag for the given packet type. + * + * @see + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @see + * RFC 9369: QUIC Version 2 + */ + private byte packetHeadersTag(PacketType packetType) { + return (byte) switch (quicVersion) { + case QUIC_V1 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xC0; + case ZERORTT -> 0xD0; + case HANDSHAKE -> 0xE0; + case RETRY -> 0xF0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + case QUIC_V2 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xD0; + case ZERORTT -> 0xE0; + case HANDSHAKE -> 0xF0; + case RETRY -> 0xC0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + }; + } + + /** + * Encode the OneRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingOneRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionId destination = packet.destinationId(); + + if (debug.on()) { + debug.log("OneRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " dst=%s, packet=%d, encodedPacket=%s," + + " payload=QuicFrames(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), destination, + packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : "incorrect packet number prefix in headers: " + headers; + + final PacketWriter writer = new PacketWriter(buffer, context, PacketType.ONERTT); + writer.writeHeaders(headers); + writer.writeShortConnectionId(destination); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderShort(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the ZeroRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingZeroRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("ZeroRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.ZERORTT); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the VersionNegotiationPacket into the provided + * buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @throws BufferOverflowException if the buffer is not large enough + */ + private static void encodePacket(OutgoingVersionNegotiationPacket packet, + ByteBuffer buffer) { + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, versions=%s, size=%d", + buffer.position(), buffer.limit(), source, destination, + Arrays.toString(packet.versions), packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int offset = buffer.position(); + int limit = buffer.limit(); + assert buffer.capacity() >= packet.size; + assert limit - offset >= packet.size; + + int typeTag = 0x80; + int rand = Encoders.RANDOM.nextInt() & 0x7F; + int headers = typeTag | rand; + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " type: 0x%02x, unused: 0x%02x, headers: 0x%02x", + typeTag, rand & ~0x80, headers); + } + assert (headers & typeTag) == typeTag : headers; + assert (headers ^ typeTag) == rand : headers; + + // headers(1 byte), version(4 bytes) + buffer.put((byte)headers); // 1 + putInt32(buffer, 0); // 4 + + // DCID: 1 byte for length, + destination id bytes + var dcidlen = destination.length(); + assert dcidlen <= MAX_CONNECTION_ID_LENGTH && dcidlen >= 0 : dcidlen; + buffer.put((byte)dcidlen); // 1 + buffer.put(destination.asReadOnlyBuffer()); + assert buffer.position() == offset + 6 + dcidlen : buffer.position(); + + // SCID: 1 byte for length, + source id bytes + var scidlen = source.length(); + assert scidlen <= MAX_CONNECTION_ID_LENGTH && scidlen >= 0 : scidlen; + buffer.put((byte) scidlen); + buffer.put(source.asReadOnlyBuffer()); + assert buffer.position() == offset + 7 + dcidlen + scidlen : buffer.position(); + + // Put payload (= supported versions) + int versionsStart = buffer.position(); + for (int i = 0; i < packet.versions.length; i++) { + putInt32(buffer, packet.versions[i]); + } + int versionsEnd = buffer.position(); + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " encoded %d bytes", offset - versionsEnd); + } + + assert versionsEnd - offset == packet.size; + assert versionsEnd - versionsStart == packet.versions.length << 2; + } + + /** + * Encode the HandshakePacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingHandshakePacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("HandshakePacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.HANDSHAKE); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the InitialPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context coding context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingInitialPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("InitialPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, token=%s, " + + "payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.token == null ? null : "byte[%s]".formatted(packet.token.length), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.INITIAL); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeToken(packet.token); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the RetryPacket into the provided buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingRetryPacket packet, + ByteBuffer buffer, + CodingContext context) throws QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("RetryPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, retryToken=%d," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.retryToken.length, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert packet.retryToken.length > 0; + assert buffer.remaining() >= packet.size; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.RETRY); + + byte headers = packetHeadersTag(packet.packetType()); + headers |= (byte)Encoders.RANDOM.nextInt(0x10); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeRetryToken(packet.retryToken); + assert writer.remaining() >= 16; // 128 bits + writer.signRetry(version); + + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + } + + public abstract static class OutgoingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected OutgoingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + + @Override + public String toString() { + + return this.getClass().getSimpleName() + "[pn=" + this.packetNumber() + + ", frames=" + frames() + "]"; + } + } + + private abstract static class OutgoingShortHeaderPacket + extends OutgoingQuicPacket implements ShortHeaderPacket { + + OutgoingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private abstract static class OutgoingLongHeaderPacket + extends OutgoingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + OutgoingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + + } + + private static final class OutgoingRetryPacket + extends OutgoingLongHeaderPacket implements RetryPacket { + + final int size; + final byte[] retryToken; + + OutgoingRetryPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] retryToken) { + super(sourceId, destinationId, version); + this.retryToken = retryToken; + this.size = computeSize(retryToken.length); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the end of the retry integrity tag. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @return the total packet size. + */ + private int computeSize(int tokenLength) { + assert tokenLength > 0; + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), + // retryTokenIntegrity(128 bits) => 7 + 16 = 23 bytes + int size = Math.addExact(23, tokenLength); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + return size; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + } + + private static final class OutgoingHandshakePacket + extends OutgoingLongHeaderPacket implements HandshakePacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + final int payloadSize; + private int tagSize; + + OutgoingHandshakePacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.payloadSize = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + this.tagSize = tagSize; + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { return frames; } + + } + + private static final class OutgoingZeroRttPacket + extends OutgoingLongHeaderPacket implements ZeroRttPacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingZeroRttPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingOneRttPacket + extends OutgoingShortHeaderPacket implements OneRttPacket { + + final long packetNumber; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(destinationId); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.size = computeSize(payloadSize, encodedPacketNumber.length, tagSize); + } + + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag + * @return the total packet size. + */ + private int computeSize(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + // Fixed size bits: + // headers(1 byte) + int size = Math.addExact(1, destinationId().length()); + + size = Math.addExact(size, payloadSize); + size = Math.addExact(size, pnsize); + size = Math.addExact(size, tagSize); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingInitialPacket + extends OutgoingLongHeaderPacket implements InitialPacket { + + final byte[] token; + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + private record InitialPacketVariableComponents(int length, byte[] token, QuicConnectionId sourceId, + QuicConnectionId destinationId) { + + } + + public OutgoingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] token, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.token = token; + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computePacketSize(new InitialPacketVariableComponents(length, token, sourceId, + destinationId)); + } + + @Override + public int tokenLength() { return token == null ? 0 : token.length; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { return size; } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private static int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param variableComponents The variable components of the packet + * + * @return the total packet size. + */ + private static int computePacketSize(InitialPacketVariableComponents variableComponents) { + assert variableComponents.length >= 0; + + // how many bytes are needed to encode the length of the token + final byte[] token = variableComponents.token; + int tkLenSpecifierSize = token == null || token.length == 0 + ? 1 : VariableLengthEncoder.getEncodedSize(token.length); + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(variableComponents.length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID length specifier(1 byte), + // SCID length specifier(1 byte), => 7 bytes + int size = Math.addExact(7, variableComponents.sourceId.length()); + size = Math.addExact(size, variableComponents.destinationId.length()); + size = Math.addExact(size, tkLenSpecifierSize); + if (token != null) { + size = Math.addExact(size, token.length); + } + size = Math.addExact(size, lnsize); + size = Math.addExact(size, variableComponents.length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingVersionNegotiationPacket + extends OutgoingLongHeaderPacket + implements VersionNegotiationPacket { + + final int[] versions; + final int size; + final int payloadSize; + + public OutgoingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int[] versions) { + super(sourceId, destinationId, 0); + this.versions = versions.clone(); + this.payloadSize = versions.length << 2; + this.size = computeSize(payloadSize); + } + + @Override + public int[] supportedVersions() { + return versions.clone(); + } + + @Override + public int size() { return size; } + + @Override + public int payloadSize() { return payloadSize; } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @return the total packet size. + */ + private int computeSize(int payloadSize) { + assert payloadSize > 0; + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, payloadSize); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + return size; + } + + } + + /** + * Create a new unencrypted InitialPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param token The token field (may be null if no token) + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The initial packet payload + * + * @param codingContext + * @return the new initial packet + */ + public OutgoingQuicPacket newInitialPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] token, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newInitialPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 + // A client MUST expand the payload of all UDP datagrams carrying Initial packets + // to at least the smallest allowed maximum datagram size of 1200 bytes + // by adding PADDING frames to the Initial packet or by coalescing the Initial packet + + // first compute the packet size + final int originalPayloadSize = frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + final int originalLength = OutgoingInitialPacket.computeLength(originalPayloadSize, + encodedPacketNumber.length, tagSize); + final int originalPacketSize = OutgoingInitialPacket.computePacketSize( + new OutgoingInitialPacket.InitialPacketVariableComponents(originalLength, token, + source, destination)); + if (originalPacketSize >= 1200) { + return new OutgoingInitialPacket(source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, frames, tagSize); + } else { + // add padding + int numPaddingBytesNeeded = 1200 - originalPacketSize; + if (originalLength < 64 && originalLength + numPaddingBytesNeeded > 64) { + // if originalLength + numPaddingBytesNeeded == 64, will send + // 1201 bytes + numPaddingBytesNeeded--; + } + final List newFrames = new ArrayList<>(); + for (QuicFrame frame : frames) { + if (frame instanceof PaddingFrame) { + // a padding frame already exists, instead of including this and the new padding + // frame in the new frames, we just include 1 single padding frame whose + // combined size will be the sum of all existing padding frames and the + // additional padding bytes needed + numPaddingBytesNeeded += frame.size(); + continue; + } + // non-padding frame, include it in the new frames + newFrames.add(frame); + } + // add the padding frame as the first frame + newFrames.add(0, new PaddingFrame(numPaddingBytesNeeded)); + return new OutgoingInitialPacket( + source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, newFrames, tagSize); + } + } + + /** + * Create a new unencrypted VersionNegotiationPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param versions The supported quic versions + * @return the new initial packet + */ + public static OutgoingQuicPacket newVersionNegotiationPacket(QuicConnectionId source, + QuicConnectionId destination, + int[] versions) { + return new OutgoingVersionNegotiationPacket(source, destination, versions); + } + + /** + * Create a new unencrypted RetryPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param retryToken The retry token + * @return the new retry packet + */ + public OutgoingQuicPacket newRetryPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] retryToken) { + return new OutgoingRetryPacket( + source, destination, this.quicVersion.versionNumber(), retryToken); + } + + /** + * Create a new unencrypted ZeroRttPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The zero RTT packet payload + * @param codingContext + * @return the new zero RTT packet + */ + public OutgoingQuicPacket newZeroRttPacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newZeroRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ZERO_RTT); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingZeroRttPacket( + source, destination, this.quicVersion.versionNumber(), packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted HandshakePacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param frames The handshake packet payload + * @param codingContext + * @return the new handshake packet + */ + public OutgoingQuicPacket newHandshakePacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long largestAckedPN, + List frames, CodingContext codingContext) { + if (debug.on()) { + debug.log("newHandshakePacket: fullPN=%d ackedPN=%d", + packetNumber, largestAckedPN); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, largestAckedPN); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.HANDSHAKE); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingHandshakePacket( + source, destination, this.quicVersion.versionNumber(), + packetNumber, encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted OneRttPacket to be transmitted over the wire + * after encryption. + * + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The one RTT packet payload + * @param codingContext + * @return the new one RTT packet + */ + public OneRttPacket newOneRttPacket(QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newOneRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ONE_RTT); + // packets should be at least 22 bytes longer than the local connection id length. + // we ensure that by padding the frames to the necessary size + int minPayloadSize = codingContext.minShortPacketPayloadSize(destination.length()); + assert protectionSampleSize == tagSize; + int minLength = Math.max(5, minPayloadSize) - encodedPacketNumber.length; + return new OutgoingOneRttPacket( + destination, packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Creates a packet in the given keyspace for the purpose of sending + * a CONNECTION_CLOSE, or a generic list of frames. + * The {@code initialToken} parameter is ignored if the key + * space is not INITIAL. + * + * @param keySpace the sending key space + * @param packetSpace the packet space + * @param sourceId the source connection id + * @param destinationId the destination connection id + * @param initialToken the initial token for INITIAL packets + * @param frames the list of frames + * @param codingContext the coding context + * @return a packet in the given key space + * @throws IllegalArgumentException if the packet number space is + * not one of INITIAL, HANDSHAKE, or APPLICATION + */ + public OutgoingQuicPacket newOutgoingPacket( + KeySpace keySpace, + PacketSpace packetSpace, + QuicConnectionId sourceId, + QuicConnectionId destinationId, + byte[] initialToken, + List frames, CodingContext codingContext) { + long largestAckedPN = packetSpace.getLargestPeerAckedPN(); + return switch (packetSpace.packetNumberSpace()) { + case APPLICATION -> { + long newPacketNumber = packetSpace.allocateNextPN(); + if (keySpace == KeySpace.ZERO_RTT) { + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ZERORTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + yield newZeroRttPacket(sourceId, + destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + } else { + assert keySpace == KeySpace.ONE_RTT; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ONERTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + final OneRttPacket oneRttPacket = newOneRttPacket(destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + assert oneRttPacket instanceof OutgoingOneRttPacket : + "unexpected 1-RTT packet type: " + oneRttPacket.getClass(); + yield (OutgoingQuicPacket) oneRttPacket; + } + } + case HANDSHAKE -> { + assert keySpace == KeySpace.HANDSHAKE; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.HANDSHAKE)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newHandshakePacket(sourceId, destinationId, + newPacketNumber, largestAckedPN, + frames, codingContext); + } + case INITIAL -> { + assert keySpace == KeySpace.INITIAL; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.INITIAL)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newInitialPacket(sourceId, destinationId, + initialToken, newPacketNumber, + largestAckedPN, + frames, codingContext); + } + case NONE -> { + throw new IllegalArgumentException("packetSpace: %s, keySpace: %s" + .formatted(packetSpace.packetNumberSpace(), keySpace)); + } + }; + } + + /** + * Encodes the given QuicPacket. + * + * @param packet the packet to encode + * @param buffer the byte buffer to write the packet into + * @param context context for encoding + * @throws IllegalArgumentException if the packet is not an OutgoingQuicPacket, + * or if the packet version does not match the encoder version + * @throws BufferOverflowException if the buffer is not large enough + * @throws QuicKeyUnavailableException if the packet could not be encrypted + * because the required encryption key is not available + * @throws QuicTransportException if encrypting the packet resulted + * in an error that requires closing the connection + */ + public void encode(QuicPacket packet, ByteBuffer buffer, CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + switch (packet) { + case OutgoingOneRttPacket p -> encodePacket(p, buffer, context); + case OutgoingZeroRttPacket p -> encodePacket(p, buffer, context); + case OutgoingVersionNegotiationPacket p -> encodePacket(p, buffer); + case OutgoingHandshakePacket p -> encodePacket(p, buffer, context); + case OutgoingInitialPacket p -> encodePacket(p, buffer, context); + case OutgoingRetryPacket p -> encodePacket(p, buffer, context); + default -> throw new IllegalArgumentException("packet is not an outgoing packet: " + + packet.getClass()); + } + } + + /** + * Compute the max size of the usable payload of an initial + * packet, given the max size of the datagram. + *

      +     * Initial Packet {
      +     *     Header (1 byte),
      +     *     Version (4 bytes),
      +     *     Destination Connection ID Length (1 byte),
      +     *     Destination Connection ID (0..20 bytes),
      +     *     Source Connection ID Length (1 byte),
      +     *     Source Connection ID (0..20 bytes),
      +     *     Token Length (variable int),
      +     *     Token (..),
      +     *     Length (variable int),
      +     *     Packet Number (1..4 bytes),
      +     *     Packet Payload (1 to ... bytes),
      +     * }
      +     * 
      + * + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param pnsize packet number length + * @param tokenLength the length of the token (or {@code 0}) + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxInitialPayloadSize(CodingContext codingContext, + int pnsize, + int tokenLength, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength + tokenLength + + VariableLengthEncoder.getEncodedSize(tokenLength); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead - 1; // at least 1 byte for length encoding + if (length <= 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + // int lenafter = VariableLengthEncoder.getEncodedSize(length); // check + // assert lenafter == lenbefore : "%s -> %s (before:%s, after:%s)" + // .formatted(maxDatagramSize - overhead -1, length, lenbefore, lenafter); + if (length <= 0) return 0; + int available = length - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + /** + * Compute the max size of the usable payload of a handshake + * packet, given the max size of the datagram. + *
      +     * Initial Packet {
      +     *     Header (1 byte),
      +     *     Version (4 bytes),
      +     *     Destination Connection ID Length (1 byte),
      +     *     Destination Connection ID (0..20 bytes),
      +     *     Source Connection ID Length (1 byte),
      +     *     Source Connection ID (0..20 bytes),
      +     *     Length (variable int),
      +     *     Packet Number (1..4 bytes),
      +     *     Packet Payload (1 to ... bytes),
      +     * }
      +     * 
      + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param packetNumber the full packet number + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxHandshakePayloadSize(CodingContext codingContext, + long packetNumber, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength; + int pnsize = computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.HANDSHAKE)); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead -1; // at least 1 byte for length encoding + if (length < 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + int available = length - pnsize - tagSize; + return available; + } + + /** + * Computes the maximum usable payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption. + * @param codingContext the coding context + * @param packetNumber the packet number + * @param dstidLength the peer connection id length + * @param maxDatagramSizeBeforeEncryption the maximum size of the datagram + * @return the maximum payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption + */ + public static int computeMaxOneRTTPayloadSize(final CodingContext codingContext, + final long packetNumber, + final int dstidLength, + final int maxDatagramSizeBeforeEncryption, + final long largestPeerAckedPN) { + // header=1 + final int overhead = 1 + dstidLength; + // always reserve four bytes for packet number to avoid issues with packet + // sizes when retransmitting. This is a hack, but it avoids having to + // repack StreamFrames. + final int pnsize = 4; //computePacketNumberLength(packetNumber, largestPeerAckedPN); + // encryption tag, included in the payload, but not usable for frames + final int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + final int available = maxDatagramSizeBeforeEncryption - overhead - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + private static ByteBuffer putInt32(ByteBuffer buffer, int value) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.putInt(value); + } + + + /** + * A {@code PacketWriter} to write a Quic packet. + *

      + * A {@code PacketWriter} offers high level helper methods to write + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code encode} method of the appropriate + * {@code OutgoingQuicPacket} type. + *

      + * A {@code PacketWriter} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is writing. + * + */ + static class PacketWriter { + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketWriter(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesWritten() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public void writeHeaders(byte headers) { + buffer.put(headers); + } + + public void writeVersion(int version) { + buffer.putInt(version); + } + + public void writeSupportedVersions(int[] versions) { + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + writeVariableLength(packetLength); + } + + private void writeTokenLength(long tokenLength) { + writeVariableLength(tokenLength); + } + + public void writeToken(byte[] token) { + if (token == null) { + buffer.put((byte)0); + } else { + writeTokenLength(token.length); + buffer.put(token); + } + } + + public void writeVariableLength(long value) { + VariableLengthEncoder.encode(buffer, value); + } + + private void maskPacketNumber(int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + public void writeEncodedPacketNumber(byte[] packetNumber) { + buffer.put(packetNumber); + } + + public void encryptPayload(final long packetNumber, final int payloadstart) + throws QuicTransportException, QuicKeyUnavailableException { + final int payloadend = buffer.position(); + buffer.position(payloadstart); // position the output buffer + final int payloadLength = payloadend - payloadstart; + final int headersLength = payloadstart - offset; + final ByteBuffer packetHeader = buffer.slice(offset, headersLength); + final ByteBuffer packetPayload = buffer.slice(payloadstart, payloadLength) + .asReadOnlyBuffer(); + try { + context.getTLSEngine().encryptPacket(packetType.keySpace().get(), packetNumber, + new HeaderGenerator(this.packetType, packetHeader), packetPayload, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + } + + public void writePayload(List frames) { + for (var frame : frames) frame.encode(buffer); + } + + public void writeLongConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put((byte)src.remaining()); + buffer.put(src); + } + + public void writeShortConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put(src); + } + + public void writeRetryToken(byte[] retryToken) { + buffer.put(retryToken); + } + + @Override + public String toString() { + return "PacketWriter(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void protectHeaderLong(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x0f); + } + + public void protectHeaderShort(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x1f); + } + + private void protectHeader(int packetNumberStart, int packetNumberLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + // expect position at the end of packet + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + assert buffer.position() - packetNumberStart >= sampleSize + 4 : buffer.position() - packetNumberStart - sampleSize - 4; + + ByteBuffer sample = buffer.slice(packetNumberStart + 4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), false, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberStart, packetNumberLength, encryptedSample); + } + + private void signRetry(final int version) throws QuicTransportException { + final QuicVersion retryVersion = QuicVersion.of(version) + .orElseThrow(() -> new IllegalArgumentException("Unknown Quic version 0x" + + Integer.toHexString(version))); + int payloadend = buffer.position(); + ByteBuffer temp = buffer.asReadOnlyBuffer(); + temp.position(offset); + temp.limit(payloadend); + try { + context.getTLSEngine().signRetryPacket(retryVersion, + context.originalServerConnId().asReadOnlyBuffer(), temp, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + // generates packet header and is capable of inserting a key phase into the header + // when appropriate + private static final class HeaderGenerator implements IntFunction { + private final PacketType packetType; + private final ByteBuffer header; + + private HeaderGenerator(final PacketType packetType, final ByteBuffer header) { + this.packetType = packetType; + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyPhase) { + // we use key phase only in 1-RTT packet header + if (packetType != PacketType.ONERTT) { + assert keyPhase == 0 : "unexpected key phase " + keyPhase + + " for packet type " + packetType; + // return the packet header without setting any key phase bit + return header; + } + // update the key phase bit in the packet header + setKeyPhase(keyPhase); + return header.position(0).asReadOnlyBuffer(); + } + + private void setKeyPhase(final int kp) { + if (kp != 0 && kp != 1) { + throw new IllegalArgumentException("Invalid key phase: " + kp); + } + final byte headerFirstByte = this.header.get(); + final byte updated = (byte) (headerFirstByte | (kp << 2)); + this.header.put(0, updated); + } + } + } + + + /** + * Adds required padding frames if necessary. + * Needed to make sure there's enough bytes to apply header protection + * @param frames requested list of frames + * @param minLength requested minimum length + * @return list of frames that meets the minimum length requirement + */ + private static List padFrames(List frames, int minLength) { + if (frames.size() >= minLength) { + return frames; + } + int size = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + if (size >= minLength) { + return frames; + } + List result = new ArrayList<>(frames.size() + 1); + // add padding frame in front - some frames extend to end of packet + result.add(new PaddingFrame(minLength - size)); + result.addAll(frames); + return result; + } + + /** + * Returns an encoder for the given Quic version. + * Returns {@code null} if no encoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return an encoder for the given Quic version or {@code null} + */ + public static QuicPacketEncoder of(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Encoders.QUIC_V1_ENCODER; + case QUIC_V2 -> Encoders.QUIC_V2_ENCODER; + default -> throw new IllegalArgumentException("No packet encoder for Quic version " + quicVersion); + }; + } + + private static final class Encoders { + static final Random RANDOM = new Random(); + static final QuicPacketEncoder QUIC_V1_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V1); + static final QuicPacketEncoder QUIC_V2_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java new file mode 100644 index 00000000000..197d46fc0b0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.ByteBuffer; + +/** + * QUIC packet number encoding/decoding routines. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class QuicPacketNumbers { + + /** + * Returns the number of bytes needed to encode a packet number + * given the full packet number and the largest ACK'd packet. + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't represented in 4 bytes + * @return the number of bytes required to encode the packet + */ + public static int computePacketNumberLength(long fullPN, long largestAcked) { + + long numUnAcked; + + if (largestAcked == -1) { + numUnAcked = fullPN + 1; + } else { + numUnAcked = fullPN - largestAcked; + } + + /* + * log(n, 2) + 1; ceil(minBits / 8); + * + * value will never be non-positive, so don't need to worry about the + * special cases. + */ + assert numUnAcked > 0 : "numUnAcked %s < 0 (fullPN: %s, largestAcked: %s)" + .formatted(numUnAcked, fullPN, largestAcked); + int minBits = 64 - Long.numberOfLeadingZeros(numUnAcked) + 1; + int numBytes = (minBits + 7) / 8; + + if (numBytes > 4) { + throw new IllegalArgumentException( + "Encoded packet number needs %s bytes for pn=%s, ack=%s" + .formatted(numBytes, fullPN, largestAcked)); + } + + return numBytes; + } + + /** + * Encode the full packet number against the largest ACK'd packet. + * + * Follows the algorithm outlined in + * + * RFC 9000. Appendix A.2 + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't be represented in 4 bytes + * @return byte array containing fullPN + */ + public static byte[] encodePacketNumber( + long fullPN, long largestAcked) { + + // throws IAE if more than 4 bytes are needed + int numBytes = computePacketNumberLength(fullPN, largestAcked); + assert numBytes <= 4 : numBytes; + return truncatePacketNumber(fullPN, numBytes); + } + + /** + * Truncate the full packet number to fill into {@code numBytes}. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.2 + * + * @apiNote + * {@code numBytes} should have been computed using + * {@link #computePacketNumberLength(long, long)} + * + * @param fullPN the full packet number + * @param numBytes the number of bytes in which to encode + * the packet number + * + * @throws IllegalArgumentException if numBytes is out of range + * @return byte array containing fullPN + */ + public static byte[] truncatePacketNumber( + long fullPN, int numBytes) { + + if (numBytes <= 0 || numBytes > 4) { + throw new IllegalArgumentException( + "Invalid packet number length: " + numBytes); + } + + // Fill in the array. + byte[] retval = new byte[numBytes]; + for (int i = numBytes - 1; i >= 0; i--) { + retval[i] = (byte) (fullPN & 0xff); + fullPN = fullPN >>> 8; + } + + return retval; + } + + /** + * Decode the packet numbers against the largest ACK'd packet after header + * protection has been removed. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.3 + * + * @param largestPN the largest packet number that has been successfully + * processed in the current packet number space + * @param buf a {@code ByteBuffer} containing the value of the + * Packet Number field + * @param pnNBytes the number of bytes indicated by the Packet + * Number Length field + * + * @throws java.nio.BufferUnderflowException if there is not enough data in the + * buffer + * @return the decoded packet number + */ + public static long decodePacketNumber( + long largestPN, ByteBuffer buf, int pnNBytes) { + + assert pnNBytes >= 1 && pnNBytes <= 4 + : "decodePacketNumber: " + pnNBytes; + + long truncatedPN = 0; + for (int i = 0; i < pnNBytes; i++) { + truncatedPN = (truncatedPN << 8) | (buf.get() & 0xffL); + } + + int pnNBits = pnNBytes * 8; + + long expectedPN = largestPN + 1L; + assert expectedPN >= 0 : "expectedPN: " + expectedPN; + long pnWin = 1L << pnNBits; + long pnHWin = pnWin / 2L; + long pnMask = pnWin - 1L; + + // The incoming packet number should be greater than + // expectedPN - pn_HWin and less than or equal to + // expectedPN + pn_HWin + // + // This means we cannot just strip the trailing bits from + // expectedPN and add the truncatedPN because that might + // yield a value outside the window. + // + // The following code calculates a candidate value and + // makes sure it's within the packet number window. + // Note the extra checks to prevent overflow and underflow. + long candidatePN = (expectedPN & ~pnMask) | truncatedPN; + + if ((candidatePN <= (expectedPN - pnHWin)) + && (candidatePN < ((1L << 62) - pnWin))) { + return candidatePN + pnWin; + } + + if ((candidatePN - pnHWin > expectedPN) + && (candidatePN >= pnWin)) { + return candidatePN - pnWin; + } + return candidatePN; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java new file mode 100644 index 00000000000..51638caf98c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Retry Packets, as defined by + * RFC 9000, Section 17.2.5: + * + *

      {@code
      + *    A Retry packet uses a long packet header with a type value of 0x03.
      + *    It carries an address validation token created by the server.
      + *    It is used by a server that wishes to perform a retry; see Section 8.1.
      + *
      + *    Retry Packet {
      + *      Header Form (1) = 1,
      + *      Fixed Bit (1) = 1,
      + *      Long Packet Type (2) = 3,
      + *      Unused (4),
      + *      Version (32),
      + *      Destination Connection ID Length (8),
      + *      Destination Connection ID (0..160),
      + *      Source Connection ID Length (8),
      + *      Source Connection ID (0..160),
      + *      Retry Token (..),
      + *      Retry Integrity Tag (128),
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Retry Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see RFC 9000, Section 8.1/a> + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface RetryPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.RETRY; + } + + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.NONE; + } + + /** + * This packet type is not numbered: always returns -1L. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + + /** + * {@return the packet's retry token} + * + * As per RFC 9000, Section 17.2.5: + *

      {@code
      +     *    An opaque token that the server can use to validate the client's address.
      +     * }
      + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] retryToken(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java new file mode 100644 index 00000000000..a56689e6a0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This interface models Quic Short Header packets, as defined by + * RFC 8999, Section 5.2: + * + *
      {@code
      + *    Short Header Packet {
      + *      Header Form (1) = 0,
      + *      Version-Specific Bits (7),
      + *      Destination Connection ID (..),
      + *      Version-Specific Data (..),
      + *    }
      + * }
      + * + *

      Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ShortHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.SHORT; } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java new file mode 100644 index 00000000000..0ba5ba08c7e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Version Negotiation Packets, as defined by + * RFC 9000, Section 17.2.1: + * + *

      {@code
      + *    A Version Negotiation packet is inherently not version-specific.
      + *    Upon receipt by a client, it will be identified as a Version
      + *    Negotiation packet based on the Version field having a value of 0.
      + *
      + *    The Version Negotiation packet is a response to a client packet that
      + *    contains a version that is not supported by the server, and is only
      + *    sent by servers.
      + *
      + *    The layout of a Version Negotiation packet is:
      + *
      + *    Version Negotiation Packet {
      + *      Header Form (1) = 1,
      + *      Unused (7),
      + *      Version (32) = 0,
      + *      Destination Connection ID Length (8),
      + *      Destination Connection ID (0..2040),
      + *      Source Connection ID Length (8),
      + *      Source Connection ID (0..2040),
      + *      Supported Version (32) ...,
      + *    }
      + * }
    + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public interface VersionNegotiationPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { return PacketType.VERSIONS; } + @Override + default int version() { return 0;} + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { return PacketNumberSpace.NONE; } + /** + * This packet type is not numbered: returns -1L always. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + int[] supportedVersions(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java new file mode 100644 index 00000000000..c680aae1486 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 0-RTT Packets, as defined by + * RFC 9000, Section 17.2.3: + * + *
    {@code
    + *    A 0-RTT packet uses long headers with a type value of 0x01, followed
    + *    by the Length and Packet Number fields; see Section 17.2. The first
    + *    byte contains the Reserved and Packet Number Length bits; see Section 17.2.
    + *    A 0-RTT packet is used to carry "early" data from the client to the server
    + *    as part of the first flight, prior to handshake completion. As part of the
    + *    TLS handshake, the server can accept or reject this early data.
    + *
    + *    See Section 2.3 of [TLS13] for a discussion of 0-RTT data and its limitations.
    +
    + *    0-RTT Packet {
    + *      Header Form (1) = 1,
    + *      Fixed Bit (1) = 1,
    + *      Long Packet Type (2) = 1,
    + *      Reserved Bits (2),
    + *      Packet Number Length (2),
    + *      Version (32),
    + *      Destination Connection ID Length (8),
    + *      Destination Connection ID (0..160),
    + *      Source Connection ID Length (8),
    + *      Source Connection ID (0..160),
    + *      Length (i),
    + *      Packet Number (8..32),
    + *      Packet Payload (..),
    + *    }
    + * } 
    + * + *

    Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same 0-RTT Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @see + * [TLS13] RFC 8446, Section 2.3 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ZeroRttPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.ZERORTT; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java new file mode 100644 index 00000000000..e1a7fce1059 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An abstract class to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code AbstractQuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * both (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +abstract sealed class AbstractQuicStream implements QuicStream + permits QuicBidiStreamImpl, QuicSenderStreamImpl, QuicReceiverStreamImpl { + + private final QuicConnectionImpl connection; + private final long streamId; + private final StreamMode mode; + + AbstractQuicStream(QuicConnectionImpl connection, long streamId) { + this.mode = mode(connection, streamId); + this.streamId = streamId; + this.connection = connection; + } + + private static StreamMode mode(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return StreamMode.READ_WRITE; + if (connection.isClientConnection()) { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.WRITE_ONLY : StreamMode.READ_ONLY; + } else { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.READ_ONLY : StreamMode.WRITE_ONLY; + } + } + + /** + * {@return the {@code QuicConnectionImpl} instance this stream + * belongs to} + */ + final QuicConnectionImpl connection() { + return connection; + } + + @Override + public final long streamId() { + return streamId; + } + + @Override + public final StreamMode mode() { + return mode; + } + + @Override + public final boolean isClientInitiated() { + return QuicStreams.isClientInitiated(type()); + } + + @Override + public final boolean isServerInitiated() { + return QuicStreams.isServerInitiated(type()); + } + + @Override + public final boolean isBidirectional() { + return QuicStreams.isBidirectional(type()); + } + + @Override + public final boolean isLocalInitiated() { + return connection().isClientConnection() == isClientInitiated(); + } + + @Override + public final boolean isRemoteInitiated() { + return connection().isClientConnection() != isClientInitiated(); + } + + @Override + public final int type() { + return QuicStreams.streamType(streamId); + } + + /** + * {@return true if this stream isn't expecting anything + * from the peer and can be removed from the streams map} + */ + public abstract boolean isDone(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java new file mode 100644 index 00000000000..cbbbf6d083d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; + +/** + * Class that buffers crypto data received from QuicTLSEngine. + * Generates CryptoFrames of requested size. + * + * Normally the frames are produced sequentially. However, when the client + * receives a Retry packet or a Version Negotiation packet, the client hello + * needs to be replayed. In that case we need to keep the processed data + * in the queues. + */ +public class CryptoWriterQueue { + private final Queue queue = new ArrayDeque<>(); + private long position = 0; + // amount of bytes remaining across all the enqueued buffers + private int totalRemaining = 0; + private boolean keepReplayData; + + /** + * Notify the writer to start keeping processed data. Can only be called on a fresh writer. + * @throws IllegalStateException if some data was processed already + */ + public synchronized void keepReplayData() { + if (position > 0) { + throw new IllegalStateException("Some data was processed already"); + } + keepReplayData = true; + } + + /** + * Notify the writer to stop keeping processed data. + */ + public synchronized void discardReplayData() { + if (!keepReplayData) { + return; + } + keepReplayData = false; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.remaining() == 0) { + iterator.remove(); + } else { + return; + } + } + } + + /** + * Rewinds the enqueued buffer positions to allow for replaying the data + * @throws IllegalStateException if replay data is not available + */ + public synchronized void replayData() { + if (!keepReplayData) { + throw new IllegalStateException("Replay data not available"); + } + if (position == 0) { + return; + } + int rewound = 0; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.position() != 0) { + rewound += next.position(); + next.position(0); + } else { + break; + } + } + assert rewound == position : rewound - position; + position = 0; + totalRemaining += rewound; + } + + /** + * Clears the queue and resets position back to zero + */ + public synchronized void reset() { + position = 0; + totalRemaining = 0; + queue.clear(); + } + + /** + * Enqueues the provided crypto data + * @param buffer data to enqueue + */ + public synchronized void enqueue(ByteBuffer buffer) { + queue.add(buffer.slice()); + totalRemaining += buffer.remaining(); + } + + /** + * Stores the next portion of queued crypto data in a frame. + * May return null if there's no data to enqueue or if + * maxSize is too small to fit at least one byte of data. + * The produced frame may be shorter than maxSize even if there are + * remaining bytes. + * @param maxSize maximum size of the returned frame, in bytes + * @return frame with next portion of crypto data, or null + * @throws IllegalArgumentException if maxSize < 0 + */ + public synchronized CryptoFrame produceFrame(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException("negative maxSize"); + } + if (totalRemaining == 0) { + return null; + } + int posLength = VariableLengthEncoder.getEncodedSize(position); + // 1 (type) + posLength (position) + 1 (length) + 1 (payload) + if (maxSize < 3 + posLength) { + return null; + } + int maxPayloadPlusLen = maxSize - 1 - posLength; + int maxPayload; + if (maxPayloadPlusLen <= 64) { //63 bytes + 1 byte for length + maxPayload = maxPayloadPlusLen - 1; + } else if (maxPayloadPlusLen <= 16385) { // 16383 bytes + 2 bytes for length + maxPayload = maxPayloadPlusLen - 2; + } else { // 4 bytes for length + maxPayload = maxPayloadPlusLen - 4; + } + // the frame length that we decide upon + final int computedFrameLength = Math.min(maxPayload, totalRemaining); + assert computedFrameLength > 0 : computedFrameLength; + ByteBuffer frameData = null; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + final ByteBuffer buffer = iterator.next(); + // amount of remaining bytes in the current bytebuffer being processed + final int numRemainingInBuffer = buffer.remaining(); + if (numRemainingInBuffer == 0) { + if (!keepReplayData) { + iterator.remove(); + } + continue; + } + if (frameData == null) { + frameData = ByteBuffer.allocate(computedFrameLength); + } + if (frameData.remaining() >= numRemainingInBuffer) { + // frame data can accommodate the entire buffered data, so copy it over + frameData.put(buffer); + if (!keepReplayData) { + iterator.remove(); + } + } else { + // target frameData buffer cannot accommodate the entire buffered data, + // so we copy over only that much that the target buffer can accommodate + + // amount of data available in the target buffer + final int spaceAvail = frameData.remaining(); + // copy over the buffered data into the target frameData buffer + frameData.put(frameData.position(), buffer, buffer.position(), spaceAvail); + // manually move the position of the target buffer to account for the copied data + frameData.position(frameData.position() + spaceAvail); + // manually move the position of the (input) buffered data to account for + // data that we just copied + buffer.position(buffer.position() + spaceAvail); + // target frameData buffer is fully populated, no more processing of available + // input buffer necessary in this round + break; + } + } + assert frameData != null; + assert !frameData.hasRemaining() : frameData.remaining(); + frameData.flip(); + long oldPosition = position; + position += computedFrameLength; + totalRemaining -= computedFrameLength; + assert totalRemaining >= 0 : totalRemaining; + assert totalRemaining > 0 || keepReplayData || queue.isEmpty(); + return new CryptoFrame(oldPosition, computedFrameLength, frameData); + } + + /** + * {@return the current number of buffered bytes} + */ + public synchronized int remaining() { + return totalRemaining; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java new file mode 100644 index 00000000000..2185507d0db --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface that represents a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public non-sealed interface QuicBidiStream extends QuicStream, QuicReceiverStream, QuicSenderStream { + + /** + * The state of a bidirectional stream can be obtained by combining + * the state of its sending part and receiving part. + * + * A bidirectional stream is composed of sending and receiving + * parts. Implementations can represent states of the bidirectional + * stream as composites of sending and receiving stream states. + * The simplest model presents the stream as "open" when either + * sending or receiving parts are in a non-terminal state and + * "closed" when both sending and receiving streams are in + * terminal states. + * + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + enum BidiStreamState implements QuicStream.StreamState { + /** + * A bidirectional stream is considered "idle" if no + * data has been sent or received on that stream. + */ + IDLE, + /** + * A bidirectional stream is considered "open" until all data + * has been received, or all data has been sent, and no reset + * has been sent or received. + */ + OPENED, + /** + * A bidirectional stream is considered locally half closed + * if the sending part is locally closed: + * all data has been sent and acknowledged, or a reset has + * been sent, but the receiving part is still receiving. + */ + HALF_CLOSED_LOCAL, + /** + * A bidirectional stream is considered remotely half closed + * if the receiving part is closed: + * all data has been read or received on the receiving part, + * or reset has been read or received on the receiving part, but + * the sending part is still sending. + */ + HALF_CLOSED_REMOTE, + /** + * A bidirectional stream is considered closed when both parts + * have been reset or all data has been sent and acknowledged + * and all data has been received. + */ + CLOSED; + + /** + * @inheritDoc + * @apiNote + * A bidirectional stream may be considered closed (which is a terminal state), + * even if the sending or receiving part of a stream haven't reached a terminal + * state. Typically, if the sending part has sent a RESET frame, the stream + * may be considered closed even if the acknowledgement hasn't been received + * yet. + */ + @Override + public boolean isTerminal() { + return this == CLOSED; + } + } + + /** + * {@return a composed simplified state computed from the state of + * the receiving part and sending part of the stream} + *

    + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + default BidiStreamState getBidiStreamState() { + return switch (sendingState()) { + case READY -> switch (receivingState()) { + case RECV -> dataReceived() == 0 + ? BidiStreamState.IDLE + : BidiStreamState.OPENED; + case SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case SEND, DATA_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case DATA_RECVD, RESET_RECVD, RESET_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.HALF_CLOSED_LOCAL; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.CLOSED; + }; + }; + } + + @Override + default StreamState state() { return getBidiStreamState(); } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0 || sndErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java new file mode 100644 index 00000000000..a964d96ef9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An implementation of a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public final class QuicBidiStreamImpl extends AbstractQuicStream implements QuicBidiStream { + + // The sender part of this bidirectional stream + private final QuicSenderStreamImpl senderPart; + + // The receiver part of this bidirectional stream + private final QuicReceiverStreamImpl receiverPart; + + QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId) { + this(connection, streamId, new QuicSenderStreamImpl(connection, streamId), + new QuicReceiverStreamImpl(connection, streamId)); + } + + private QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId, + QuicSenderStreamImpl sender, QuicReceiverStreamImpl receiver) { + super(connection, streamId); + this.senderPart = sender; + this.receiverPart = receiver; + assert isBidirectional(); + } + + @Override + public ReceivingStreamState receivingState() { + return receiverPart.receivingState(); + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + return receiverPart.connectReader(scheduler); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + receiverPart.disconnectReader(reader); + } + + @Override + public void requestStopSending(long errorCode) { + receiverPart.requestStopSending(errorCode); + } + + @Override + public boolean isStopSendingRequested() { + return receiverPart.isStopSendingRequested(); + } + + @Override + public long dataReceived() { + return receiverPart.dataReceived(); + } + + @Override + public long maxStreamData() { + return receiverPart.maxStreamData(); + } + + @Override + public SendingStreamState sendingState() { + return senderPart.sendingState(); + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return senderPart.connectWriter(scheduler); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + senderPart.disconnectWriter(writer); + } + + @Override + public void reset(long errorCode) throws IOException { + senderPart.reset(errorCode); + } + + @Override + public long dataSent() { + return senderPart.dataSent(); + } + + /** + * {@return the sender part implementation of this bidirectional stream} + */ + public QuicSenderStreamImpl senderPart() { + return senderPart; + } + + /** + * {@return the receiver part implementation of this bidirectional stream} + */ + public QuicReceiverStreamImpl receiverPart() { + return receiverPart; + } + + @Override + public boolean isDone() { + return receiverPart.isDone() && senderPart.isDone(); + } + + @Override + public long rcvErrorCode() { + return receiverPart.rcvErrorCode(); + } + + @Override + public long sndErrorCode() { + return senderPart.sndErrorCode(); + } + + @Override + public boolean stopSendingReceived() { + return senderPart.stopSendingReceived(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java new file mode 100644 index 00000000000..3b6198682df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java @@ -0,0 +1,1590 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; +import jdk.internal.net.http.quic.streams.QuicStream.StreamMode; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicTransportErrors; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.streams.QuicStreams.*; + +/** + * A helper class to help manage Quic streams in a Quic connection + */ +public final class QuicConnectionStreams { + + // the (sliding) window size of MAX_STREAMS limit + private static final long MAX_BIDI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_BIDI_STREAMS; + private static final long MAX_UNI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_UNI_STREAMS; + + // These atomic long ids record the expected next stream ID that + // should be allocated for the next stream of a given type. + // The type of a stream is a number in [0..3], and is used + // as an index in this list. + private final List nextStreamID = List.of( + new AtomicLong(), // 0: client initiated bidi + new AtomicLong(SRV_MASK), // 1: server initiated bidi + new AtomicLong(UNI_MASK), // 2: client initiated uni + new AtomicLong(UNI_MASK | SRV_MASK)); // 3: server initiated uni + + // the max uni streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localUniMaxStreamLimit = new StreamCreationPermit(0); + // the max bidi streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localBidiMaxStreamLimit = new StreamCreationPermit(0); + // the max uni streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteUniMaxStreamLimit = new AtomicLong(0); + // the max bidi streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteBidiMaxStreamLimit = new AtomicLong(0); + + private final StreamsContainer streams = new StreamsContainer(); + + // A collection of senders which have available data ready to send, (or which possibly + // are blocked and need to send STREAM_DATA_BLOCKED). + // A stream stays in the queue until it is blocked or until it + // has no more data available to send: when a stream has no more data available for sending it is not + // put back in the queue. It will be put in the queue again when selectForSending is called. + private final ReadyStreamCollection sendersReady; + + // A map that contains streams for which sending a RESET_STREAM frame was requested + // and their corresponding error codes. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap sendersReset = new ConcurrentHashMap<>(); + + // A map that contains streams for which sending a MAX_STREAM_DATA frame was requested. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap receiversSend = new ConcurrentHashMap<>(); + + // A queue of remote initiated streams that have not been acquired yet. + // see pollNewRemoteStreams and addRemoteStreamListener + private final ConcurrentLinkedQueue newRemoteStreams = new ConcurrentLinkedQueue<>(); + // A set of listeners listening to new streams created by the peer + private final Set> streamListeners = ConcurrentHashMap.newKeySet(); + // A lock to ensure consistency between invocation of streamListeners and + // the content of the newRemoteStreams queue. + private final Lock newRemoteStreamsLock = new ReentrantLock(); + + // The connection to which the streams managed by this + // instance of QuicConnectionStreams belong to. + private final QuicConnectionImpl connection; + + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for uni + // streams. this indicates the peer isn't able to create any more uni streams, past this limit + private final AtomicLong peerUniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for bidi + // streams. this indicates the peer isn't able to create any more bidi streams, past this limit + private final AtomicLong peerBidiStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a uni stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong uniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a bidi stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong bidiStreamsBlocked = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for uni streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "uniStreamsBlocked" exceeds this + // "lastUniStreamsBlockedSent" + private final AtomicLong lastUniStreamsBlockedSent = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for bidi streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "bidiStreamsBlocked" exceeds this + // "lastBidiStreamsBlockedSent" + private final AtomicLong lastBidiStreamsBlockedSent = new AtomicLong(-1); + // streams that have been blocked and aren't able to send data to the peer, + // due to reaching flow control limit imposed on those streams by the peer. + private final Set flowControlBlockedStreams = Collections.synchronizedSet(new HashSet<>()); + + private final Logger debug; + + // A QuicConnectionStream instance can be tied to a client connection + // or a server connection. + // If the connection is a client connection, then localFlag=0x00, + // localBidi=0x00, remoteBidi=0x01, localUni=0x02, remoteUni=0x03 + // If the connection is a server connection, then localFlag=0x01, + // localBidi=0x01, remoteBidi=0x00, localUni=0x03, remoteUni=0x02 + private final int localFlag, localBidi, remoteBidi, localUni, remoteUni; + + /** + * Creates a new instance of {@code QuicConnectionStreams} for the + * given connection. There is a 1-1 relationship between a + * {@code QuicConnectionImpl} instance and a {@code QuicConnectionStreams} + * instance. + * @param connection the connection to which the streams managed by this + * instance of {@code QuicConnectionStreams} belong. + */ + public QuicConnectionStreams(QuicConnectionImpl connection, Logger debug) { + this.connection = connection; + this.debug = Objects.requireNonNull(debug); + // implicit null check for connection + boolean isClient = connection.isClientConnection(); + localFlag = isClient ? 0 : SRV_MASK; + localBidi = isClient ? 0 : SRV_MASK; + remoteBidi = isClient ? SRV_MASK : 0; + localUni = isClient ? UNI_MASK : UNI_MASK | SRV_MASK; + remoteUni = isClient ? UNI_MASK | SRV_MASK : UNI_MASK; + sendersReady = isClient ? + new ReadyStreamQueue() : // faster stream opening + new ReadyStreamSortedQueue(); // faster stream closing + } + + /** + * {@return the next unallocated stream ID that would be expected + * for a stream of the given type} + * This method expects {@code streamType} to be a number in [0..3] but + * does not check it. An assert may be fired if an invalid type is passed. + * @param streamType The stream type, a number in [0..3] + */ + public long peekNextStreamId(int streamType) { + assert streamType >= 0 && streamType < 4; + var id = nextStreamID.get(streamType & 0x03); + return id.get(); + } + + /** + * Creates a new locally initiated unidirectional stream. + *

    + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicSenderStream} + */ + public CompletableFuture createNewLocalUniStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localUni, + StreamMode.WRITE_ONLY, timeout); + return streamCF; + } + + /** + * Creates a new locally initiated bidirectional stream. + *

    + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicBidiStream} + */ + public CompletableFuture createNewLocalBidiStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localBidi, + StreamMode.READ_WRITE, timeout); + return streamCF; + } + + private void register(long streamId, AbstractQuicStream stream) { + var previous = streams.put(streamId, stream); + assert previous == null : "stream " + streamId + " is already registered!"; + QuicTransportParameters peerParameters = connection.peerTransportParameters(); + if (peerParameters != null) { + if (debug.on()) { + debug.log("setting initial peer parameters on stream " + streamId); + } + newInitialPeerParameters(stream, peerParameters); + } + QuicTransportParameters localParameters = connection.localTransportParameters(); + if (localParameters != null) { + if (debug.on()) { + debug.log("setting initial local parameters on stream " + streamId); + } + newInitialLocalParameters(stream, localParameters); + } + if (stream instanceof QuicReceiverStream receiver && stream.isRemoteInitiated()) { + if (debug.on()) { + debug.log("accepting remote stream " + streamId); + } + newRemoteStreams.add(receiver); + acceptRemoteStreams(); + } + if (debug.on()) { + debug.log("new stream %s %s registered", streamId, stream.mode()); + } + } + + private void acceptRemoteStreams() { + newRemoteStreamsLock.lock(); + try { + for (var listener : streamListeners) { + var iterator = newRemoteStreams.iterator(); + while (iterator.hasNext()) { + var stream = iterator.next(); + if (debug.on()) { + debug.log("invoking remote stream listener for stream %s", + stream.streamId()); + } + if (listener.test(stream)) iterator.remove(); + } + } + } finally { + newRemoteStreamsLock.unlock(); + } + } + + private CompletableFuture createNewLocalStream( + final int localType, final StreamMode mode, final Duration timeout) { + assert localType >= 0 && localType < 4 : "bad local stream type " + localType; + assert (localType & SRV_MASK) == localFlag : "bad local stream type " + localType; + assert (localType & UNI_MASK) == 0 || mode == StreamMode.WRITE_ONLY + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + assert (localType & UNI_MASK) == UNI_MASK || mode == StreamMode.READ_WRITE + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + final boolean bidi = isBidirectional(localType); + final StreamCreationPermit permit = bidi ? this.localBidiMaxStreamLimit + : this.localUniMaxStreamLimit; + final CompletableFuture permitAcquisitionCF; + final long currentLimit = permit.currentLimit(); + final boolean acquired = permit.tryAcquire(); + if (acquired) { + permitAcquisitionCF = MinimalFuture.completedFuture(true); + } else { + // stream limit reached, request sending a STREAMS_BLOCKED frame + announceStreamsBlocked(bidi, currentLimit); + if (timeout.isPositive()) { + final Executor executor = this.connection.quicInstance().executor(); + if (debug.on()) { + debug.log("stream creation limit = " + permit.currentLimit() + + " reached; waiting for it to increase, timeout=" + timeout); + } + permitAcquisitionCF = permit.tryAcquire(timeout.toNanos(), NANOSECONDS, executor); + } else { + permitAcquisitionCF = MinimalFuture.completedFuture(false); + } + } + final CompletableFuture streamCF = + permitAcquisitionCF.thenCompose((acq) -> { + if (!acq) { + final String msg = "Stream limit = " + permit.currentLimit() + + " reached for locally initiated " + + (bidi ? "bidi" : "uni") + " streams"; + return MinimalFuture.failedFuture(new QuicStreamLimitException(msg)); + } + // stream limit hasn't been reached, we are allowed to create new one + final long streamId = nextStreamID.get(localType).getAndAdd(4); + final AbstractQuicStream stream = QuicStreams.createStream(connection, streamId); + assert stream.mode() == mode; + assert stream.type() == localType; + if (debug.on()) { + var strtype = (localType & UNI_MASK) == UNI_MASK ? "uni" : "bidi"; + debug.log("created new local %s stream type:%s, mode:%s, id:%s", + strtype, localType, mode, streamId); + } + register(streamId, stream); + return MinimalFuture.completedFuture(stream); + }); + return streamCF; + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, + * to potentially trigger sending a STREAMS_BLOCKED frame to the peer + * @param bidi true if the local endpoint is blocked for bidi streams, false for uni streams + * @param blockedOnLimit the stream creation limit due to which the local endpoint is + * currently blocked + */ + private void announceStreamsBlocked(final boolean bidi, final long blockedOnLimit) { + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.bidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.bidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.bidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.uniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.uniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.uniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission to send " + (bidi ? "bidi" : "uni") + + " STREAMS_BLOCKED with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, to potentially trigger + * sending a MAX_STREAMS frame to the peer, upon receiving the STREAMS_BLOCKED {@code frame} + * from that peer + * @param frame the STREAMS_BLOCKED frame that was received from the peer + */ + public void peerStreamsBlocked(final StreamsBlockedFrame frame) { + final boolean bidi = frame.isBidi(); + final long blockedOnLimit = frame.maxStreams(); + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerBidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.peerUniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerUniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerUniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission in response to receiving " + + (bidi ? "bidi" : "uni") + " STREAMS_BLOCKED from peer," + + " blocked with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Gets or opens a remotely initiated stream with the given stream ID. + * Creates all streams with lower IDs if needed. + * @param streamId the stream ID + * @param frameType type of the frame received, used in exceptions + * @return a remotely initiated stream with the given stream ID. + * May return null if the stream was already closed. + * @throws IllegalArgumentException if the streamID is of the wrong type for + * a remote stream. + * @throws QuicTransportException if the streamID is higher than allowed + */ + public QuicStream getOrCreateRemoteStream(long streamId, long frameType) + throws QuicTransportException { + final int streamType = streamType(streamId); + if ((streamId & SRV_MASK) == localFlag) { + throw new IllegalArgumentException("bad remote stream type %s for stream %s" + .formatted(streamType, streamId)); + } + final boolean bidi = isBidirectional(streamId); + final long maxStreamLimit = bidi ? this.remoteBidiMaxStreamLimit.get() + : this.remoteUniMaxStreamLimit.get(); + if (maxStreamLimit <= (streamId >> 2)) { + throw new QuicTransportException("stream ID %s exceeds the number of allowed streams(%s)" + .formatted(streamId, maxStreamLimit), QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_LIMIT_ERROR); + } + + newRemoteStreamsLock.lock(); + try { + var id = nextStreamID.get(streamType); + long nextId = id.get(); + if (nextId > streamId) { + // already created + return streams.get(streamId); + } + // id must not be modified outside newRemoteStreamsLock + long altId = id.getAndSet(streamId + 4); + assert altId == nextId : "next ID concurrently modified"; + + AbstractQuicStream stream = null; + for (long i = nextId; i <= streamId; i += 4) { + stream = QuicStreams.createStream(connection, i); + assert stream.isRemoteInitiated(); + register(i, stream); + } + assert stream != null; + assert stream.streamId() == streamId : stream.streamId(); + return stream; + } finally { + newRemoteStreamsLock.unlock(); + } + } + + + /** + * Finds a stream with the given stream ID. Returns {@code null} if no + * stream with that ID is found. + * @param streamId a stream ID + * @return the stream with the given stream ID if found, {@code null} + * otherwise. + */ + public QuicStream findStream(long streamId) { + return streams.get(streamId); + } + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) + * stream type} of the given streamId. + * The listener should return true if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + * + */ + public void addRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + streamListeners.add(streamConsumer); + acceptRemoteStreams(); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + return streamListeners.remove(streamConsumer); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * {@return a stream of all currently active {@link QuicStream} in the connection} + */ + public Stream quicStreams() { + return streams.all(); + } + + /** + * {@return {@code true} if there is some data to send} + * @apiNote + * This method may return true in the case where a + * STREAM_DATA_BLOCKED frame needs to be sent, even if no + * other data is available. + */ + public boolean hasAvailableData() { + return !sendersReady.isEmpty(); + } + + /** + * {@return true if there are control frames to send} + * Typically, these are STREAMS_BLOCKED, MAX_STREAMS, RESET_STREAM, STOP_SENDING, and + * MAX_STREAM_DATA. + */ + public boolean hasControlFrames() { + return !sendersReset.isEmpty() || !receiversSend.isEmpty() + // either of these imply we may send a MAX_STREAMS frame + || peerUniStreamsBlocked.get() != -1 || peerBidiStreamsBlocked.get() != -1 + // either of these imply we should send a STREAMS_BLOCKED frame + || uniStreamsBlocked.get() > lastUniStreamsBlockedSent.get() + || bidiStreamsBlocked.get() > lastBidiStreamsBlockedSent.get(); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a receiving part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or peer-initiated. + * @param streamId a stream ID + */ + public boolean isReceivingStream(long streamId) { + return !isLocalUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a sending part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or local-initiated. + * @param streamId a stream ID + */ + public boolean isSendingStream(long streamId) { + return !isRemoteUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * unidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalUni(long streamId) { + return streamType(streamId) == localUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * bidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalBidi(long streamId) { + return streamType(streamId) == localBidi; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated unidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteUni(long streamId) { + return streamType(streamId) == remoteUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated bidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteBidi(long streamId) { + return streamType(streamId) == remoteBidi; + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code ResetStreamFrame} as needing a RESET_STREAM frame to be sent. + * It will put the stream and the frame in the {@code sendersReset} map. + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert isSendingStream(streamId); + var stream = senderImpl(streams.get(streamId)); + if (stream == null) { + if (debug.on()) { + debug.log("Can't reset stream %d: no such stream", streamId); + } + return; + } + sendersReset.putIfAbsent(stream, errorCode); + if (debug.on()) { + debug.log("Reset stream scheduled"); + } + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code MaxStreamDataFrame} as needing a MAX_STREAM_DATA frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param maxStreamDataFrame the MAX_STREAM_DATA frame to send + */ + public void requestSendMaxStreamData(MaxStreamDataFrame maxStreamDataFrame) { + Objects.requireNonNull(maxStreamDataFrame, "maxStreamDataFrame"); + long streamId = maxStreamDataFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send MaxStreamDataFrame %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't replace a stop sending frame, and don't replace + // a max stream data frame if it has a bigger max stream data + receiversSend.compute(receiver, (s, frame) -> { + if (frame instanceof StopSendingFrame stopSendingFrame) { + assert s.streamId() == stopSendingFrame.streamID(); + // no need to send max data frame if we are requesting + // stop sending + return frame; + } + if (frame instanceof MaxStreamDataFrame maxFrame) { + assert s.streamId() == maxFrame.streamID(); + if (maxFrame.maxStreamData() > maxStreamDataFrame.maxStreamData()) { + // send the frame that has the greater max data + return maxFrame; + } else return maxStreamDataFrame; + } + assert frame == null; + return maxStreamDataFrame; + }); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + maxStreamDataFrame.getClass(), streamId); + } + } + } + + + /** + * Mark the stream whose ID is encoded in the given + * {@code StopSendingFrame} as needing a STOP_SENDING frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param stopSendingFrame the STOP_SENDING frame to send + */ + public void scheduleStopSendingFrame(StopSendingFrame stopSendingFrame) { + Objects.requireNonNull(stopSendingFrame, "stopSendingFrame"); + long streamId = stopSendingFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send STOP_SENDING to stream %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't need to check if we already have a frame registered: + // stop sending takes precedence. + receiversSend.put(receiver, stopSendingFrame); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + stopSendingFrame.getClass(), streamId); + } + } + } + + /** + * Called when the RESET_STREAM frame is acknowledged by the peer. + * @param reset the RESET_STREAM frame + */ + public void streamResetAcknowledged(ResetStreamFrame reset) { + Objects.requireNonNull(reset, "reset"); + long streamId = reset.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.resetAcknowledged(reset.finalSize()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "resetAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged reset for stream %d", streamId); + } + } + } + + /** + * Called when the final STREAM frame is acknowledged by the peer. + * @param streamFrame the final STREAM frame + */ + public void streamDataSentAcknowledged(StreamFrame streamFrame) { + long streamId = streamFrame.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + assert streamFrame.isLast(); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.dataAcknowledged(streamFrame.offset() + streamFrame.dataLength()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "dataAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged data for stream %d", streamId); + } + } + } + + /** + * Tracks a stream, belonging to this connection, as being blocked from sending data + * due to flow control limit. + * + * @param streamId the stream id + */ + final void trackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.add(streamId); + } + + /** + * Stops tracking a stream, belonging to this connection, that may have been previously + * tracked as being blocked due to flow control limit. + * + * @param streamId the stream id + */ + final void untrackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.remove(streamId); + } + + + /** + * Removes a stream from the stream map after its state has been + * switched to DATA_RECVD or RESET_RECVD + * @param streamId the stream id + * @param stream the stream instance + */ + private void removeStream(long streamId, QuicStream stream) { + // if we were tracking this stream as blocked due to flow control, then + // stop tracking the stream. + untrackBlockedStream(streamId); + if (stream instanceof AbstractQuicStream astream) { + if (astream.isDone()) { + if (debug.on()) { + debug.log("Removing stream %d (%s)", + stream.streamId(), stream.getClass().getSimpleName()); + } + streams.remove(streamId, astream); + if (stream.isRemoteInitiated()) { + // the queue is not expected to contain many elements. + newRemoteStreams.remove(stream); + if (shouldSendMaxStreams(stream.isBidirectional())) { + this.connection.runAppPacketSpaceTransmitter(); + } + } + } else { + if (debug.on()) { + debug.log("Can't remove stream yet: %d (%s) is %s", + stream.streamId(), stream.getClass().getSimpleName(), + stream.state()); + } + } + } + assert stream instanceof AbstractQuicStream + : "stream %s: unexpected stream class: %s" + .formatted(streamId, stream.getClass()); + } + + /** + * Called when new local transport parameters are available + * @param params the new local transport parameters + */ + public void newLocalTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the remote peer by the local endpoint + final long newRemoteUniMax = params.getIntParameter(ParameterId.initial_max_streams_uni); + tryIncreaseLimitTo(this.remoteUniMaxStreamLimit, newRemoteUniMax); + final long newRemoteBidiMax = params.getIntParameter(ParameterId.initial_max_streams_bidi); + tryIncreaseLimitTo(this.remoteBidiMaxStreamLimit, newRemoteBidiMax); + streams.all().forEach(s -> newInitialLocalParameters(s, params)); + } + + /** + * Called when new peer transport parameters are available + * @param params the new local transport parameters + */ + public void newPeerTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the local endpoint by the remote peer + final long localUniMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_uni); + if (debug.on()) { + debug.log("increasing localUniMaxStreamLimit to initial_max_streams_uni: " + + localUniMaxStreams); + } + this.localUniMaxStreamLimit.tryIncreaseLimitTo(localUniMaxStreams); + final long localBidiMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_bidi); + if (debug.on()) { + debug.log("increasing localBidiMaxStreamLimit to initial_max_streams_bidi: " + + localBidiMaxStreams); + } + this.localBidiMaxStreamLimit.tryIncreaseLimitTo(localBidiMaxStreams); + // set initial parameters on streams + streams.all().forEach(s -> newInitialPeerParameters(s, params)); + if (debug.on()) { + debug.log("all streams updated (%s)", streams.streams.size()); + } + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialPeerParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isLocalUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + // remote for the peer is local for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + // local for the peer is remote for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + senderImpl(stream).setMaxStreamData(maxData); + } + } + } + + private static boolean tryIncreaseLimitTo(final AtomicLong limit, final long newLimit) { + long currentLimit = limit.get(); + while (currentLimit < newLimit) { + if (limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = limit.get(); + } + return false; + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialLocalParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isRemoteUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } + } + + /** + * Set max stream data for a stream. + * Called when a {@link jdk.internal.net.http.quic.frames.MaxStreamDataFrame + * MaxStreamDataFrame} is received. + * @param stream the stream + * @param maxStreamData the max data that the peer is willing to accept on this stream + */ + public void setMaxStreamData(QuicSenderStream stream, long maxStreamData) { + var sender = senderImpl(stream); + if (sender != null) { + final long newFinalizedLimit = sender.setMaxStreamData(maxStreamData); + // if the connection was tracking this stream as blocked due to flow control + // and if this new MAX_STREAM_DATA limit unblocked this stream, then + // stop tracking the stream. + if (newFinalizedLimit == maxStreamData) { // the proposed limit was accepted + if (!sender.isBlocked()) { + untrackBlockedStream(stream.streamId()); + } + } + } + } + + /** + * This method is called when a {@link + * jdk.internal.net.http.quic.frames.StopSendingFrame} is received + * from the peer. + * @param stream the stream for which stop sending was requested + * by the peer + * @param errorCode the error code + */ + public void stopSendingReceived(QuicSenderStream stream, long errorCode) { + var sender = senderImpl(stream); + if (sender != null) { + // if the stream was being tracked as blocked from sending data, + // due to flow control limits imposed by the peer, then we now + // stop tracking it since the peer no longer wants us to send data + // on this stream. + untrackBlockedStream(stream.streamId()); + sender.stopSendingReceived(errorCode); + } + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + var stream = streams.get(streamId); + if (stream != null) { + removeStream(streamId, stream); + } + } + + /** + * Called when the connection is closed by the higher level + * protocol + * @param terminationCause the termination cause + */ + public void terminate(final TerminationCause terminationCause) { + assert terminationCause != null : "termination cause is null"; + // make sure all active streams are woken up when we close a connection + streams.all().forEach((stream) -> { + if (stream instanceof QuicSenderStream) { + var sender = senderImpl(stream); + try { + sender.terminate(terminationCause); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to close sender stream %s: %s", sender.streamId(), t); + } + } + } + if (stream instanceof QuicReceiverStream) { + var receiver = receiverImpl(stream); + try { + receiver.terminate(terminationCause); + } catch (Throwable t) { + // log and ignore + if (debug.on()) { + debug.log("failed to close receiver stream %s: %s", receiver.streamId(), t); + } + } + } + }); + } + + /** + * This method is called by when a stream has data available for sending. + * + * @param streamId the stream id of the stream which is ready + * @see QuicConnectionImpl#streamDataAvailableForSending + */ + public void enqueueForSending(long streamId) { + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) + debug.log("WARNING: stream %d not found", streamId); + return; + } + if (stream instanceof QuicSenderStream sender) { + // No need to check/assert the presence of this sender in the queue. + // In fact there is no guarantee that the sender isn't already in the + // queue, since the scheduler loop can also put it back into the queue, + // if for example, not everything that the sender wanted to send could + // fit in the quic packet. + sendersReady.add(sender); + } else { + String msg = String.format("Stream %s not a sending or bidi stream: %s", + streamId, stream.getClass().getName()); + if (debug.on()) { + debug.log("WARNING: " + msg); + } + throw new AssertionError(msg); + } + } + + /** + * If there are any streams in this connection that have been blocked from sending + * data due to flow control limit on that stream, then this method enqueues a + * {@code STREAM_DATA_BLOCKED} frame to be sent for each such stream. + */ + public final void enqueueStreamDataBlocked() { + connection.streamDataAvailableForSending(this.flowControlBlockedStreams); + } + + /** + * {@return the sender part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a sending part + * (that is, if it is a unidirectional peer initiated stream). + * @param stream a sending or bidirectional stream + */ + QuicSenderStreamImpl senderImpl(QuicStream stream) { + if (stream instanceof QuicSenderStreamImpl sender) { + return sender; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.senderPart(); + } + return null; + } + + /** + * {@return the receiver part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a receiver part + * (that is, if it is a unidirectional local initiated stream). + * @param stream a receiving or bidirectional stream + */ + QuicReceiverStreamImpl receiverImpl(QuicStream stream) { + if (stream instanceof QuicReceiverStreamImpl receiver) { + return receiver; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.receiverPart(); + } + return null; + } + + /** + * Called when a StreamFrame is received. + * @param stream the stream for which the StreamFrame was received + * @param frame the stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, StreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingFrame(frame); + } + + /** + * Called when a ResetStreamFrame is received. + * @param stream the stream for which the ResetStreamFrame was received + * @param frame the reset stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, ResetStreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingResetFrame(frame); + } + + public void processIncomingFrame(final QuicStream stream, final StreamDataBlockedFrame frame) { + assert stream.streamId() == frame.streamId() : "unexpected stream id " + frame.streamId() + + " in frame, expected " + stream.streamId(); + final QuicReceiverStreamImpl rcvrStream = receiverImpl(stream); + assert rcvrStream != null : "missing receiver stream for stream " + stream.streamId(); + rcvrStream.processIncomingFrame(frame); + } + + public boolean tryIncreaseStreamLimit(final MaxStreamsFrame maxStreamsFrame) { + final StreamCreationPermit permit = maxStreamsFrame.isBidi() + ? localBidiMaxStreamLimit : localUniMaxStreamLimit; + final long newLimit = maxStreamsFrame.maxStreams(); + if (debug.on()) { + if (maxStreamsFrame.isBidi()) { + debug.log("increasing localBidiMaxStreamLimit limit to: " + newLimit); + } else { + debug.log("increasing localUniMaxStreamLimit limit to: " + newLimit); + } + } + return permit.tryIncreaseLimitTo(newLimit); + } + + /** + * Checks whether any stream needs to have a STOP_SENDING, RESET_STREAM or any connection + * control frames like STREAMS_BLOCKED, MAX_STREAMS sent and adds the frame to the list + * if there's room. + * @param frames list of frames + * @param remaining maximum number of bytes that can be added by this method + * @return number of bytes actually added + */ + private long checkResetAndOtherControls(List frames, long remaining) { + if (debug.on()) + debug.log("checking reset and other control frames..."); + long added = 0; + // check STREAMS_BLOCKED, only send it if the local endpoint is blocked on a limit + // for which we haven't yet sent a STREAMS_BLOCKED + final long uniStreamsBlockedLimit = this.uniStreamsBlocked.get(); + final long lastUniStreamsBlockedSent = this.lastUniStreamsBlockedSent.get(); + if (uniStreamsBlockedLimit != -1 && uniStreamsBlockedLimit > lastUniStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(false, uniStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for uni streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastUniStreamsBlockedSent.set(frame.maxStreams()); + } + } + final long bidiStreamsBlockedLimit = this.bidiStreamsBlocked.get(); + final long lastBidiStreamsBlockedSent = this.lastBidiStreamsBlockedSent.get(); + if (bidiStreamsBlockedLimit != -1 && bidiStreamsBlockedLimit > lastBidiStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(true, bidiStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for bidi streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastBidiStreamsBlockedSent.set(frame.maxStreams()); + } + } + // check STOP_SENDING and MAX_STREAM_DATA + var rcvIterator = receiversSend.entrySet().iterator(); + while (rcvIterator.hasNext()) { + var entry = rcvIterator.next(); + var frame = entry.getValue(); + if (frame.size() > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for %s", + entry.getKey().streamId(), frame); + } + break; + } + var receiver = receiverImpl(entry.getKey()); + var size = checkSendControlFrame(receiver, frame, frames); + if (size > 0) { + added += size; + } + rcvIterator.remove(); + } + + // check RESET_STREAM + var sndIterator = sendersReset.entrySet().iterator(); + while (sndIterator.hasNext()) { + Map.Entry entry = sndIterator.next(); + var sender = senderImpl(entry.getKey()); + assert sender != null; + long finalSize = sender.dataSent(); + ResetStreamFrame frame = new ResetStreamFrame(sender.streamId(), entry.getValue(), finalSize); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for ResetFrame", + sender.streamId()); + } + break; + } + if (debug.on()) + debug.log("Stream %s: Adding ResetFrame", sender.streamId()); + frames.add(frame); + added += size; + sender.resetSent(); + sndIterator.remove(); + } + + if (remaining - added > 18) { + // add MAX_STREAMS if necessary + added += addMaxStreamsFrame(frames, false); + added += addMaxStreamsFrame(frames, true); + } + return added; + } + + private boolean shouldSendMaxStreams(final boolean bidi) { + final boolean rcvdStreamsBlocked = bidi + ? this.peerBidiStreamsBlocked.get() != -1 + : this.peerUniStreamsBlocked.get() != -1; + // if we either received a STREAMS_BLOCKED from the peer for that stream type + // or if our internal algorithm decides that the peer is about to reach the stream + // creation limit + return rcvdStreamsBlocked || nextMaxStreamsLimit(bidi) > 0; + } + + private long addMaxStreamsFrame(final List frames, final boolean bidi) { + final long newMaxStreamsLimit = connection.nextMaxStreamsLimit(bidi); + if (newMaxStreamsLimit == 0) { + return 0; + } + final boolean limitIncreased; + if (bidi) { + limitIncreased = tryIncreaseLimitTo(remoteBidiMaxStreamLimit, newMaxStreamsLimit); + } else { + limitIncreased = tryIncreaseLimitTo(remoteUniMaxStreamLimit, newMaxStreamsLimit); + } + if (!limitIncreased) { + return 0; + } + final MaxStreamsFrame frame = new MaxStreamsFrame(bidi, newMaxStreamsLimit); + frames.add(frame); + // now that we are sending MAX_STREAMS frame to the peer, reset the relevant + // STREAMS_BLOCKED flag that we might have set when/if we had received a STREAMS_BLOCKED + // from the peer + if (bidi) { + this.peerBidiStreamsBlocked.set(-1); + } else { + this.peerUniStreamsBlocked.set(-1); + } + if (debug.on()) { + debug.log("Increasing max remote %s streams to %s", + bidi ? "bidi" : "uni", newMaxStreamsLimit); + } + return frame.size(); + } + + public long nextMaxStreamsLimit(final boolean bidi) { + return bidi ? streams.remoteBidiNextMaxStreams : streams.remoteUniNextMaxStreams; + } + + /** + * {@return true if there are any streams on this connection which are blocked from + * sending data due to flow control limit, false otherwise} + */ + public final boolean hasBlockedStreams() { + return !this.flowControlBlockedStreams.isEmpty(); + } + + /** + * Checks whether the given stream is recorded as needing a control + * frame to be sent, and if so, add that frame to the list + * + * @param receiver the receiver part of the stream + * @param frame the frame to send + * @param frames list of frames + * @return size of the added frame, or zero if no frame was added + * @apiNote Typically, the control frame that is sent is either a MAX_STREAM_DATA + * or a STOP_SENDING frame + */ + private long checkSendControlFrame(QuicReceiverStreamImpl receiver, + QuicFrame frame, + List frames) { + if (frame == null) { + if (debug.on()) + debug.log("Stream %s: no receiver frame to send", receiver.streamId()); + return 0; + } + if (frame instanceof MaxStreamDataFrame maxStreamDataFrame) { + if (receiver.receivingState() == ReceivingStreamState.RECV) { + // if we know the final size, no point in increasing max data + if (debug.on()) + debug.log("Stream %s: Adding MaxStreamDataFrame", receiver.streamId()); + frames.add(frame); + receiver.updateMaxStreamData(maxStreamDataFrame.maxStreamData()); + return frame.size(); + } + return 0; + } else if (frame instanceof StopSendingFrame) { + if (debug.on()) + debug.log("Stream %s: Adding StopSendingFrame", receiver.streamId()); + frames.add(frame); + return frame.size(); + } else { + throw new InternalError("Should not reach here - not a control frame: " + frame); + } + } + + /** + * Package available data in {@link StreamFrame} instances and add them + * to the provided frames list. Additional frames, like connection control frames + * {@code STREAMS_BLOCKED}, {@code MAX_STREAMS} or stream flow control frames like + * {@code STREAM_DATA_BLOCKED} may also be added if space allows. The {@link StreamDataBlockedFrame} + * is added only once for a given stream, until the stream becomes ready again. + * @implSpec + * The total cumulated size of the returned frames must not exceed {@code maxSize}. + * The total cumulated lengths of the returned frames must not exceed {@code maxConnectionData}. + * + * @param encoder the {@link QuicPacketEncoder}, used if anything is quic version + * dependent. + * @param maxSize the cumulated maximum size of all the frames + * @param maxConnectionData the maximum number of stream data bytes that can + * be packaged to respect connection flow control + * constraints + * @param frames a list of frames in which to add the packaged data + * @return the total number of stream data bytes packaged in the created + * frames. This will not exceed the given {@code maxConnectionData}. + */ + public long produceFramesToSend(QuicPacketEncoder encoder, long maxSize, + long maxConnectionData, List frames) + throws QuicTransportException { + long remaining = maxSize; + long produced = 0; + try { + remaining -= checkResetAndOtherControls(frames, remaining); + // scan the streams and compose a list of frames - possibly including + // stream data blocked frames, + QuicSenderStreamImpl sender; + NEXT_STREAM: while ((sender = senderImpl(sendersReady.poll())) != null) { + long streamId = sender.streamId(); + boolean stillReady = true; + try { + do { + if (remaining == 0 || maxConnectionData == 0) break; + var state = sender.sendingState(); + switch (state) { + case SEND -> { + long offset = sender.dataSent(); + int headerSize = StreamFrame.headerSize(encoder, streamId, offset, remaining); + if (headerSize >= remaining) { + break NEXT_STREAM; + } + long maxControlled = Math.min(maxConnectionData, remaining - headerSize); + int maxData = (int) Math.min(Integer.MAX_VALUE, maxControlled); + if (maxData <= 0) { + break NEXT_STREAM; + } + ByteBuffer buffer = sender.poll(maxData); + if (buffer != null) { + int length = buffer.remaining(); + assert length <= remaining; + assert length <= maxData; + long streamSize = sender.streamSize(); + boolean fin = streamSize >= 0 && streamSize == offset + length; + if (fin) { + stillReady = false; + } + if (length > 0 || fin) { + StreamFrame frame = new StreamFrame(streamId, offset, length, fin, buffer); + int size = frame.size(); + assert size <= remaining : "stream:%s: size %s > remaining %s" + .formatted(streamId, size, remaining); + if (debug.on()) { + debug.log("stream:%s Adding StreamFrame: %s", + streamId, frame); + } + frames.add(frame); + remaining -= size; + produced += length; + maxConnectionData -= length; + } + } + var blocked = sender.isBlocked(); + if (blocked) { + // track this stream as blocked due to flow control + trackBlockedStream(streamId); + final var dataBlocked = new StreamDataBlockedFrame(streamId, sender.dataSent()); + // This might produce multiple StreamDataBlocked frames + // if the stream was added to sendersReady multiple times, so + // we check before actually sending a STREAM_DATA_BLOCKED frame + if (!frames.contains(dataBlocked)) { + var fdbSize = dataBlocked.size(); + if (dataBlocked.size() > remaining) { + // keep the stream in the ready list if we haven't been + // able to generate the StreamDataBlockedFrame + break NEXT_STREAM; + } + if (debug.on()) { + debug.log("stream:" + streamId + " sender is blocked: " + dataBlocked); + } + frames.add(dataBlocked); + remaining -= fdbSize; + } + stillReady = false; + continue NEXT_STREAM; + } + if (buffer == null) { + stillReady = sender.available() != 0; + continue NEXT_STREAM; + } + } + case DATA_SENT, DATA_RECVD, RESET_SENT, RESET_RECVD -> { + stillReady = false; + continue NEXT_STREAM; + } + case READY -> { + String msg = "stream:%s: illegal state %s".formatted(streamId, state); + throw new IllegalStateException(msg); + } + } + if (debug.on()) { + debug.log("packageStreamData: stream:%s, remaining:%s, " + + "maxConnectionData: %s, produced:%s", + streamId, remaining, maxConnectionData, produced); + } + } while (remaining > 0 && maxConnectionData > 0); + } catch (RuntimeException | AssertionError x) { + stillReady = false; + throw new QuicTransportException("Failed to compose frames for stream " + streamId, + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } finally { + if (stillReady) { + if (debug.on()) + debug.log("stream:%s is still ready", streamId); + enqueueForSending(streamId); + } else { + if (debug.on()) + debug.log("stream:%s is no longer ready", streamId); + } + } + assert maxConnectionData >= 0 : "produced " + produced + " max is " + maxConnectionData; + if (remaining == 0 || maxConnectionData == 0) break; + } + } catch (RuntimeException | AssertionError x) { + if (debug.on()) debug.log("Failed to compose frames", x); + if (Log.errors()) { + Log.logError(connection.logTag() + + ": Failed to compose frames", x); + } + throw new QuicTransportException("Failed to compose frames", + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } + return produced; + } + + private interface ReadyStreamCollection { + boolean isEmpty(); + + void add(QuicSenderStream sender); + + QuicStream poll(); + } + //This queue is used to ensure fair sending of stream data: the packageStreamData method + // will pop and push streams from/to this queue in a round-robin fashion so that one stream + // doesn't starve all the others. + private static class ReadyStreamQueue implements ReadyStreamCollection { + private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.add(sender); + } + + public QuicStream poll() { + return queue.poll(); + } + } + // This queue is used to ensure fast closing of streams: it always returns + // the ready stream with the lowest ID. + private static class ReadyStreamSortedQueue implements ReadyStreamCollection { + private ConcurrentSkipListMap queue = new ConcurrentSkipListMap<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.put(sender.streamId(), sender); + } + + public QuicStream poll() { + Map.Entry entry = queue.pollFirstEntry(); + if (entry == null) return null; + return entry.getValue(); + } + } + + // provides a limited view/operations over a ConcurrentHashMap(). we compute additional + // state in the remove() and put() APIs. providing only a limited set of APIs allows us + // to keep the places where we do that additional state computation, to minimal. + private final class StreamsContainer { + // A map of + private final ConcurrentMap streams = new ConcurrentHashMap<>(); + // active remote bidi stream count + private final AtomicLong remoteBidiActiveStreams = new AtomicLong(); + // active remote uni stream count + private final AtomicLong remoteUniActiveStreams = new AtomicLong(); + + private volatile long remoteBidiNextMaxStreams; + private volatile long remoteUniNextMaxStreams; + + AbstractQuicStream get(final long streamId) { + return streams.get(streamId); + } + + boolean remove(final long streamId, final AbstractQuicStream stream) { + if (!streams.remove(streamId, stream)) { + return false; + } + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.decrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.decrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return true; + } + + AbstractQuicStream put(final long streamId, final AbstractQuicStream stream) { + final AbstractQuicStream previous = streams.put(streamId, stream); + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.incrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.incrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return previous; + } + + Stream all() { + return streams.values().stream(); + } + + /** + * Returns the next (higher) max streams limit that can be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased. + */ + private long computeNextMaxStreamsLimit( + final int streamType, final long currentActiveCount, + final long currentMaxStreamsLimit) { + // we only deal with remote bidi or remote uni + assert (streamType == remoteBidi || streamType == remoteUni) + : "stream type is neither remote bidi nor remote uni: " + streamType; + final long usedRemoteStreams = peekNextStreamId(streamType) >> 2; + final boolean bidi = streamType == remoteBidi; + final var desiredStreamCount = bidi ? MAX_BIDI_STREAMS_WINDOW_SIZE + : MAX_UNI_STREAMS_WINDOW_SIZE; + final long desiredMaxStreams = usedRemoteStreams - currentActiveCount + desiredStreamCount; + // we compute a new limit after we consumed 25% (arbitrary decision) of the desired window + if (desiredMaxStreams - currentMaxStreamsLimit > desiredStreamCount >> 2) { + return desiredMaxStreams; + } + return 0; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java new file mode 100644 index 00000000000..1a20cbe5211 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the receiving part of a stream. + *

    From RFC 9000: + * + * On the receiving part of a stream, an application protocol can: + *

      + *
    • read data; and
    • + *
    • abort reading of the stream and request closure, possibly + * resulting in a STOP_SENDING frame (Section 19.5).
    • + *
    + * + */ +public non-sealed interface QuicReceiverStream extends QuicStream { + + /** + * An enum that models the state of the receiving part of a stream. + */ + enum ReceivingStreamState implements QuicStream.StreamState { + /** + * The initial state for the receiving part of a + * stream is "Recv". + *

    + * In the "Recv" state, the endpoint receives STREAM + * and STREAM_DATA_BLOCKED frames. Incoming data is buffered + * and can be reassembled into the correct order for delivery + * to the application. As data is consumed by the application + * and buffer space becomes available, the endpoint sends + * MAX_STREAM_DATA frames to allow the peer to send more data. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RECV, + /** + * When a STREAM frame with a FIN bit is received, the final size of + * the stream is known; see Section 4.5. The receiving part of the + * stream then enters the "Size Known" state. In this state, the + * endpoint no longer needs to send MAX_STREAM_DATA frames; it only + * receives any retransmissions of stream data. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SIZE_KNOWN, + /** + * Once all data for the stream has been received, the receiving part + * enters the "Data Recvd" state. This might happen as a result of + * receiving the same STREAM frame that causes the transition to + * "Size Known". After all data has been received, any STREAM or + * STREAM_DATA_BLOCKED frames for the stream can be discarded. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * The "Data Recvd" state persists until stream data has been delivered + * to the application. Once stream data has been delivered, the stream + * enters the "Data Read" state, which is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_READ, + /** + * Receiving a RESET_STREAM frame in the "Recv" or "Size Known" state + * causes the stream to enter the "Reset Recvd" state. This might + * cause the delivery of stream data to the application to be + * interrupted. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD, + /** + * Once the application receives the signal indicating that the + * stream was reset, the receiving part of the stream transitions to + * the "Reset Read" state, which is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_READ; + + @Override + public boolean isTerminal() { + return this == DATA_READ || this == RESET_READ; + } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_RECVD || this == RESET_READ; } + } + + /** + * {@return the receiving state of the stream} + */ + ReceivingStreamState receivingState(); + + /** + * Connects an {@linkplain QuicStreamReader#started() unstarted} reader + * to the receiver end of this stream. + * @param scheduler A sequential scheduler that will be invoked + * when the reader is started and new data becomes available for reading + * @return a {@code QuicStreamReader} to read data from this + * stream. + * @throws IllegalStateException if a reader is already connected. + */ + QuicStreamReader connectReader(SequentialScheduler scheduler); + + /** + * Disconnect the reader, so that a new reader can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having read + * or peeked at some bytes. + * + * @param reader the reader to be disconnected + * @throws IllegalStateException if the given reader is not currently + * connected to the stream + */ + void disconnectReader(QuicStreamReader reader); + + /** + * Cancels the reading side of this stream by sending + * a STOP_SENDING frame. + * + * @param errorCode the application error code + * + */ + void requestStopSending(long errorCode); + + /** + * {@return the amount of data that has been received so far} + * @apiNote This may include data that has not been read by the + * application yet, but does not count any data that may have + * been received twice. + */ + long dataReceived(); + + /** + * {@return the maximum amount of data that can be received on + * this stream} + * + * @apiNote This corresponds to the maximum amount of data that + * the peer has been allowed to send. + */ + long maxStreamData(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long rcvErrorCode(); + + default boolean isStopSendingRequested() { return false; } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java new file mode 100644 index 00000000000..120a9bffd5b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java @@ -0,0 +1,942 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.StreamDataFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static jdk.internal.net.http.quic.QuicConnectionImpl.DEFAULT_INITIAL_STREAM_MAX_DATA; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState.*; + +/** + * A class that implements the receiver part of a quic stream. + */ +final class QuicReceiverStreamImpl extends AbstractQuicStream implements QuicReceiverStream { + + private static final int MAX_SMALL_FRAGMENTS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxSmallFragments", 100); + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + + // The dataFlow reorders incoming stream frames and removes duplicates. + // It contains frames that cannot be delivered yet because they are not + // at the expected offset. + private final StreamDataFlow dataFlow = new StreamDataFlow(); + // The orderedQueue contains frames that can be delivered to the application now. + // They are inserted in the queue in order. + // The QuicStreamReader's scheduler loop consumes this queue. + private final ConcurrentLinkedQueue orderedQueue = new ConcurrentLinkedQueue<>(); + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize; + // Maximum stream data + private volatile long maxStreamData; + // how much data has been processed on this stream. + // This is data that was poll'ed from orderedQueue or dropped after stream reset. + private volatile long processed; + // how much data has been delivered to orderedQueue. This doesn't take into account + // frames that may be stored in the dataFlow. + private volatile long received; + // maximum of offset+length across all received frames + private volatile long maxReceivedData; + // the size of the stream, when known. Defaults to 0 when unknown. + private volatile long knownSize; + // the connected reader + private volatile QuicStreamReaderImpl reader; + // eof when the last payload has been polled by the application + private volatile boolean eof; + // the state of the receiving stream + private volatile ReceivingStreamState receivingState; + private volatile boolean requestedStopSending; + private volatile long errorCode; + + private final static long MIN_BUFFER_SIZE = 16L << 10; + QuicReceiverStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + receivingState = ReceivingStreamState.RECV; + dbgTag = connection.streamDbgTag(streamId, "R"); + long bufsize = DEFAULT_INITIAL_STREAM_MAX_DATA; + desiredBufferSize = Math.clamp(bufsize, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() == QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A locally initiated stream can't be read-only"); + } + return streamId; + } + + /** + * Sends a {@link ConnectionCloseFrame} due to MAX_STREAM_DATA exceeded + * for the stream. + * @param streamFrame the stream frame that caused the excess + * @param maxData the value of MAX_STREAM_DATA which was exceeded + */ + private static QuicTransportException streamControlOverflow(StreamFrame streamFrame, long maxData) throws QuicTransportException { + String reason = "Stream max data exceeded: offset=%s, length=%s, max stream data=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + + // debug tag for debug logger + String dbgTag() { + return dbgTag; + } + + @Override + public StreamState state() { + return receivingState(); + } + + @Override + public ReceivingStreamState receivingState() { + return receivingState; + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + var reader = this.reader; + if (reader == null) { + reader = new QuicStreamReaderImpl(scheduler); + if (Handles.READER.compareAndSet(this, null, reader)) { + if (debug.on()) debug.log("reader connected"); + return reader; + } + } + throw new IllegalStateException("reader already connected"); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + var previous = this.reader; + if (reader == previous) { + if (Handles.READER.compareAndSet(this, reader, null)) { + if (debug.on()) debug.log("reader disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + @Override + public boolean isStopSendingRequested() { + return requestedStopSending; + } + + @Override + public void requestStopSending(final long errorCode) { + if (Handles.STOP_SENDING.compareAndSet(this, false, true)) { + assert requestedStopSending : "requestedStopSending should be true!"; + if (debug.on()) debug.log("requestedStopSending: true"); + var state = receivingState; + try { + setErrorCode(errorCode); + switch(state) { + case RECV, SIZE_KNOWN -> { + connection().scheduleStopSendingFrame(streamId(), errorCode); + } + // otherwise do nothing + } + } finally { + // RFC-9000, section 3.5: "If an application is no longer interested in the data it is + // receiving on a stream, it can abort reading the stream and specify an application + // error code." + // So it implies that the application isn't anymore interested in receiving the data + // that has been buffered in the stream, so we drop all buffered data on this stream + if (state != RECV && state != DATA_READ) { + // we know the final size; we can remove the stream + increaseProcessedData(knownSize); + if (switchReceivingState(RESET_READ)) { + eof = false; + } + } + dataFlow.clear(); + orderedQueue.clear(); + if (debug.on()) { + debug.log("Dropped all buffered frames on stream %d after STOP_SENDING was requested" + + " with error code 0x%s", streamId(), Long.toHexString(errorCode)); + } + } + } + } + + @Override + public long dataReceived() { + return received; + } + + @Override + public long maxStreamData() { + return maxStreamData; + } + + @Override + public boolean isDone() { + return switch (receivingState()) { + case DATA_READ, DATA_RECVD, RESET_READ, RESET_RECVD -> + // everything received from peer + true; + default -> + // the stream is only half closed + false; + }; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param resetStreamFrame the frame received + */ + void processIncomingResetFrame(final ResetStreamFrame resetStreamFrame) + throws QuicTransportException { + try { + checkUpdateState(resetStreamFrame); + if (requestedStopSending) { + increaseProcessedData(knownSize); + switchReceivingState(RESET_READ); + } + } finally { + // make sure the state is switched to reset received. + // even if we're closing the connection + switchReceivingState(RESET_RECVD); + // wakeup reader, then throw exception. + QuicStreamReaderImpl reader = this.reader; + if (reader != null) reader.wakeup(); + } + } + + void processIncomingFrame(final StreamDataBlockedFrame streamDataBlocked) { + assert streamDataBlocked.streamId() == streamId() : "unexpected stream id"; + final long peerBlockedOn = streamDataBlocked.maxStreamData(); + final long currentLimit = this.maxStreamData; + if (peerBlockedOn > currentLimit) { + // shouldn't have happened. ignore and don't increase the limit. + return; + } + // the peer has stated that the stream is blocked due to flow control limit that we have + // imposed and has requested for increasing the limit. we approve that request + // and increase the limit only if the amount of received data that we have received and + // processed on this stream is more than 1/4 of the credit window. + if (!requestedStopSending + && currentLimit - processed < (desiredBufferSize - desiredBufferSize / 4)) { + demand(desiredBufferSize); + } else { + if (debug.on()) { + debug.log("ignoring STREAM_DATA_BLOCKED frame %s," + + " since current limit %d is large enough", streamDataBlocked, currentLimit); + } + } + } + + private void demand(final long additional) { + assert additional > 0 && additional < MAX_VL_INTEGER : "invalid demand: " + additional; + var received = dataReceived(); + var maxStreamData = maxStreamData(); + + final long newMax = Math.clamp(received + additional, maxStreamData, MAX_VL_INTEGER); + if (newMax > maxStreamData) { + connection().requestSendMaxStreamData(streamId(), newMax); + updateMaxStreamData(newMax); + } + } + + /** + * Called when the connection is closed + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + final QuicStreamReaderImpl reader = this.reader; + if (reader != null) { + reader.wakeup(); + } + } + + @Override + public long rcvErrorCode() { + return errorCode; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param streamFrame the frame received + */ + public void processIncomingFrame(final StreamFrame streamFrame) + throws QuicTransportException { + // RFC-9000, section 3.5: "STREAM frames received after sending a STOP_SENDING frame + // are still counted toward connection and stream flow control, even though these + // frames can be discarded upon receipt." + // so we do the necessary data size checks before checking if we sent a "STOP_SENDING" + // frame + checkUpdateState(streamFrame); + final ReceivingStreamState state = receivingState; + if (debug.on()) debug.log("receivingState: " + state); + long knownSize = this.knownSize; + // RESET was read or received: drop the frame. + if (state == RESET_READ || state == RESET_RECVD) { + if (debug.on()) { + debug.log("Dropping frame on stream %d since state is %s", + streamId(), state); + } + return; + } + if (requestedStopSending) { + // drop the frame + if (debug.on()) { + debug.log("Dropping frame that was received after a STOP_SENDING" + + " frame was sent on stream %d", streamId()); + } + increaseProcessedData(maxReceivedData); + if (state != RECV) { + // we know the final size; we can remove the stream + switchReceivingState(RESET_READ); + } + return; + } + + var readyFrame = dataFlow.receive(streamFrame); + var received = this.received; + boolean needWakeup = false; + while (readyFrame != null) { + // check again - this avoids a race condition where a frame + // would be considered ready if requestStopSending had been + // called concurrently, and `receive` was called after the + // state had been switched + if (requestedStopSending) { + return; + } + assert received == readyFrame.offset() + : "data received (%s) doesn't match offset (%s)" + .formatted(received, readyFrame.offset()); + this.received = received = received + readyFrame.dataLength(); + offer(readyFrame); + needWakeup = true; + readyFrame = dataFlow.poll(); + } + if (state == SIZE_KNOWN && received == knownSize) { + if (switchReceivingState(DATA_RECVD)) { + offerEof(); + needWakeup = true; + } + } + if (needWakeup) { + var reader = this.reader; + if (reader != null) reader.wakeup(); + } else { + int numFrames = dataFlow.size(); + long numBytes = dataFlow.buffered(); + if (numFrames > MAX_SMALL_FRAGMENTS && numBytes / numFrames < 400) { + // The peer sent a large number of small fragments + // that follow a gap and can't be immediately released to the reader; + // we need to buffer them, and the memory overhead is unreasonably high. + throw new QuicTransportException("Excessive stream fragmentation", + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.frameType(), + QuicTransportErrors.INTERNAL_ERROR); + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param streamFrame received stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(StreamFrame streamFrame) throws QuicTransportException { + long offset = streamFrame.offset(); + long length = streamFrame.dataLength(); + assert offset >= 0; + assert length >= 0; + + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size; + try { + size = Math.addExact(offset, length); + } catch (ArithmeticException x) { + // should not happen + if (debug.on()) { + debug.log("offset + length exceeds max value", x); + } + throw streamControlOverflow(streamFrame, Long.MAX_VALUE); + } + if (size > maxData) { + throw streamControlOverflow(streamFrame, maxData); + } + ReceivingStreamState state = receivingState; + // check finalSize if known + long knownSize = this.knownSize; + assert knownSize >= 0; + if (state != RECV && size > knownSize) { + String reason = "Stream final size exceeded: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + // check maxData + updateMaxReceivedData(size, streamFrame.getTypeField()); + if (streamFrame.isLast()) { + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: offset=%s, length=%s, max received=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(SIZE_KNOWN)) { + this.knownSize = size; + } else { + if (size != knownSize) { + String reason = "Stream final size changed: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param resetStreamFrame received reset stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(ResetStreamFrame resetStreamFrame) throws QuicTransportException { + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size = resetStreamFrame.finalSize(); + long errorCode = resetStreamFrame.errorCode(); + setErrorCode(errorCode); + if (size > maxData) { + String reason = "Stream max data exceeded: finalSize=%s, max stream data=%s" + .formatted(size, maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + ReceivingStreamState state = receivingState; + updateMaxReceivedData(size, resetStreamFrame.getTypeField()); + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: finalSize=%s, max received=%s" + .formatted(size, maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(RESET_RECVD)) { + this.knownSize = size; + } else { + if (state == SIZE_KNOWN) { + switchReceivingState(RESET_RECVD); + } + if (size != knownSize) { + String reason = "Stream final size changed: new finalSize=%s, old final size=%s" + .formatted(size, knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + private void offer(StreamFrame frame) { + var payload = frame.payload(); + if (payload.hasRemaining()) { + orderedQueue.add(payload.slice()); + } + } + + private void offerEof() { + orderedQueue.add(QuicStreamReader.EOF); + } + + /** + * Update the value of MAX_STREAM_DATA for this stream + * @param newMaxStreamData + */ + public void updateMaxStreamData(long newMaxStreamData) { + long maxStreamData = this.maxStreamData; + boolean updated = false; + while (maxStreamData < newMaxStreamData) { + if (updated = Handles.MAX_STREAM_DATA.compareAndSet(this, maxStreamData, newMaxStreamData)) break; + maxStreamData = this.maxStreamData; + } + if (updated) { + if (debug.on()) { + debug.log("updateMaxStreamData: max stream data updated from %s to %s", + maxStreamData, newMaxStreamData); + } + } + } + + /** + * Update the {@code maxReceivedData} value, and return the amount + * by which {@code maxReceivedData} was increased. This method is a + * no-op and returns 0 if {@code maxReceivedData >= newMax}. + * + * @param newMax the new max offset - typically obtained + * by adding the length of a frame to its + * offset + * @param frameType type of frame received + * @throws QuicTransportException if flow control was violated + */ + private void updateMaxReceivedData(long newMax, long frameType) throws QuicTransportException { + assert newMax >= 0; + var max = this.maxReceivedData; + while (max < newMax) { + if (Handles.MAX_RECEIVED_DATA.compareAndSet(this, max, newMax)) { + // report accepted data to connection flow control, + // and update the amount of data received in the + // connection. This will also check whether connection + // flow control is exceeded, and throw in + // this case + connection().increaseReceivedData(newMax - max, frameType); + return; + } + max = this.maxReceivedData; + } + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedDataBy(int diff) { + assert diff >= 0; + if (diff <= 0) return; + synchronized (this) { + if (requestedStopSending) { + // once we request stop sending, updates are handled by increaseProcessedData + return; + } + assert processed + diff <= received : processed+"+"+diff+">"+received+"("+maxReceivedData+")"; + processed += diff; + } + connection().increaseProcessedData(diff); + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedData(long newProcessed) { + long diff; + synchronized (this) { + if (newProcessed > processed) { + diff = newProcessed - processed; + processed = newProcessed; + } else { + diff = 0; + } + } + if (diff > 0) { + connection().increaseProcessedData(diff); + } + } + + // private implementation of a QuicStreamReader for this stream + private final class QuicStreamReaderImpl extends QuicStreamReader { + + static final int STARTED = 1; + static final int PENDING = 2; + // should not need volatile here, as long as we + // switch to using synchronize whenever state & STARTED == 0 + // Once state & STARTED != 0 the state should no longer change + private int state; + + QuicStreamReaderImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + @Override + public ReceivingStreamState receivingState() { + checkConnected(); + return QuicReceiverStreamImpl.this.receivingState(); + } + + @Override + public ByteBuffer poll() throws IOException { + checkConnected(); + var buffer = orderedQueue.poll(); + if (buffer == null) { + if (eof) return EOF; + var state = receivingState; + if (state == RESET_RECVD) { + increaseProcessedData(knownSize); + } + checkReset(); + // unfulfilled = maxStreamData - received; + // if we have received more than 1/4 of the buffer, update maxStreamData + if (!requestedStopSending && unfulfilled() < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return null; + } + + if (requestedStopSending) { + // check reset again + checkReset(); + return null; + } + increaseProcessedDataBy(buffer.capacity()); + if (buffer == EOF) { + eof = true; + assert processed == received : processed + "!=" + received; + switchReceivingState(DATA_READ); + return EOF; + } + // if the amount of received data that has been processed on this stream is + // more than 1/4 of the credit window then send a MaxStreamData frame. + if (!requestedStopSending && maxStreamData - processed < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return buffer; + } + + /** + * Checks whether the stream was reset and throws an exception if + * yes. + * + * @throws IOException if the stream is reset + */ + private void checkReset() throws IOException { + var state = receivingState; + if (state == RESET_READ || state == RESET_RECVD) { + if (state == RESET_RECVD) { + switchReceivingState(RESET_READ); + } + if (requestedStopSending) { + throw new IOException("Stream %s closed".formatted(streamId())); + } else { + throw new IOException("Stream %s reset by peer".formatted(streamId())); + } + } + checkOpened(); + } + + @Override + public ByteBuffer peek() throws IOException { + checkConnected(); + var buffer = orderedQueue.peek(); + if (buffer == null) { + checkReset(); + return eof ? EOF : null; + } + return buffer; + } + + private long unfulfilled() { + // TODO: should we synchronize to ensure consistency? + var max = maxStreamData; + var rcved = received; + return max - rcved; + } + + @Override + public QuicReceiverStream stream() { + var stream = QuicReceiverStreamImpl.this; + var reader = stream.reader; + return reader == this ? stream : null; + } + + @Override + public boolean connected() { + var reader = QuicReceiverStreamImpl.this.reader; + return reader == this; + } + + @Override + public boolean started() { + int state = this.state; + if ((state & STARTED) == STARTED) return true; + synchronized (this) { + state = this.state; + return (state & STARTED) == STARTED; + } + } + + private boolean wakeupOnStart(int state) { + assert Thread.holdsLock(this); + return (state & PENDING) != 0 + || !orderedQueue.isEmpty() + || receivingState != RECV; + } + + @Override + public void start() { + // Run the scheduler if woken up before starting + int state = this.state; + if ((state & STARTED) == 0) { + boolean wakeup = false; + synchronized (this) { + state = this.state; + if ((state & STARTED) == 0) { + wakeup = wakeupOnStart(state); + state = this.state = STARTED; + } + } + assert started(); + if (debug.on()) { + debug.log("reader started (wakeup: %s)", wakeup); + } + if (wakeup || !orderedQueue.isEmpty() || receivingState != RECV) wakeup(); + } + assert started(); + } + + private void checkConnected() { + if (!connected()) throw new IllegalStateException("reader not connected"); + } + + void wakeup() { + // Only run the scheduler after the reader is started. + int state = this.state; + boolean notstarted, pending = false; + if (notstarted = ((state & STARTED) == 0)) { + synchronized (this) { + state = this.state; + if (notstarted = ((state & STARTED) == 0)) { + state = this.state = (state | PENDING); + pending = (state & PENDING) == PENDING; + assert !started(); + } + } + } + if (notstarted) { + if (debug.on()) { + debug.log("reader not started (pending: %s)", pending); + } + return; + } + assert started(); + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + } + + /** + * Called when a state change is needed + * @param newState the new state. + */ + private boolean switchReceivingState(ReceivingStreamState newState) { + ReceivingStreamState oldState = receivingState; + if (debug.on()) { + debug.log("switchReceivingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SIZE_KNOWN -> markSizeKnown(); + case DATA_RECVD -> markDataRecvd(); + case RESET_RECVD -> markResetRecvd(); + case RESET_READ -> markResetRead(); + case DATA_READ -> markDataRead(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched receiving state from %s to %s", oldState, newState); + } else { + debug.log("receiving state not switched; state is %s", receivingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(ReceivingStreamState state) { + assert state == DATA_READ || state == RESET_READ : state; + connection().notifyTerminalState(streamId(), state); + } + + // DATA_RECV is reached when the last frame is received, + // and there's no gap + private boolean markDataRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while (!done); + return switched; + } + + // SIZE_KNOWN is reached when a stream frame with the FIN bit is received + private boolean markSizeKnown() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, SIZE_KNOWN); + case DATA_RECVD, DATA_READ, SIZE_KNOWN, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // RESET_RECV is reached when a RESET_STREAM frame is received + private boolean markResetRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // Called when the consumer has polled the last data + // DATA_READ is a terminal state + private boolean markDataRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, DATA_READ)); + }; + } while(!done); + return switched; + } + + // Called when the consumer has read the reset + // RESET_READ is a terminal state + private boolean markResetRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, RESET_READ)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + private static final class Handles { + static final VarHandle READER; + static final VarHandle RECEIVING_STATE; + static final VarHandle MAX_STREAM_DATA; + static final VarHandle MAX_RECEIVED_DATA; + static final VarHandle STOP_SENDING; + static final VarHandle ERROR_CODE; + static { + try { + var lookup = MethodHandles.lookup(); + RECEIVING_STATE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "receivingState", ReceivingStreamState.class); + READER = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "reader", QuicStreamReaderImpl.class); + MAX_STREAM_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxStreamData", long.class); + MAX_RECEIVED_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxReceivedData", long.class); + STOP_SENDING = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "requestedStopSending", boolean.class); + ERROR_CODE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "errorCode", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java new file mode 100644 index 00000000000..bdd5b55ee0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the sending part of a stream. + *

    From RFC 9000: + * + * On the sending part of a stream, an application protocol can: + *

      + *
    • write data, understanding when stream flow control credit + * (Section 4.1) has successfully been reserved to send the + * written data;
    • + *
    • end the stream (clean termination), resulting in a STREAM frame + * (Section 19.8) with the FIN bit set; and
    • + *
    • reset the stream (abrupt termination), resulting in a RESET_STREAM + * frame (Section 19.4) if the stream was not already in a terminal + * state.
    • + *
    + * + */ +public non-sealed interface QuicSenderStream extends QuicStream { + + /** + * An enum that models the state of the sending part of a stream. + */ + enum SendingStreamState implements QuicStream.StreamState { + /** + * The "Ready" state represents a newly created stream that is able + * to accept data from the application. Stream data might be + * buffered in this state in preparation for sending. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + READY, + /** + * In the "Send" state, an endpoint transmits -- and retransmits as + * necessary -- stream data in STREAM frames. The endpoint respects + * the flow control limits set by its peer and continues to accept + * and process MAX_STREAM_DATA frames. An endpoint in the "Send" state + * generates STREAM_DATA_BLOCKED frames if it is blocked from sending + * by stream flow control limits (Section 4.1). + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SEND, + /** + * After the application indicates that all stream data has been sent + * and a STREAM frame containing the FIN bit is sent, the sending part + * of the stream enters the "Data Sent" state. From this state, the + * endpoint only retransmits stream data as necessary. The endpoint + * does not need to check flow control limits or send STREAM_DATA_BLOCKED + * frames for a stream in this state. MAX_STREAM_DATA frames might be received + * until the peer receives the final stream offset. The endpoint can safely + * ignore any MAX_STREAM_DATA frames it receives from its peer for a + * stream in this state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_SENT, + /** + * From any state that is one of "Ready", "Send", or "Data Sent", an + * application can signal that it wishes to abandon transmission of + * stream data. Alternatively, an endpoint might receive a STOP_SENDING + * frame from its peer. In either case, the endpoint sends a RESET_STREAM + * frame, which causes the stream to enter the "Reset Sent" state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_SENT, + /** + * Once all stream data has been successfully acknowledged, the sending + * part of the stream enters the "Data Recvd" state, which is a + * terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * Once a packet containing a RESET_STREAM has been acknowledged, the + * sending part of the stream enters the "Reset Recvd" state, which + * is a terminal state. + *

    + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD; + + @Override + public boolean isTerminal() { + return this == DATA_RECVD || this == RESET_RECVD; + } + + /** + * {@return true if a stream in this state can be used for sending, that is, + * if this state is either {@link #READY} or {@link #SEND}}. + */ + public boolean isSending() { return this == READY || this == SEND; } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_SENT || this == RESET_RECVD; } + } + + /** + * {@return the sending state of the stream} + */ + SendingStreamState sendingState(); + + /** + * Connects a writer to the sending end of this stream. + * @param scheduler A sequential scheduler that will + * push data on the returned {@linkplain + * QuicStreamWriter#QuicStreamWriter(SequentialScheduler) + * writer}. + * @return a {@code QuicStreamWriter} to write data to this + * stream. + * @throws IllegalStateException if a writer is already connected. + */ + QuicStreamWriter connectWriter(SequentialScheduler scheduler); + + /** + * Disconnect the writer, so that a new writer can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having written + * some bytes. + * + * @param writer the writer to be disconnected + * @throws IllegalStateException if the given writer is not currently + * connected to the stream + */ + public void disconnectWriter(QuicStreamWriter writer); + + /** + * Abruptly closes the writing side of a stream by sending + * a RESET_STREAM frame. + * @param errorCode the application error code + */ + void reset(long errorCode) throws IOException; + + /** + * {@return the amount of data that has been sent} + * @apiNote + * This may include data that has not been acknowledged. + */ + long dataSent(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long sndErrorCode(); + + /** + * {@return true if STOP_SENDING was received} + */ + boolean stopSendingReceived(); + + @Override + default boolean hasError() { + return sndErrorCode() >= 0; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java new file mode 100644 index 00000000000..292face444c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java @@ -0,0 +1,662 @@ +/* + * Copyright (c) 2021, 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.Set; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; + +/** + * A class that implements the sending part of a quic stream. + */ +public final class QuicSenderStreamImpl extends AbstractQuicStream implements QuicSenderStream { + private volatile SendingStreamState sendingState; + private volatile QuicStreamWriterImpl writer; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + private final StreamWriterQueueImpl queue = new StreamWriterQueueImpl(); + private volatile long errorCode; + private volatile boolean stopSendingReceived; + + QuicSenderStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + sendingState = SendingStreamState.READY; + dbgTag = connection.streamDbgTag(streamId, "W"); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() != QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A remotely initiated stream can't be write-only"); + } + return streamId; + } + + String dbgTag() { + return dbgTag; + } + + @Override + public SendingStreamState sendingState() { + return sendingState; + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + var writer = this.writer; + if (writer == null) { + writer = new QuicStreamWriterImpl(scheduler); + if (Handles.WRITER.compareAndSet(this, null, writer)) { + if (debug.on()) debug.log("writer connected"); + return writer; + } + } + throw new IllegalStateException("writer already connected"); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + var previous = this.writer; + if (writer == previous) { + if (Handles.WRITER.compareAndSet(this, writer, null)) { + if (debug.on()) debug.log("writer disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + + @Override + public void reset(long errorCode) throws IOException { + if (debug.on()) { + debug.log("Resetting stream %s due to %s", streamId(), + connection().quicInstance().appErrorToString(errorCode)); + } + setErrorCode(errorCode); + if (switchSendingState(SendingStreamState.RESET_SENT)) { + long streamId = streamId(); + if (debug.on()) { + debug.log("Requesting to send RESET_STREAM(%d, %d)", + streamId, errorCode); + } + queue.markReset(); + if (connection().isOpen()) { + connection().requestResetStream(streamId, errorCode); + } + } + } + + @Override + public long sndErrorCode() { + return errorCode; + } + + @Override + public boolean stopSendingReceived() { + return stopSendingReceived; + } + + @Override + public long dataSent() { + // returns the amount of data that has been submitted for + // sending downstream. This will be the amount of data that + // has been consumed by the downstream consumer. + return queue.bytesConsumed(); + } + + /** + * Called to set the max stream data for this stream. + * @apiNote as per RFC 9000, any value less than the current + * max stream data is ignored + * @param newLimit the proposed new max stream data + * @return the new limit that has been finalized for max stream data. + * This new limit may or may not have been increased to the proposed {@code newLimit}. + */ + public long setMaxStreamData(final long newLimit) { + return queue.setMaxStreamData(newLimit); + } + + /** + * Called by {@link QuicConnectionStreams} after a RESET_STREAM frame + * has been sent + */ + public void resetSent() { + queue.markReset(); + queue.close(); + } + + /** + * Called when the packet containing the RESET_STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to RESET_RECVD as a result + * of this method invocation + */ + public boolean resetAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.RESET_RECVD)) { + if (debug.on()) { + debug.log("Reset received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged reset has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + return true; + } + return false; + } + + /** + * Called when the packet containing the final STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to DATA_RECVD as a result + * of this method invocation + */ + public boolean dataAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.DATA_RECVD)) { + if (debug.on()) { + debug.log("Last data received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged data has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + } + return false; + } + + /** + * Called when a STOP_SENDING frame is received from the peer + * @param errorCode the error code + */ + public void stopSendingReceived(long errorCode) { + if (queue.stopSending(errorCode)) { + stopSendingReceived = true; + setErrorCode(errorCode); + try { + if (connection().isOpen()) { + reset(errorCode); + } + } catch (IOException io) { + if (debug.on()) debug.log("Reset failed: " + io); + } finally { + QuicStreamWriterImpl writer = this.writer; + if (writer != null) writer.wakeupWriter(); + } + } + } + + /** + * Called when the connection is closed locally + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + queue.close(); + final QuicStreamWriterImpl writer = this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + /** + * A concrete implementation of the {@link StreamWriterQueue} for this + * stream. + */ + private final class StreamWriterQueueImpl extends StreamWriterQueue { + @Override + protected void wakeupProducer() { + // The scheduler is provided by the producer + // to wakeup and run the producer's write loop. + var writer = QuicSenderStreamImpl.this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void wakeupConsumer() { + // Notify the connection impl that either the data is available + // for writing or the stream is blocked and the peer needs to be + // made aware. The connection should + // eventually call QuicSenderStreamImpl::poll to + // get the data available for writing and package it + // in a StreamFrame or notice that the stream is blocked and send a + // STREAM_DATA_BLOCKED frame. + connection().streamDataAvailableForSending(Set.of(streamId())); + } + + @Override + protected void switchState(SendingStreamState state) { + // called to indicate a change in the stream state. + // at the moment the only expected value is DATA_SENT + assert state == SendingStreamState.DATA_SENT; + switchSendingState(state); + } + + @Override + protected long streamId() { + return QuicSenderStreamImpl.this.streamId(); + } + } + + + /** + * The stream internal implementation of a QuicStreamWriter. + * Most of the logic is implemented in the StreamWriterQueue, + * which is subclassed here to provide an implementation of its + * few abstract methods. + */ + private class QuicStreamWriterImpl extends QuicStreamWriter { + QuicStreamWriterImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + void wakeupWriter() { + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + + @Override + public SendingStreamState sendingState() { + checkConnected(); + return QuicSenderStreamImpl.this.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + checkConnected(); + SendingStreamState state = sending(last); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + // allow a last empty buffer to be submitted even + // if the connection is closed. That can help + // unblock the consumer side. + if (buffer != QuicStreamReader.EOF || !last) { + checkOpened(); + } + queue.submit(buffer, last); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + checkConnected(); + SendingStreamState state = sending(false); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + checkOpened(); + queue.queue(buffer); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * stream was reset + * @return a new IOException + */ + IOException streamResetException() { + long resetByPeer = queue.resetByPeer(); + if (resetByPeer < 0) { + return new IOException("stream %s reset by peer: errorCode %s" + .formatted(streamId(), - resetByPeer - 1)); + } else { + return new IOException("stream %s has been reset".formatted(streamId())); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * the final data has been sent + * @return a new IOException + */ + IOException streamClosedException() { + return new IOException("stream %s is closed - all data has been sent" + .formatted(streamId())); + } + + @Override + public long credit() { + checkConnected(); + // how much data the producer can send before + // reaching the flow control limit. Could be + // negative if the limit has been reached already. + return queue.producerCredit(); + } + + @Override + public void reset(long errorCode) throws IOException { + setErrorCode(errorCode); + checkConnected(); + QuicSenderStreamImpl.this.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + var stream = QuicSenderStreamImpl.this; + var writer = stream.writer; + return writer == this ? stream : null; + } + + @Override + public boolean connected() { + var writer = QuicSenderStreamImpl.this.writer; + return writer == this; + } + + private void checkConnected() { + if (!connected()) { + throw new IllegalStateException("writer not connected"); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + /** + * {@return the number of bytes that are available for sending, subject + * to flow control} + * @implSpec + * This method does not return more than what flow control for this + * stream would allow at the time the method is called. + * @implNote + * If the sender part is not finished initializing the default + * implementation of this method will return 0. + */ + public long available() { + return queue.readyToSend(); + } + + /** + * Whether the sending is blocked due to flow control. + * @return {@code true} if sending is blocked due to flow control + */ + public boolean isBlocked() { + return queue.consumerBlocked(); + } + + /** + * {@return the size of this stream, if known} + * @implSpec + * This method returns {@code -1} if the size of the stream is not + * known. + */ + public long streamSize() { + return queue.streamSize(); + } + + /** + * Polls at most {@code maxBytes} from the {@link StreamWriterQueue} of + * this stream. The semantics are equivalent to that of {@link + * StreamWriterQueue#poll(int)} + * @param maxBytes the maximum number of bytes to poll for sending + * @return a ByteBuffer containing at most {@code maxBytes} remaining + * bytes. + */ + public ByteBuffer poll(int maxBytes) { + return queue.poll(maxBytes); + } + + @Override + public boolean isDone() { + return switch (sendingState()) { + case DATA_RECVD, RESET_RECVD -> + // everything acknowledged + true; + default -> + // the stream is only half closed + false; + }; + } + + @Override + public StreamState state() { + return sendingState(); + } + + /** + * Called when some data is submitted (or offered) by the + * producer. If the stream is in the READY state, this will + * switch the sending state to SEND. + * @implNote + * The parameter {@code last} is ignored at this stage. + * {@link #switchSendingState(SendingStreamState) + * switchSendingState(SendingStreamState.DATA_SENT)} will be called + * later on when the last piece of data has been pushed downstream. + * + * @param last whether there will be no further data submitted + * by the producer. + * + * @return the state before switching to SEND. + */ + private SendingStreamState sending(boolean last) { + SendingStreamState state = sendingState; + if (state == SendingStreamState.READY) { + switchSendingState(SendingStreamState.SEND); + } + return state; + } + + /** + * Called when the StreamWriterQueue implementation notifies of + * a state change. + * @param newState the new state, according to the StreamWriterQueue. + */ + private boolean switchSendingState(SendingStreamState newState) { + SendingStreamState oldState = sendingState; + if (debug.on()) { + debug.log("switchSendingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SEND -> markSending(); + case DATA_SENT -> markDataSent(); + case DATA_RECVD -> markDataRecvd(); + case RESET_SENT -> markResetSent(); + case RESET_RECVD -> markResetRecvd(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched sending state from %s to %s", oldState, newState); + } else { + debug.log("sending state not switched; state is %s", sendingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(SendingStreamState state) { + assert state.isTerminal() : state; + connection().notifyTerminalState(streamId(), state); + } + + // SEND can only be set from the READY state + private boolean markSending() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.SEND); + case SEND, RESET_RECVD, RESET_SENT -> true; + // there should be no further submission of data after DATA_SENT + case DATA_SENT, DATA_RECVD -> + throw new IllegalStateException(String.valueOf(oldState)); + }; + } while(!done); + return switched; + } + + // DATA_SENT can only be set from the SEND state + private boolean markDataSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case SEND -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_SENT); + case DATA_SENT, RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + case READY -> throw new IllegalStateException(String.valueOf(oldState)); + }; + } while (!done); + return switched; + } + + // Reset can only be set in the READY, SEND, or DATA_SENT state + private boolean markResetSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY, SEND, DATA_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_SENT); + case RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + }; + } while(!done); + return switched; + } + + // Called when the packet containing the last frame is acknowledged + // DATA_RECVD is a terminal state + private boolean markDataRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + // Called when the packet containing the reset frame is acknowledged + // RESET_RECVD is a terminal state + private boolean markResetRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + // Some VarHandles to implement CAS semantics on top of plain + // volatile fields in this class. + private static class Handles { + static final VarHandle SENDING_STATE; + static final VarHandle WRITER; + static final VarHandle ERROR_CODE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + SENDING_STATE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "sendingState", SendingStreamState.class); + WRITER = lookup.findVarHandle(QuicSenderStreamImpl.class, + "writer", QuicStreamWriterImpl.class); + ERROR_CODE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "errorCode", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java new file mode 100644 index 00000000000..4d99784299f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code QuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * {@link QuicBidiStream} (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +public sealed interface QuicStream + permits QuicSenderStream, QuicReceiverStream, QuicBidiStream, AbstractQuicStream { + + /** + * An interface that unifies the three different stream states. + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + sealed interface StreamState permits + QuicReceiverStream.ReceivingStreamState, + QuicSenderStream.SendingStreamState, + QuicBidiStream.BidiStreamState { + String name(); + + /** + * {@return true if this is a terminal state} + */ + boolean isTerminal(); + } + + /** + * The stream operation mode. + * One of {@link #READ_ONLY}, {@link #WRITE_ONLY}, or {@link #READ_WRITE}. + */ + enum StreamMode { + READ_ONLY, WRITE_ONLY, READ_WRITE; + + /** + * {@return true if this operation mode allows reading data from the stream} + */ + public boolean isReadable() { + return this != WRITE_ONLY; + } + + /** + * {@return true if this operation mode allows writing data to the stream} + */ + public boolean isWritable() { + return this != READ_ONLY; + } + } + + /** + * {@return the stream ID of this stream} + */ + long streamId(); + + /** + * {@return this stream operation mode} + * One of {@link StreamMode#READ_ONLY}, {@link StreamMode#WRITE_ONLY}, + * or {@link StreamMode#READ_WRITE}. + */ + StreamMode mode(); + + /** + * {@return whether this stream is client initiated} + */ + boolean isClientInitiated(); + + /** + * {@return whether this stream is server initiated} + */ + boolean isServerInitiated(); + + /** + * {@return whether this stream is bidirectional} + */ + boolean isBidirectional(); + + /** + * {@return true if this stream is local initiated} + */ + boolean isLocalInitiated(); + + /** + * {@return true if this stream is remote initiated} + */ + boolean isRemoteInitiated(); + + /** + * The type of this stream, as an int. This is a number between + * 0 and 3 inclusive, and corresponds to the last two lowest bits + * of the stream ID. + *

      + *
    • 0x00: bidirectional, client initiated
    • + *
    • 0x01: bidirectional, server initiated
    • + *
    • 0x02: unidirectional, client initiated
    • + *
    • 0x03: unidirectional, server initiated
    • + *
    + * @return the type of this stream, as an int + */ + int type(); + + /** + * {@return the combined stream state} + * + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + StreamState state(); + + /** + * {@return true if the stream has errors} + * For a {@linkplain QuicBidiStream bidirectional} stream, + * this method returns true if either its sending part or + * its receiving part was closed with a non-zero error code. + */ + boolean hasError(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java new file mode 100644 index 00000000000..f38a12d3d7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021, 2023, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; + +/** + * An abstract class to model a reader plugged into + * a QuicStream from which data can be read + */ +public abstract class QuicStreamReader { + + /** + * A sentinel inserted into the queue after the FIN it has been received. + */ + public static final ByteBuffer EOF = ByteBuffer.wrap(new byte[0]).asReadOnlyBuffer(); + + // The scheduler to invoke when data becomes + // available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamReader. + * The given scheduler will not be invoked until the reader + * is {@linkplain #start() started}. + * + * @param scheduler A sequential scheduler that will + * poll data out of this reader. + */ + public QuicStreamReader(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the receiving state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicReceiverStream} + * to which this writer is {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + * + */ + public abstract ReceivingStreamState receivingState(); + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available}. If the end of the stream is + * reached then {@link #EOF} is returned. + * + * @implSpec + * This method behave just like {@link Queue#poll()}. + * + * @throws IOException if the stream was closed locally or + * reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer poll() throws IOException; + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available} + * + * @implSpec + * This method behave just like {@link Queue#peek()}. + * + * @throws IOException if the stream was reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer peek() throws IOException; + + /** + * {@return the stream this reader is connected to, or {@code null} + * if this reader is not currently {@linkplain #connected() connected}} + */ + public abstract QuicReceiverStream stream(); + + + /** + * {@return true if this reader is connected to its stream} + * @see QuicReceiverStream#connectReader(SequentialScheduler) + * @see QuicReceiverStream#disconnectReader(QuicStreamReader) + */ + public abstract boolean connected(); + + /** + * {@return true if this reader has been {@linkplain #start() started}} + */ + public abstract boolean started(); + + /** + * Starts the reader. The {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) scheduler} + * will not be invoked until the reader is {@linkplain #start() started}. + */ + public abstract void start(); + + /** + * {@return whether reset was received or read by this reader} + */ + public boolean isReset() { + return stream().receivingState().isReset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java new file mode 100644 index 00000000000..ef1de558e10 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2022, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * An abstract class to model a writer plugged into + * a QuicStream to which data can be written. The data + * is wrapped in {@link StreamFrame} + * before being written. + */ +public abstract class QuicStreamWriter { + + // The scheduler to invoke when flow credit + // become available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamWriter. + * @param scheduler A sequential scheduler that will + * push data into this writer. + */ + public QuicStreamWriter(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the sending state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicSenderStream} + * to which this writer is {@linkplain + * QuicSenderStream#connectWriter(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract SendingStreamState sendingState(); + + /** + * Pushes a ByteBuffer to be scheduled for writing on the stream. + * The ByteBuffer will be wrapped in a StreamFrame before being + * sent. Data that cannot be sent due to a lack of flow + * credit will be buffered. + * + * @param buffer A byte buffer to schedule for writing + * @param last Whether that's the last data that will be sent + * through this stream. + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void scheduleForWriting(ByteBuffer buffer, boolean last) + throws IOException; + + /** + * Queues a {@code ByteBuffer} on the writing queue for this stream. + * The consumer will not be woken up. More data should be submitted + * using {@link #scheduleForWriting(ByteBuffer, boolean)} in order + * to wake the consumer. + * + * @apiNote + * Use this method as a hint that more data will be + * upcoming shortly that might be aggregated with + * the data being queued in order to reduce the number + * of packets that will be sent to the peer. + * This is useful when a small number of bytes + * need to be written to the stream before actual stream + * data. Typically, this can be used for writing the + * HTTP/3 stream type for a unidirectional HTTP/3 stream + * before starting to send stream data. + * + * @param buffer A byte buffer to schedule for writing + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void queueForWriting(ByteBuffer buffer) + throws IOException; + + /** + * Indicates how many bytes the writer is + * prepared to received for sending. + * When that value grows from 0, and if the queue has + * no pending data, the {@code scheduler} + * is triggered to elicit more calls to + * {@link #scheduleForWriting(ByteBuffer,boolean)}. + * + * @apiNote This information is used to avoid + * buffering too much data while waiting for flow + * credit on the underlying stream. When flow credit + * is available, the {@code scheduler} loop is + * invoked to resume writing. The scheduler can then + * call this method to figure out how much data to + * request from upstream. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract long credit(); + + /** + * Abruptly resets the stream. + * + * @param errorCode the application error code + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void reset(long errorCode) throws IOException; + + /** + * {@return the stream this writer is connected to, or {@code null} + * if this writer isn't currently {@linkplain #connected() connected}} + */ + public abstract QuicSenderStream stream(); + + /** + * {@return true if this writer is connected to its stream} + * @see QuicSenderStream#connectWriter(SequentialScheduler) + * @see QuicSenderStream#disconnectWriter(QuicStreamWriter) + */ + public abstract boolean connected(); + + /** + * {@return true if STOP_SENDING was received} + */ + public boolean stopSendingReceived() { + return connected() ? stream().stopSendingReceived() : false; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java new file mode 100644 index 00000000000..8706cdcf887 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * A collection of utilities methods to analyze and work with + * quic streams. + */ +public final class QuicStreams { + private QuicStreams() { throw new InternalError("should not come here"); } + + public static final int TYPE_MASK = 0x03; + public static final int UNI_MASK = 0x02; + public static final int SRV_MASK = 0x01; + + public static int streamType(long streamId) { + return (int) streamId & TYPE_MASK; + } + + public static boolean isBidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == UNI_MASK; + } + + public static boolean isBidirectional(int streamType) { + return (streamType & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(int streamType) { + return (streamType & UNI_MASK) == UNI_MASK; + } + + public static boolean isClientInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == SRV_MASK; + } + + public static boolean isClientInitiated(int streamType) { + return (streamType & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(int streamType) { + return (streamType & SRV_MASK) == SRV_MASK; + } + + public static AbstractQuicStream createStream(QuicConnectionImpl connection, long streamId) { + int type = streamType(streamId); + boolean isClient = connection.isClientConnection(); + return switch (type) { + case 0x00, 0x01 -> new QuicBidiStreamImpl(connection, streamId); + case 0x02 -> isClient ? new QuicSenderStreamImpl(connection, streamId) + : new QuicReceiverStreamImpl(connection, streamId); + case 0x03 -> isClient ? new QuicReceiverStreamImpl(connection, streamId) + : new QuicSenderStreamImpl(connection, streamId); + default -> throw new IllegalArgumentException("bad stream type %s for stream %s" + .formatted(type, streamId)); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java new file mode 100644 index 00000000000..5495681b12a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2023, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; +import java.util.function.Function; + +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * Quic specifies limits on the number of uni and bidi streams that an endpoint can create. + * This {@code StreamCreationPermit} is used to keep track of that limit and is expected to be + * used before attempting to open a Quic stream. Either of {@link #tryAcquire()} or + * {@link #tryAcquire(long, TimeUnit, Executor)} must be used before attempting to open a new stream. Stream + * must only be opened if that method returns {@code true} which implies the stream creation limit + * hasn't yet reached. + *

    + * It is expected that for each of the stream types (remote uni, remote bidi, local uni + * and local bidi) a separate instance of {@code StreamCreationPermit} will be used. + *

    + * An instance of {@code StreamCreationPermit} starts with an initial limit and that limit can be + * increased to newer higher values whenever necessary. The limit however cannot be reduced to a + * lower value. + *

    + * None of the methods, including {@link #tryAcquire(long, TimeUnit, Executor)} and {@link #tryAcquire()} + * block the caller thread. + */ +final class StreamCreationPermit { + + private final InternalSemaphore semaphore; + private final SequentialScheduler permitAcquisitionScheduler = + SequentialScheduler.lockingScheduler(new TryAcquireTask()); + + private final ConcurrentLinkedQueue acquirers = new ConcurrentLinkedQueue<>(); + + /** + * @param initialMaxStreams the initial max streams limit + * @throws IllegalArgumentException if {@code initialMaxStreams} is less than 0 + * @throws NullPointerException if executor is null + */ + StreamCreationPermit(final long initialMaxStreams) { + if (initialMaxStreams < 0) { + throw new IllegalArgumentException("Invalid max streams limit: " + initialMaxStreams); + } + this.semaphore = new InternalSemaphore(initialMaxStreams); + } + + /** + * Attempts to increase the limit to {@code newLimit}. The limit will be atomically increased + * to the {@code newLimit}. If the {@linkplain #currentLimit() current limit} is higher than + * the {@code newLimit}, then the limit isn't changed and this method returns {@code false}. + * + * @param newLimit the new limit + * @return true if the limit was successfully increased to {@code newLimit}, false otherwise. + */ + boolean tryIncreaseLimitTo(final long newLimit) { + final boolean increased = this.semaphore.tryIncreaseLimitTo(newLimit); + if (increased) { + // let any waiting acquirers attempt acquiring a permit + permitAcquisitionScheduler.runOrSchedule(); + } + return increased; + } + + /** + * Attempts to acquire a permit to open a new stream. This method does not block and returns + * immediately. A stream should only be opened if the permit was successfully acquired. + * + * @return true if the permit was acquired and a new stream is allowed to be opened. + * false otherwise. + */ + boolean tryAcquire() { + return this.semaphore.tryAcquireShared(1) >= 0; + } + + /** + * Attempts to acquire a permit to open a new stream. If the permit is available then this method + * returns immediately with a {@link CompletableFuture} whose result is {@code true}. If the + * permit isn't currently available then this method returns a {@code CompletableFuture} which + * completes with a result of {@code false} if no permits were available for the duration + * represented by the {@code timeout}. If during this {@code timeout} period, a permit is + * acquired, because of an increase in the stream limit, then the returned + * {@code CompletableFuture} completes with a result of {@code true}. + * + * @param timeout the maximum amount of time to attempt acquiring a permit, after which the + * {@code CompletableFuture} will complete with a result of {@code false} + * @param unit the timeout unit + * @param executor the executor that will be used to asynchronously complete the + * returned {@code CompletableFuture} if a permit is acquired after this + * method has returned + * @return a {@code CompletableFuture} whose result will be {@code true} if the permit was + * acquired and {@code false} otherwise + * @throws IllegalArgumentException if {@code timeout} is negative + * @throws NullPointerException if the {@code executor} is null + */ + CompletableFuture tryAcquire(final long timeout, final TimeUnit unit, + final Executor executor) { + Objects.requireNonNull(executor); + if (timeout < 0) { + throw new IllegalArgumentException("invalid timeout: " + timeout); + } + if (tryAcquire()) { + return MinimalFuture.completedFuture(true); + } + final CompletableFuture future = new MinimalFuture() + .orTimeout(timeout, unit) + .handle((acquired, t) -> { + if (t instanceof TimeoutException te) { + // timed out + return MinimalFuture.completedFuture(false); + } + if (t == null) { + // completed normally + return MinimalFuture.completedFuture(acquired); + } + return MinimalFuture.failedFuture(t); + }).thenComposeAsync(Function.identity(), executor); + var waiter = new Waiter(future, executor); + this.acquirers.add(waiter); + // if the future completes in timeout the Waiter should be removed from the list. + // because this is a queue it might not be too efficient... + future.whenComplete((r,t) -> { if (r != null && !r) acquirers.remove(waiter);}); + // if stream limit might have increased in the meantime, + // trigger the task to have this newly registered waiter notified + // TODO: should we call runOrSchedule(executor) here instead? + permitAcquisitionScheduler.runOrSchedule(); + return future; + } + + /** + * {@return the current limit for stream creation} + */ + long currentLimit() { + return this.semaphore.currentLimit(); + } + + private final record Waiter(CompletableFuture acquirer, + Executor executor) { + Waiter { + assert acquirer != null : "Acquirer cannot be null"; + assert executor != null : "Executor cannot be null"; + } + } + + /** + * A task which iterates over the waiting acquirers and attempt + * to acquire a permit. If successful, the waiting acquirer(s) (i.e. the CompletableFuture(s)) + * are completed successfully. If not, the waiting acquirers continue to stay in the wait list + */ + private final class TryAcquireTask implements Runnable { + + @Override + public void run() { + Waiter waiter = null; + while ((waiter = acquirers.peek()) != null) { + final CompletableFuture acquirer = waiter.acquirer; + if (acquirer.isCancelled() || acquirer.isDone()) { + // no longer interested, or already completed, remove it + acquirers.remove(waiter); + continue; + } + if (!tryAcquire()) { + // limit reached, no permits available yet + break; + } + // compose a step which rolls back the acquired permit if the + // CompletableFuture completed in some other thread, after the permit was acquired. + acquirer.whenComplete((acquired, t) -> { + final boolean shouldRollback = acquirer.isCancelled() + || t != null + || !acquired; + if (shouldRollback) { + final boolean released = StreamCreationPermit.this.semaphore.releaseShared(1); + assert released : "acquired permit wasn't released"; + // an additional permit is now available due to the release, let any waiters + // acquire it if needed + permitAcquisitionScheduler.runOrSchedule(); + } + }); + // got a permit, complete the waiting acquirer + acquirers.remove(waiter); + acquirer.completeAsync(() -> true, waiter.executor); + } + } + } + + /** + * A {@link AbstractQueuedLongSynchronizer} whose {@linkplain #getState() state} represents + * the number of permits that have currently been acquired. This {@code Semaphore} only + * supports "shared" mode; i.e. exclusive mode isn't supported. + *

    + * The {@code Semaphore} maintains a {@linkplain #limit limit} which represents + * the maximum number of permits that can be acquired through an instance of this class. + * The {@code limit} can be {@linkplain #tryIncreaseLimitTo(long) increased} but cannot be + * reduced from the previous set limit. + */ + private static final class InternalSemaphore extends AbstractQueuedLongSynchronizer { + private static final long serialVersionUID = 4280985311770761500L; + + private final AtomicLong limit; + + /** + * @param initialLimit the initial limit, must be >=0 + */ + private InternalSemaphore(final long initialLimit) { + assert initialLimit >= 0 : "not a positive initial limit: " + initialLimit; + this.limit = new AtomicLong(initialLimit); + setState(0 /* num acquired */); + } + + /** + * Attempts to acquire additional permits. If no permits can be acquired, + * then this method returns -1. Upon successfully acquiring the + * {@code additionalAcquisitions} this method returns a value {@code >=0} which represents + * the additional number of permits that are available for acquisition. + * + * @param additionalAcquisitions the additional permits that are requested + * @return -1 If no permits can be acquired. Value >=0, representing the permits that are + * still available for acquisition. + */ + @Override + protected long tryAcquireShared(final long additionalAcquisitions) { + while (true) { + final long alreadyAcquired = getState(); + final long totalOnAcquisition = alreadyAcquired + additionalAcquisitions; + final long currentLimit = limit.get(); + if (totalOnAcquisition > currentLimit) { + return -1; // exceeds limit, so cannot acquire + } + final long numAvailableUponAcquisition = currentLimit - totalOnAcquisition; + if (compareAndSetState(alreadyAcquired, totalOnAcquisition)) { + return numAvailableUponAcquisition; + } + } + } + + /** + * Attempts to release permits + * + * @param releases the number of permits to release + * @return true if the permits were released, false otherwise + * @throws IllegalArgumentException if the number of {@code releases} exceeds the total + * number of permits that have been acquired + */ + @Override + protected boolean tryReleaseShared(final long releases) { + while (true) { + final long currentAcquisitions = getState(); + final long totalAfterRelease = currentAcquisitions - releases; + if (totalAfterRelease < 0) { + // we attempted to release more permits than what was acquired + throw new IllegalArgumentException("cannot release " + releases + + " permits from " + currentAcquisitions + " acquisitions"); + } + if (compareAndSetState(currentAcquisitions, totalAfterRelease)) { + return true; + } + } + } + + /** + * Tries to increase the limit to the {@code newLimit}. If the {@code newLimit} is lesser + * than the current limit, then this method returns false. Otherwise, this method will attempt + * to atomically increase the limit to {@code newLimit}. + * + * @param newLimit The new limit to set + * @return true if the limit was increased to {@code newLimit}. false otherwise + */ + private boolean tryIncreaseLimitTo(final long newLimit) { + long currentLimit = this.limit.get(); + while (currentLimit < newLimit) { + if (this.limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = this.limit.get(); + } + return false; + } + + /** + * {@return the current limit} + */ + private long currentLimit() { + return this.limit.get(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java new file mode 100644 index 00000000000..ebf205479f6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java @@ -0,0 +1,550 @@ +/* + * Copyright (c) 2022, 2024, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * A class to handle the writing queue of a {@link QuicSenderStream}. + * This class maintains a queue of byte buffer containing stream data + * that has not yet been packaged for sending. It also keeps track of + * the max stream data value. + * It acts as a mailbox between a producer (typically a {@link QuicStreamWriter}), + * and a consumer (typically a {@link jdk.internal.net.http.quic.QuicConnectionImpl}). + * This class is abstract: a concrete implementation of this class must only + * implement {@link #wakeupProducer()} and {@link #wakeupConsumer()} which should + * wake up the producer and consumer respectively, when data can be polled or + * submitted from the queue. + */ +abstract class StreamWriterQueue { + /** + * The amount of data that a StreamWriterQueue is willing to buffer. + * The queue will buffer excess data, but will not wake up the producer + * until the excess is consumed. + */ + private static final int BUFFER_SIZE = + Utils.getIntegerProperty("jdk.httpclient.quic.streamBufferSize", 1 << 16); + + // The current buffer containing data to send. + private ByteBuffer current; + // The offset of the data that has been consumed + private volatile long bytesConsumed; + // The offset of the data that has been supplied by the + // producer. + // bytesProduced >= bytesConsumed at all times. + private volatile long bytesProduced; + // The stream size, when known, -1 otherwise. + // The stream size may be known at the creation of the stream, + // or at the latest when the last ByteBuffer is provided by + // the producer. + private volatile long streamSize = -1; + // true if reset was requested, false otherwise + private volatile boolean resetRequested; + // The maximum offset that will be accepted by the peer at this + // time. bytesConsumed <= maxStreamData at all times. + private volatile long maxStreamData; + // negative if stop sending was received; contains -(errorCode + 1) + private volatile long stopSending; + // The queue to buffer data before it's polled by the consumer + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + protected final void lock() { + lock.lock(); + } + + protected final void unlock() { + lock.unlock(); + } + + protected abstract Logger debug(); + + /** + * This method is called by the consumer to poll data from the stream + * queue. This method will return a {@code ByteBuffer} with at most + * {@code maxbytes} remaining bytes. The {@code ByteBuffer} may contain + * less bytes if not enough bytes are available, or if there is not + * enough {@linkplain #consumerCredit() credit} to send {@code maxbytes} + * to the peer. Only stream credit is taken into account. Taking into + * account connection credit is the responsibility of the caller. + * If there is no credit, or if there is no data available, {@code null} + * is returned. When credit and data are available again, {@link #wakeupConsumer()} + * is called to wake up the consumer. + * + * @apiNote + * This method increases the consumer offset. It must not be called concurrently + * by two different threads. + * + * @implNote + * If the producer was blocked due to full buffer before this method was called + * and the method removes enough of buffered data, + * {@link #wakeupProducer()} is called. + * + * @param maxbytes the maximum number of bytes the consumer is prepared + * to consume. + * @return a {@code ByteBuffer} containing at most {@code maxbytes}, or {@code null} + * if no data is available or data is blocked by flow control. + */ + public final ByteBuffer poll(int maxbytes) { + boolean producerWasBlocked, producerUnblocked; + long produced, consumed; + ByteBuffer buffer; + long credit = consumerCredit(); + assert credit >= 0 : credit; + if (credit < maxbytes) { + maxbytes = (int)credit; + } + if (maxbytes <= 0) return null; + lock(); + try { + producerWasBlocked = producerBlocked(); + buffer = current; + if (buffer == null) { + buffer = current = queue.poll(); + } + if (buffer == null) { + return null; + } + int remaining = buffer.remaining(); + int position = buffer.position(); + consumed = bytesConsumed; + if (remaining <= maxbytes) { + current = queue.poll(); + bytesConsumed = consumed = Math.addExact(consumed, remaining); + } else { + buffer = buffer.slice(position, maxbytes); + current.position(position + maxbytes); + bytesConsumed = consumed = Math.addExact(consumed, maxbytes); + } + long size = streamSize; + produced = bytesProduced; + producerUnblocked = producerWasBlocked && !producerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + assert consumed <= produced + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || consumed <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || produced <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + } + if (size >= 0 && consumed == size) { + switchState(SendingStreamState.DATA_SENT); + } + } finally { + unlock(); + } + if (producerUnblocked) { + debug().log("producer unblocked produced:%s, consumed:%s", + produced, consumed); + wakeupProducer(); + } + return buffer; + } + + /** + * Updates the flow control credit for this queue. + * The maximum offset that will be accepted by the consumer + * can only increase. Value that are less or equal to the + * current value of the max stream data are ignored. + * + * @implSpec + * If the consumer was blocked due to flow control before + * this method was called, and the new value of the max + * stream data allows to unblock the consumer, and data + * is available, {@link #wakeupConsumer()} is called. + * + * @param data the maximum offset that will be accepted by + * the consumer + * @return the maximum offset that will be accepted by the + * consumer. + */ + public final long setMaxStreamData(long data) { + assert data >= 0 : "maxStreamData: " + data; + long max, produced, consumed; + boolean consumerWasBlocked, consumerUnblocked; + lock(); + try { + max = maxStreamData; + consumed = bytesConsumed; + produced = bytesProduced; + consumerWasBlocked = consumerBlocked(); + if (data <= max) return max; + maxStreamData = max = data; + consumerUnblocked = consumerWasBlocked && !consumerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + long size = streamSize; + assert consumed <= produced; + assert size == -1 || consumed <= size; + assert size == -1 || produced <= size; + } + } finally { + unlock(); + } + debug().log("set max stream data: %s", max); + if (consumerUnblocked && produced > 0) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + return max; + } + + /** + * Whether the producer is blocked due to flow control. + * + * @return whether the producer is blocked due to full buffers + */ + public final boolean producerBlocked() { + return producerCredit() <= 0; + } + + /** + * Whether the consumer is blocked due to flow control. + * + * @return whether the producer is blocked due to flow control + */ + public final boolean consumerBlocked() { + return consumerCredit() <= 0; + } + + /** + * {@return the offset of the data consumed by the consumer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #poll(int)} is called concurrently + * by another thread. + */ + public final long bytesConsumed() { + return bytesConsumed; + } + + /** + * {@return the offset of the data provided by the producer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} are called concurrently + * by another thread. + */ + public final long bytesProduced() { + return bytesProduced; + } + + /** + * {@return the amount of produced data which has not been consumed yet} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} or + * {@link #poll(int)} are called concurrently + * by another thread. + */ + public final long available() { + return bytesProduced - bytesConsumed; + } + + /** + * {@return the stream size if known, {@code -1} otherwise} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final long streamSize() { + return streamSize; + } + + /** + * {@return the maximum offset that the peer is prepared to accept} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #setMaxStreamData(long)} is called + * concurrently by another thread. + */ + public final long maxStreamData() { + return maxStreamData; + } + + /** + * {@return {@code true} if the consumer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #poll(int)} are called concurrently + * by another thread. + */ + public final boolean isConsumerDone() { + long size = streamSize; + long consumed = bytesConsumed; + assert size == -1 || size >= consumed; + return size >= 0 && size <= consumed; + } + + /** + * {@return {@code true} if the producer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final boolean isProducerDone() { + return streamSize >= 0; + } + + /** + * This method is called by the producer to submit data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * If sufficient credit is available, this method will wake + * up the consumer. + * + * @param buffer a buffer containing data for the stream + * @param last whether this is the last buffer that will ever be + * provided by the provided + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void submit(ByteBuffer buffer, boolean last) throws IOException { + offer(buffer, last, false); + } + + /** + * This method is called by the producer to queue data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * The consumer will not be woken, even if enough credit is + * available. More data should be submitted using + * {@link #submit(ByteBuffer, boolean)} in order to wake up the consumer. + * + * @param buffer a buffer containing data for the stream + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void queue(ByteBuffer buffer) throws IOException { + offer(buffer, false, true); + } + + /** + * Queues a buffer in the writing queue. + * + * @param buffer the buffer to queue + * @param last whether this is the last data for the stream + * @param waitForMore whether we should wait for the next submission before + * waking up the consumer + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + private void offer(ByteBuffer buffer, boolean last, boolean waitForMore) + throws IOException { + long length = buffer.remaining(); + long consumed, produced, max; + boolean wakeupConsumer; + lock(); + try { + long stopSending = this.stopSending; + if (stopSending < 0) { + throw new IOException("Stream %s reset by peer: errorCode %s" + .formatted(streamId(), 1 - stopSending)); + } + if (resetRequested) return; + if (streamSize >= 0) { + throw new IllegalStateException("Too many bytes provided"); + } + consumed = bytesConsumed; + max = maxStreamData; + produced = Math.addExact(bytesProduced, length); + bytesProduced = produced; + if (length > 0 || last) { + // allow to queue a zero-length buffer if it's the last. + queue.offer(buffer); + } + if (last) { + streamSize = produced; + } + assert consumed <= produced; + wakeupConsumer = consumed < max && consumed < produced + || consumed == produced && last; + } finally { + unlock(); + } + if (wakeupConsumer && !waitForMore) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + } + + /** + * {@return the credit of the producer} + * @implSpec + * this is the desired buffer size minus the amount of data already buffered. + */ + public final long producerCredit() { + lock(); + try { + return BUFFER_SIZE - available(); + } finally { + unlock(); + } + } + + /** + * {@return the credit of the consumer} + * @implSpec + * This is equivalent to {@link #maxStreamData()} - {@link #bytesConsumed()}. + */ + public final long consumerCredit() { + lock(); + try { + return maxStreamData - bytesConsumed; + } finally { + unlock(); + } + } + + /** + * {@return the amount of available data that can be sent + * with respect to flow control in this stream}. + * This does not take into account the global connection + * flow control. + */ + public final long readyToSend() { + long consumed, produced, max; + lock(); + try { + consumed = bytesConsumed; + max = maxStreamData; + produced = bytesProduced; + } finally { + unlock(); + } + assert max >= consumed; + assert produced >= consumed; + return Math.min(max - consumed, produced - consumed); + } + + public final void markReset() { + lock(); + try { + resetRequested = true; + } finally { + unlock(); + } + } + + final void close() { + lock(); + try { + bytesProduced = bytesConsumed; + queue.clear(); + current = null; + } finally { + unlock(); + } + } + + /** + * Called when a stop sending frame is received for this stream + * @param errorCode the error code + */ + protected final boolean stopSending(long errorCode) { + long stopSending; + lock(); + try { + if (resetRequested) return false; + if (streamSize >= 0 && bytesConsumed == streamSize) return false; + if ((stopSending = this.stopSending) < 0) return false; + this.stopSending = stopSending = - (errorCode + 1); + } finally { + unlock(); + } + assert stopSending < 0 && stopSending == - (errorCode + 1); + return true; + } + + /** + * {@return -1 minus the error code that was supplied by the peer + * when requesting for stop sending} + * @apiNote a strictly negative value indicates that the stream was + * reset by the peer. The error code supplied by the peer + * can be obtained with the formula:

    {@code
    +     *    long errorCode = - (resetByPeer() + 1);
    +     *    }
    + */ + final long resetByPeer() { + return stopSending; + } + + /** + * This method is called to wake up the consumer when there is + * credit and data available for the consumer. + */ + protected abstract void wakeupConsumer(); + + /** + * This method is called to wake up the producer when there is + * credit available for the producer. + */ + protected abstract void wakeupProducer(); + + /** + * Called to switch the sending state when data has been sent. + * @param dataSent the new state - typically {@link SendingStreamState#DATA_SENT} + */ + protected abstract void switchState(SendingStreamState dataSent); + + /** + * {@return the stream id this queue was created for} + */ + protected abstract long streamId(); + +} diff --git a/src/java.net.http/share/classes/module-info.java b/src/java.net.http/share/classes/module-info.java index 14cbb85291d..48f23953ad0 100644 --- a/src/java.net.http/share/classes/module-info.java +++ b/src/java.net.http/share/classes/module-info.java @@ -48,7 +48,9 @@ * depending on the context. These restrictions cannot be overridden by this property. * *
  • {@systemProperty jdk.httpclient.bufsize} (default: 16384 bytes or 16 kB)
    - * The size to use for internal allocated buffers in bytes. + * The capacity of internal ephemeral buffers allocated to pass data to and from the + * client, in bytes. Valid values are in the range [1, 2^14 (16384)]. + * If an invalid value is provided, the default value is used. *

  • *
  • {@systemProperty jdk.httpclient.connectionPoolSize} (default: 0)
    * The maximum number of connections to keep in the HTTP/1.1 keep alive cache. A value of 0 @@ -75,6 +77,18 @@ *

  • {@systemProperty jdk.httpclient.hpack.maxheadertablesize} (default: 16384 or * 16 kB)
    The HTTP/2 client maximum HPACK header table size in bytes. *

  • + *
  • {@systemProperty jdk.httpclient.qpack.decoderMaxTableCapacity} (default: 0) + *
    The HTTP/3 client maximum QPACK decoder dynamic header table size in bytes. + *
    Setting this value to a positive number will allow HTTP/3 servers to add entries + * to the QPack decoder's dynamic table. When set to 0, servers are not permitted to add + * entries to the client's QPack encoder's dynamic table. + *

  • + *
  • {@systemProperty jdk.httpclient.qpack.encoderTableCapacityLimit} (default: 4096, + * or 4 kB) + *
    The HTTP/3 client maximum QPACK encoder dynamic header table size in bytes. + *
    Setting this value to a positive number allows the HTTP/3 client's QPack encoder to + * add entries to the server's QPack decoder's dynamic table, if the server permits it. + *

  • *
  • {@systemProperty jdk.httpclient.HttpClient.log} (default: none)
    * Enables high-level logging of various events through the {@linkplain java.lang.System.Logger * Platform Logging API}. The value contains a comma-separated list of any of the @@ -88,6 +102,8 @@ *

  • ssl
  • *
  • trace
  • *
  • channel
  • + *
  • http3
  • + *
  • quic
  • *
    * You can append the frames item with a colon-separated list of any of the following items: *
      @@ -96,31 +112,57 @@ *
    • window
    • *
    • all
    • *

    + * You can append the quic item with a colon-separated list of any of the following items; + * packets are logged in an abridged form that only shows frames offset and length, + * but not content: + *
      + *
    • ack: packets containing ack frames will be logged
    • + *
    • cc: information on congestion control will be logged
    • + *
    • control: packets containing quic controls (such as frames affecting + * flow control, or frames opening or closing streams) + * will be logged
    • + *
    • crypto: packets containing crypto frames will be logged
    • + *
    • data: packets containing stream frames will be logged
    • + *
    • dbb: information on direct byte buffer usage will be logged
    • + *
    • ping: packets containing ping frames will be logged
    • + *
    • processed: information on flow control (processed bytes) will be logged
    • + *
    • retransmit: information on packet loss and recovery will be logged
    • + *
    • timer: information on send task scheduling will be logged
    • + *
    • all
    • + *

    * Specifying an item adds it to the HTTP client's log. For example, if you specify the * following value, then the Platform Logging API logs all possible HTTP Client events:
    * "errors,requests,headers,frames:control:data:window,ssl,trace,channel"
    * Note that you can replace control:data:window with all. The name of the logger is * "jdk.httpclient.HttpClient", and all logging is at level INFO. + * To debug issues with the quic protocol a good starting point is to specify + * {@code quic:control:retransmit}. * *
  • {@systemProperty jdk.httpclient.keepalive.timeout} (default: 30)
    - * The number of seconds to keep idle HTTP connections alive in the keep alive cache. This - * property applies to both HTTP/1.1 and HTTP/2. The value for HTTP/2 can be overridden - * with the {@code jdk.httpclient.keepalive.timeout.h2 property}. + * The number of seconds to keep idle HTTP connections alive in the keep alive cache. + * By default this property applies to HTTP/1.1, HTTP/2 and HTTP/3. + * The value for HTTP/2 and HTTP/3 can be overridden with the + * {@code jdk.httpclient.keepalive.timeout.h2} and {@code jdk.httpclient.keepalive.timeout.h3} + * properties respectively. The value specified for HTTP/2 acts as default value for HTTP/3. *

  • *
  • {@systemProperty jdk.httpclient.keepalive.timeout.h2} (default: see * below)
    The number of seconds to keep idle HTTP/2 connections alive. If not set, then the * {@code jdk.httpclient.keepalive.timeout} setting is used. *

  • + *
  • {@systemProperty jdk.httpclient.keepalive.timeout.h3} (default: see + * below)
    The number of seconds to keep idle HTTP/3 connections alive. If not set, then the + * {@code jdk.httpclient.keepalive.timeout.h2} setting is used. + *

  • *
  • {@systemProperty jdk.httpclient.maxframesize} (default: 16384 or 16kB)
    * The HTTP/2 client maximum frame size in bytes. The server is not permitted to send a frame * larger than this. *

  • *
  • {@systemProperty jdk.httpclient.maxLiteralWithIndexing} (default: 512)
    * The maximum number of header field lines (header name and value pairs) that a - * client is willing to add to the HPack Decoder dynamic table during the decoding + * client is willing to add to the HPack or QPACK Decoder dynamic table during the decoding * of an entire header field section. * This is purely an implementation limit. - * If a peer sends a field section with encoding that + * If a peer sends a field section or a set of QPACK instructions with encoding that * exceeds this limit a {@link java.net.ProtocolException ProtocolException} will be raised. * A value of zero or a negative value means no limit. *

  • @@ -135,7 +177,7 @@ * A value of zero or a negative value means no limit. * *
  • {@systemProperty jdk.httpclient.maxstreams} (default: 100)
    - * The maximum number of HTTP/2 push streams that the client will permit servers to open + * The maximum number of HTTP/2 or HTTP/3 push streams that the client will permit servers to open * simultaneously. *

  • *
  • {@systemProperty jdk.httpclient.receiveBufferSize} (default: operating system @@ -187,6 +229,61 @@ * value means no limit. *

  • * + *

    + * The following system properties can be used to configure some aspects of the + * QUIC Protocol + * implementation used for HTTP/3: + *

      + *
    • {@systemProperty jdk.httpclient.quic.receiveBufferSize} (default: operating system + * default)
      The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_RCVBUF receive buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.sendBufferSize} (default: operating system + * default)
      The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_SNDBUF send buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.defaultMTU} (default: 1200 bytes)
      + * The default Maximum Transmission Unit (MTU) size that will be used on quic connections. + * The default implementation of the HTTP/3 client does not implement Path MTU Detection, + * but will attempt to send 1-RTT packets up to the size defined by this property. + * Specifying a higher value may give better upload performance when the client and + * servers are located on the same machine, but is likely to result in irrecoverable + * packet loss if used over the network. Allowed values are in the range [1200, 65527]. + * If an out-of-range value is specified, the minimum default value will be used. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxBytesInFlight} (default: + * 16777216 bytes or 16MB)
      + * This is the maximum number of unacknowledged bytes that the quic congestion + * controller allows to be in flight. When this amount is reached, no new + * data is sent until some of the packets in flight are acknowledged. + *
      + * Allowed values are in the range [2^14, 2^24] (or [16kB, 16MB]). + * If an out-of-range value is specified, it will be clamped to the closest + * value in range. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxInitialData} (default: 15728640 + * bytes, or 15MB)
      + * The initial flow control limit for quic connections in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxStreamInitialData} (default: 6291456 + * bytes, or 6MB)
      + * The initial flow control limit for quic streams in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

    • + *
    • {@systemProperty jdk.httpclient.quic.maxInitialTimeout} (default: 30 + * seconds)
      + * This is the maximum time, in seconds, during which the client will wait for a + * response from the server, and continue retransmitting the first Quic INITIAL packet, + * before raising a {@link java.net.ConnectException}. The first INITIAL packet received + * from the target server will disarm this timeout. + *

    • + * + *
    * @moduleGraph * @since 11 */ diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java b/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java deleted file mode 100644 index 96fd6d010ac..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/Main.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Copyright (c) 2005, 2025, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ - -package com.sun.tools.script.shell; - -import java.io.*; -import java.net.*; -import java.nio.charset.Charset; -import java.text.*; -import java.util.*; -import javax.script.*; - -/** - * This is the main class for Java script shell. - */ -public class Main { - /** - * main entry point to the command line tool - * @param args command line argument array - */ - public static void main(String[] args) { - // print deprecation warning - getError().println(getMessage("deprecated.warning", - new Object[] { PROGRAM_NAME })); - - // parse command line options - String[] scriptArgs = processOptions(args); - - // process each script command - for (Command cmd : scripts) { - cmd.run(scriptArgs); - } - - System.exit(EXIT_SUCCESS); - } - - // Each -e or -f or interactive mode is represented - // by an instance of Command. - private static interface Command { - public void run(String[] arguments); - } - - /** - * Parses and processes command line options. - * @param args command line argument array - */ - private static String[] processOptions(String[] args) { - // current scripting language selected - String currentLanguage = DEFAULT_LANGUAGE; - // current script file encoding selected - String currentEncoding = null; - - // check for -classpath or -cp first - checkClassPath(args); - - // have we seen -e or -f ? - boolean seenScript = false; - // have we seen -f - already? - boolean seenStdin = false; - for (int i=0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-classpath") || - arg.equals("-cp")) { - // handled already, just continue - i++; - continue; - } - - // collect non-option arguments and pass these as script arguments - if (!arg.startsWith("-")) { - int numScriptArgs; - int startScriptArg; - if (seenScript) { - // if we have seen -e or -f already all non-option arguments - // are passed as script arguments - numScriptArgs = args.length - i; - startScriptArg = i; - } else { - // if we have not seen -e or -f, first non-option argument - // is treated as script file name and rest of the non-option - // arguments are passed to script as script arguments - numScriptArgs = args.length - i - 1; - startScriptArg = i + 1; - ScriptEngine se = getScriptEngine(currentLanguage); - addFileSource(se, args[i], currentEncoding); - } - // collect script arguments and return to main - String[] result = new String[numScriptArgs]; - System.arraycopy(args, startScriptArg, result, 0, numScriptArgs); - return result; - } - - if (arg.startsWith("-D")) { - String value = arg.substring(2); - int eq = value.indexOf('='); - if (eq != -1) { - System.setProperty(value.substring(0, eq), - value.substring(eq + 1)); - } else { - if (!value.isEmpty()) { - System.setProperty(value, ""); - } else { - // do not allow empty property name - usage(EXIT_CMD_NO_PROPNAME); - } - } - continue; - } else if (arg.equals("-?") || - arg.equals("-h") || - arg.equals("--help") || - // -help: legacy. - arg.equals("-help")) { - usage(EXIT_SUCCESS); - } else if (arg.equals("-e")) { - seenScript = true; - if (++i == args.length) - usage(EXIT_CMD_NO_SCRIPT); - - ScriptEngine se = getScriptEngine(currentLanguage); - addStringSource(se, args[i]); - continue; - } else if (arg.equals("-encoding")) { - if (++i == args.length) - usage(EXIT_CMD_NO_ENCODING); - currentEncoding = args[i]; - continue; - } else if (arg.equals("-f")) { - seenScript = true; - if (++i == args.length) - usage(EXIT_CMD_NO_FILE); - ScriptEngine se = getScriptEngine(currentLanguage); - if (args[i].equals("-")) { - if (seenStdin) { - usage(EXIT_MULTIPLE_STDIN); - } else { - seenStdin = true; - } - addInteractiveMode(se); - } else { - addFileSource(se, args[i], currentEncoding); - } - continue; - } else if (arg.equals("-l")) { - if (++i == args.length) - usage(EXIT_CMD_NO_LANG); - currentLanguage = args[i]; - continue; - } else if (arg.equals("-q")) { - listScriptEngines(); - } - // some unknown option... - usage(EXIT_UNKNOWN_OPTION); - } - - if (! seenScript) { - ScriptEngine se = getScriptEngine(currentLanguage); - addInteractiveMode(se); - } - return new String[0]; - } - - /** - * Adds interactive mode Command - * @param se ScriptEngine to use in interactive mode. - */ - private static void addInteractiveMode(final ScriptEngine se) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - processSource(se, "-", null); - } - }); - } - - /** - * Adds script source file Command - * @param se ScriptEngine used to evaluate the script file - * @param fileName script file name - * @param encoding script file encoding - */ - private static void addFileSource(final ScriptEngine se, - final String fileName, - final String encoding) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - processSource(se, fileName, encoding); - } - }); - } - - /** - * Adds script string source Command - * @param se ScriptEngine to be used to evaluate the script string - * @param source Script source string - */ - private static void addStringSource(final ScriptEngine se, - final String source) { - scripts.add(new Command() { - public void run(String[] args) { - setScriptArguments(se, args); - String oldFile = setScriptFilename(se, ""); - try { - evaluateString(se, source); - } finally { - setScriptFilename(se, oldFile); - } - } - }); - } - - /** - * Prints list of script engines available and exits. - */ - private static void listScriptEngines() { - List factories = engineManager.getEngineFactories(); - for (ScriptEngineFactory factory: factories) { - getError().println(getMessage("engine.info", - new Object[] { factory.getLanguageName(), - factory.getLanguageVersion(), - factory.getEngineName(), - factory.getEngineVersion() - })); - } - System.exit(EXIT_SUCCESS); - } - - /** - * Processes a given source file or standard input. - * @param se ScriptEngine to be used to evaluate - * @param filename file name, can be null - * @param encoding script file encoding, can be null - */ - private static void processSource(ScriptEngine se, String filename, - String encoding) { - if (filename.equals("-")) { - Charset charset = Charset.forName(System.getProperty("stdin.encoding"), Charset.defaultCharset()); - BufferedReader in = new BufferedReader(new InputStreamReader(System.in, charset)); - boolean hitEOF = false; - String prompt = getPrompt(se); - se.put(ScriptEngine.FILENAME, ""); - while (!hitEOF) { - getError().print(prompt); - String source = ""; - try { - source = in.readLine(); - } catch (IOException ioe) { - getError().println(ioe.toString()); - } - if (source == null) { - hitEOF = true; - break; - } - Object res = evaluateString(se, source, false); - if (res != null) { - res = res.toString(); - if (res == null) { - res = "null"; - } - getError().println(res); - } - } - } else { - FileInputStream fis = null; - try { - fis = new FileInputStream(filename); - } catch (FileNotFoundException fnfe) { - getError().println(getMessage("file.not.found", - new Object[] { filename })); - System.exit(EXIT_FILE_NOT_FOUND); - } - evaluateStream(se, fis, filename, encoding); - } - } - - /** - * Evaluates given script source - * @param se ScriptEngine to evaluate the string - * @param script Script source string - * @param exitOnError whether to exit the process on script error - */ - private static Object evaluateString(ScriptEngine se, - String script, boolean exitOnError) { - try { - return se.eval(script); - } catch (ScriptException sexp) { - getError().println(getMessage("string.script.error", - new Object[] { sexp.getMessage() })); - if (exitOnError) - System.exit(EXIT_SCRIPT_ERROR); - } catch (Exception exp) { - exp.printStackTrace(getError()); - if (exitOnError) - System.exit(EXIT_SCRIPT_ERROR); - } - - return null; - } - - /** - * Evaluate script string source and exit on script error - * @param se ScriptEngine to evaluate the string - * @param script Script source string - */ - private static void evaluateString(ScriptEngine se, String script) { - evaluateString(se, script, true); - } - - /** - * Evaluates script from given reader - * @param se ScriptEngine to evaluate the string - * @param reader Reader from which is script is read - * @param name file name to report in error. - */ - private static Object evaluateReader(ScriptEngine se, - Reader reader, String name) { - String oldFilename = setScriptFilename(se, name); - try { - return se.eval(reader); - } catch (ScriptException sexp) { - getError().println(getMessage("file.script.error", - new Object[] { name, sexp.getMessage() })); - System.exit(EXIT_SCRIPT_ERROR); - } catch (Exception exp) { - exp.printStackTrace(getError()); - System.exit(EXIT_SCRIPT_ERROR); - } finally { - setScriptFilename(se, oldFilename); - } - return null; - } - - /** - * Evaluates given input stream - * @param se ScriptEngine to evaluate the string - * @param is InputStream from which script is read - * @param name file name to report in error - */ - private static Object evaluateStream(ScriptEngine se, - InputStream is, String name, - String encoding) { - BufferedReader reader = null; - if (encoding != null) { - try { - reader = new BufferedReader(new InputStreamReader(is, - encoding)); - } catch (UnsupportedEncodingException uee) { - getError().println(getMessage("encoding.unsupported", - new Object[] { encoding })); - System.exit(EXIT_NO_ENCODING_FOUND); - } - } else { - reader = new BufferedReader(new InputStreamReader(is)); - } - return evaluateReader(se, reader, name); - } - - /** - * Prints usage message and exits - * @param exitCode process exit code - */ - private static void usage(int exitCode) { - getError().println(getMessage("main.usage", - new Object[] { PROGRAM_NAME })); - System.exit(exitCode); - } - - /** - * Gets prompt for interactive mode - * @return prompt string to use - */ - private static String getPrompt(ScriptEngine se) { - List names = se.getFactory().getNames(); - return names.get(0) + "> "; - } - - /** - * Get formatted, localized error message - */ - private static String getMessage(String key, Object[] params) { - return MessageFormat.format(msgRes.getString(key), params); - } - - // stream to print error messages - private static PrintStream getError() { - return System.err; - } - - // get current script engine - private static ScriptEngine getScriptEngine(String lang) { - ScriptEngine se = engines.get(lang); - if (se == null) { - se = engineManager.getEngineByName(lang); - if (se == null) { - getError().println(getMessage("engine.not.found", - new Object[] { lang })); - System.exit(EXIT_ENGINE_NOT_FOUND); - } - - // initialize the engine - initScriptEngine(se); - // to avoid re-initialization of engine, store it in a map - engines.put(lang, se); - } - return se; - } - - // initialize a given script engine - private static void initScriptEngine(ScriptEngine se) { - // put engine global variable - se.put("engine", se); - - // load init. file from resource - List exts = se.getFactory().getExtensions(); - InputStream sysIn = null; - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - for (String ext : exts) { - try { - sysIn = Main.class.getModule().getResourceAsStream("com/sun/tools/script/shell/init." + ext); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - if (sysIn != null) break; - } - if (sysIn != null) { - evaluateStream(se, sysIn, "", null); - } - } - - /** - * Checks for -classpath, -cp in command line args. Creates a ClassLoader - * and sets it as Thread context loader for current thread. - * - * @param args command line argument array - */ - private static void checkClassPath(String[] args) { - String classPath = null; - for (int i = 0; i < args.length; i++) { - if (args[i].equals("-classpath") || - args[i].equals("-cp")) { - if (++i == args.length) { - // just -classpath or -cp with no value - usage(EXIT_CMD_NO_CLASSPATH); - } else { - classPath = args[i]; - } - } - } - - if (classPath != null) { - /* We create a class loader, configure it with specified - * classpath values and set the same as context loader. - * Note that ScriptEngineManager uses context loader to - * load script engines. So, this ensures that user defined - * script engines will be loaded. For classes referred - * from scripts, Rhino engine uses thread context loader - * but this is script engine dependent. We don't have - * script engine independent solution anyway. Unless we - * know the class loader used by a specific engine, we - * can't configure correct loader. - */ - URL[] urls = pathToURLs(classPath); - URLClassLoader loader = new URLClassLoader(urls); - Thread.currentThread().setContextClassLoader(loader); - } - - // now initialize script engine manager. Note that this has to - // be done after setting the context loader so that manager - // will see script engines from user specified classpath - engineManager = new ScriptEngineManager(); - } - - /** - * Utility method for converting a search path string to an array - * of directory and JAR file URLs. - * - * @param path the search path string - * @return the resulting array of directory and JAR file URLs - */ - private static URL[] pathToURLs(String path) { - String[] components = path.split(File.pathSeparator); - URL[] urls = new URL[components.length]; - int count = 0; - while(count < components.length) { - URL url = fileToURL(new File(components[count])); - if (url != null) { - urls[count++] = url; - } - } - if (urls.length != count) { - URL[] tmp = new URL[count]; - System.arraycopy(urls, 0, tmp, 0, count); - urls = tmp; - } - return urls; - } - - /** - * Returns the directory or JAR file URL corresponding to the specified - * local file name. - * - * @param file the File object - * @return the resulting directory or JAR file URL, or null if unknown - */ - private static URL fileToURL(File file) { - String name; - try { - name = file.getCanonicalPath(); - } catch (IOException e) { - name = file.getAbsolutePath(); - } - name = name.replace(File.separatorChar, '/'); - if (!name.startsWith("/")) { - name = "/" + name; - } - // If the file does not exist, then assume that it's a directory - if (!file.isFile()) { - name = name + "/"; - } - try { - @SuppressWarnings("deprecation") - var result = new URL("file", "", name); - return result; - } catch (MalformedURLException e) { - throw new IllegalArgumentException("file"); - } - } - - private static void setScriptArguments(ScriptEngine se, String[] args) { - se.put("arguments", args); - se.put(ScriptEngine.ARGV, args); - } - - private static String setScriptFilename(ScriptEngine se, String name) { - String oldName = (String) se.get(ScriptEngine.FILENAME); - se.put(ScriptEngine.FILENAME, name); - return oldName; - } - - // exit codes - private static final int EXIT_SUCCESS = 0; - private static final int EXIT_CMD_NO_CLASSPATH = 1; - private static final int EXIT_CMD_NO_FILE = 2; - private static final int EXIT_CMD_NO_SCRIPT = 3; - private static final int EXIT_CMD_NO_LANG = 4; - private static final int EXIT_CMD_NO_ENCODING = 5; - private static final int EXIT_CMD_NO_PROPNAME = 6; - private static final int EXIT_UNKNOWN_OPTION = 7; - private static final int EXIT_ENGINE_NOT_FOUND = 8; - private static final int EXIT_NO_ENCODING_FOUND = 9; - private static final int EXIT_SCRIPT_ERROR = 10; - private static final int EXIT_FILE_NOT_FOUND = 11; - private static final int EXIT_MULTIPLE_STDIN = 12; - - // default scripting language - private static final String DEFAULT_LANGUAGE = "js"; - // list of scripts to process - private static List scripts; - // the script engine manager - private static ScriptEngineManager engineManager; - // map of engines we loaded - private static Map engines; - // error messages resource - private static ResourceBundle msgRes; - private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages"; - private static String PROGRAM_NAME = "jrunscript"; - - static { - scripts = new ArrayList(); - engines = new HashMap(); - msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault()); - } -} diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js b/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js deleted file mode 100644 index ced3ba06367..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/init.js +++ /dev/null @@ -1,927 +0,0 @@ -/* - * Copyright (c) 2005, 2013, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ - -/** - * jrunscript JavaScript built-in functions and objects. - */ - -/** - * Creates an object that delegates all method calls on - * it to the 'invoke' method on the given delegate object.
    - * - * Example: - *
    - * 
    - *     var x  = { invoke: function(name, args) { //code...}
    - *     var y = new JSInvoker(x);
    - *     y.func(3, 3); // calls x.invoke('func', args); where args is array of arguments
    - * 
    - * 
    - * @param obj object to be wrapped by JSInvoker - * @constructor - */ -function JSInvoker(obj) { - return new JSAdapter({ - __get__ : function(name) { - return function() { - return obj.invoke(name, arguments); - } - } - }); -} - -/** - * This variable represents OS environment. Environment - * variables can be accessed as fields of this object. For - * example, env.PATH will return PATH value configured. - */ -var env = new JSAdapter({ - __get__ : function (name) { - return java.lang.System.getenv(name); - }, - __has__ : function (name) { - return java.lang.System.getenv().containsKey(name); - }, - __getIds__ : function() { - return java.lang.System.getenv().keySet().toArray(); - }, - __delete__ : function(name) { - println("can't delete env item"); - }, - __put__ : function (name, value) { - println("can't change env item"); - }, - toString: function() { - return java.lang.System.getenv().toString(); - } -}); - -/** - * Creates a convenient script object to deal with java.util.Map instances. - * The result script object's field names are keys of the Map. For example, - * scriptObj.keyName can be used to access value associated with given key.
    - * Example: - *
    - * 
    - *     var x = java.lang.SystemProperties();
    - *     var y = jmap(x);
    - *     println(y['java.class.path']); // prints java.class.path System property
    - *     delete y['java.class.path']; // remove java.class.path System property
    - * 
    - * 
    - * - * @param map java.util.Map instance that will be wrapped - * @constructor - */ -function jmap(map) { - return new JSAdapter({ - __get__ : function(name) { - if (map.containsKey(name)) { - return map.get(name); - } else { - return undefined; - } - }, - __has__ : function(name) { - return map.containsKey(name); - }, - - __delete__ : function (name) { - return map.remove(name); - }, - __put__ : function(name, value) { - map.put(name, value); - }, - __getIds__ : function() { - return map.keySet().toArray(); - }, - toString: function() { - return map.toString(); - } - }); -} - -/** - * Creates a convenient script object to deal with java.util.List instances. - * The result script object behaves like an array. For example, - * scriptObj[index] syntax can be used to access values in the List instance. - * 'length' field gives size of the List.
    - * - * Example: - *
    - * 
    - *    var x = new java.util.ArrayList(4);
    - *    x.add('Java');
    - *    x.add('JavaScript');
    - *    x.add('SQL');
    - *    x.add('XML');
    - *
    - *    var y = jlist(x);
    - *    println(y[2]); // prints third element of list
    - *    println(y.length); // prints size of the list
    - *
    - * @param map java.util.List instance that will be wrapped
    - * @constructor
    - */
    -function jlist(list) {
    -    function isValid(index) {
    -        return typeof(index) == 'number' &&
    -            index > -1 && index < list.size();
    -    }
    -    return new JSAdapter({
    -        __get__ :  function(name) {
    -            if (isValid(name)) {
    -                return list.get(name);
    -            } else if (name == 'length') {
    -                return list.size();
    -            } else {
    -                return undefined;
    -            }
    -        },
    -        __has__ : function (name) {
    -            return isValid(name) || name == 'length';
    -        },
    -        __delete__ : function(name) {
    -            if (isValid(name)) {
    -                list.remove(name);
    -            }
    -        },
    -        __put__ : function(name, value) {
    -            if (isValid(name)) {
    -                list.set(name, value);
    -            }
    -        },
    -        __getIds__: function() {
    -            var res = new Array(list.size());
    -            for (var i = 0; i < res.length; i++) {
    -                res[i] = i;
    -            }
    -            return res;
    -        },
    -        toString: function() {
    -            return list.toString();
    -        }
    -    });
    -}
    -
    -/**
    - * This is java.lang.System properties wrapped by JSAdapter.
    - * For eg. to access java.class.path property, you can use
    - * the syntax sysProps["java.class.path"]
    - */
    -var sysProps = new JSAdapter({
    -    __get__ : function (name) {
    -        return java.lang.System.getProperty(name);
    -    },
    -    __has__ : function (name) {
    -        return java.lang.System.getProperty(name) != null;
    -    },
    -    __getIds__ : function() {
    -        return java.lang.System.getProperties().keySet().toArray();
    -    },
    -    __delete__ : function(name) {
    -        java.lang.System.clearProperty(name);
    -        return true;
    -    },
    -    __put__ : function (name, value) {
    -        java.lang.System.setProperty(name, value);
    -    },
    -    toString: function() {
    -        return "";
    -    }
    -});
    -
    -// stdout, stderr & stdin
    -var out = java.lang.System.out;
    -var err = java.lang.System.err;
    -// can't use 'in' because it is a JavaScript keyword :-(
    -var inp = java.lang.System["in"];
    -
    -var BufferedInputStream = java.io.BufferedInputStream;
    -var BufferedOutputStream = java.io.BufferedOutputStream;
    -var BufferedReader = java.io.BufferedReader;
    -var DataInputStream = java.io.DataInputStream;
    -var File = java.io.File;
    -var FileInputStream = java.io.FileInputStream;
    -var FileOutputStream = java.io.FileOutputStream;
    -var InputStream = java.io.InputStream;
    -var InputStreamReader = java.io.InputStreamReader;
    -var OutputStream = java.io.OutputStream;
    -var Reader = java.io.Reader;
    -var URL = java.net.URL;
    -
    -/**
    - * Generic any object to input stream mapper
    - * @param str input file name, URL or InputStream
    - * @return InputStream object
    - * @private
    - */
    -function inStream(str) {
    -    if (typeof(str) == "string") {
    -        // '-' means standard input
    -        if (str == '-') {
    -            return java.lang.System["in"];
    -        }
    -        // try file first
    -        var file = null;
    -        try {
    -            file = pathToFile(str);
    -        } catch (e) {
    -        }
    -        if (file && file.exists()) {
    -            return new FileInputStream(file);
    -        } else {
    -            try {
    -                // treat the string as URL
    -                return new URL(str).openStream();
    -            } catch (e) {
    -                throw 'file or URL ' + str + ' not found';
    -            }
    -        }
    -    } else {
    -        if (str instanceof InputStream) {
    -            return str;
    -        } else if (str instanceof URL) {
    -            return str.openStream();
    -        } else if (str instanceof File) {
    -            return new FileInputStream(str);
    -        }
    -    }
    -    // everything failed, just give input stream
    -    return java.lang.System["in"];
    -}
    -
    -/**
    - * Generic any object to output stream mapper
    - *
    - * @param out output file name or stream
    - * @return OutputStream object
    - * @private
    - */
    -function outStream(out) {
    -    if (typeof(out) == "string") {
    -        if (out == '>') {
    -            return java.lang.System.out;
    -        } else {
    -            // treat it as file
    -            return new FileOutputStream(pathToFile(out));
    -        }
    -    } else {
    -        if (out instanceof OutputStream) {
    -            return out;
    -        } else if (out instanceof File) {
    -            return new FileOutputStream(out);
    -        }
    -    }
    -
    -    // everything failed, just return System.out
    -    return java.lang.System.out;
    -}
    -
    -/**
    - * stream close takes care not to close stdin, out & err.
    - * @private
    - */
    -function streamClose(stream) {
    -    if (stream) {
    -        if (stream != java.lang.System["in"] &&
    -            stream != java.lang.System.out &&
    -            stream != java.lang.System.err) {
    -            try {
    -                stream.close();
    -            } catch (e) {
    -                println(e);
    -            }
    -        }
    -    }
    -}
    -
    -/**
    - * Loads and evaluates JavaScript code from a stream or file or URL
    - * - * Examples: - *
    - * 
    - *    load('test.js'); // load script file 'test.js'
    - *    load('http://java.sun.com/foo.js'); // load from a URL
    - * 
    - * 
    - * - * @param str input from which script is loaded and evaluated - */ -if (typeof(load) == 'undefined') { - this.load = function(str) { - var stream = inStream(str); - var bstream = new BufferedInputStream(stream); - var reader = new BufferedReader(new InputStreamReader(bstream)); - var oldFilename = engine.get(engine.FILENAME); - engine.put(engine.FILENAME, str); - try { - engine.eval(reader); - } finally { - engine.put(engine.FILENAME, oldFilename); - streamClose(stream); - } - } -} - -// file system utilities - -/** - * Creates a Java byte[] of given length - * @param len size of the array to create - * @private - */ -function javaByteArray(len) { - return java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len); -} - -var curDir = new File('.'); - -/** - * Print present working directory - */ -function pwd() { - println(curDir.getAbsolutePath()); -} - -/** - * Changes present working directory to given directory - * @param target directory to change to. optional, defaults to user's HOME - */ -function cd(target) { - if (target == undefined) { - target = sysProps["user.home"]; - } - if (!(target instanceof File)) { - target = pathToFile(target); - } - if (target.exists() && target.isDirectory()) { - curDir = target; - } else { - println(target + " is not a directory"); - } -} - -/** - * Converts path to java.io.File taking care of shell present working dir - * - * @param pathname file path to be converted - * @private - */ -function pathToFile(pathname) { - var tmp = pathname; - if (!(tmp instanceof File)) { - tmp = new File(tmp); - } - if (!tmp.isAbsolute()) { - return new File(curDir, pathname); - } else { - return tmp; - } -} - -/** - * Copies a file or URL or stream to another file or stream - * - * @param from input file or URL or stream - * @param to output stream or file - */ -function cp(from, to) { - if (from == to) { - println("file " + from + " cannot be copied onto itself!"); - return; - } - var inp = inStream(from); - var out = outStream(to); - var binp = new BufferedInputStream(inp); - var bout = new BufferedOutputStream(out); - var buff = javaByteArray(1024); - var len; - while ((len = binp.read(buff)) > 0 ) - bout.write(buff, 0, len); - - bout.flush(); - streamClose(inp); - streamClose(out); -} - -/** - * Shows the content of a file or URL or any InputStream
    - * Examples: - *
    - * 
    - *    cat('test.txt'); // show test.txt file contents
    - *    cat('http://java.net'); // show the contents from the URL http://java.net
    - * 
    - * 
    - * @param obj input to show - * @param pattern optional. show only the lines matching the pattern - */ -function cat(obj, pattern) { - if (obj instanceof File && obj.isDirectory()) { - ls(obj); - return; - } - - var inp = null; - if (!(obj instanceof Reader)) { - inp = inStream(obj); - obj = new BufferedReader(new InputStreamReader(inp)); - } - var line; - if (pattern) { - var count = 1; - while ((line=obj.readLine()) != null) { - if (line.match(pattern)) { - println(count + "\t: " + line); - } - count++; - } - } else { - while ((line=obj.readLine()) != null) { - println(line); - } - } -} - -/** - * Returns directory part of a filename - * - * @param pathname input path name - * @return directory part of the given file name - */ -function dirname(pathname) { - var dirName = "."; - // Normalize '/' to local file separator before work. - var i = pathname.replace('/', File.separatorChar ).lastIndexOf( - File.separator ); - if ( i != -1 ) - dirName = pathname.substring(0, i); - return dirName; -} - -/** - * Creates a new dir of given name - * - * @param dir name of the new directory - */ -function mkdir(dir) { - dir = pathToFile(dir); - println(dir.mkdir()? "created" : "can not create dir"); -} - -/** - * Creates the directory named by given pathname, including - * any necessary but nonexistent parent directories. - * - * @param dir input path name - */ -function mkdirs(dir) { - dir = pathToFile(dir); - println(dir.mkdirs()? "created" : "can not create dirs"); -} - -/** - * Removes a given file - * - * @param pathname name of the file - */ -function rm(pathname) { - var file = pathToFile(pathname); - if (!file.exists()) { - println("file not found: " + pathname); - return false; - } - // note that delete is a keyword in JavaScript! - println(file["delete"]()? "deleted" : "can not delete"); -} - -/** - * Removes a given directory - * - * @param pathname name of the directory - */ -function rmdir(pathname) { - rm(pathname); -} - -/** - * Synonym for 'rm' - */ -function del(pathname) { - rm(pathname); -} - -/** - * Moves a file to another - * - * @param from original name of the file - * @param to new name for the file - */ -function mv(from, to) { - println(pathToFile(from).renameTo(pathToFile(to))? - "moved" : "can not move"); -} - -/** - * Synonym for 'mv'. - */ -function ren(from, to) { - mv(from, to); -} - -var months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; - -/** - * Helper function called by ls - * @private - */ -function printFile(f) { - var sb = new java.lang.StringBuffer(); - sb.append(f.isDirectory()? "d" : "-"); - sb.append(f.canRead() ? "r": "-" ); - sb.append(f.canWrite() ? "w": "-" ); - sb.append(" "); - - var d = new java.util.Date(f.lastModified()); - var c = new java.util.GregorianCalendar(); - c.setTime(d); - var day = c.get(java.util.Calendar.DAY_OF_MONTH); - sb.append(months[c.get(java.util.Calendar.MONTH)] - + " " + day ); - if (day < 10) { - sb.append(" "); - } - - // to get fixed length 'length' field - var fieldlen = 8; - var len = new java.lang.StringBuffer(); - for(var j=0; j - * - * Examples: - *
    - * 
    - *    find('.')
    - *    find('.', '.*\.class', rm);  // remove all .class files
    - *    find('.', '.*\.java');       // print fullpath of each .java file
    - *    find('.', '.*\.java', cat);  // print all .java files
    - * 
    - * 
    - * - * @param dir directory to search files - * @param pattern to search in the files - * @param callback function to call for matching files - */ -function find(dir, pattern, callback) { - dir = pathToFile(dir); - if (!callback) callback = print; - var files = dir.listFiles(); - for (var f in files) { - var file = files[f]; - if (file.isDirectory()) { - find(file, pattern, callback); - } else { - if (pattern) { - if (file.getName().match(pattern)) { - callback(file); - } - } else { - callback(file); - } - } - } -} - -// process utilities - -/** - * Exec's a child process, waits for completion & returns exit code - * - * @param cmd command to execute in child process - */ -function exec(cmd) { - var process = java.lang.Runtime.getRuntime().exec(cmd); - var inp = new DataInputStream(process.getInputStream()); - var line = null; - while ((line = inp.readLine()) != null) { - println(line); - } - process.waitFor(); - $exit = process.exitValue(); -} - -if (typeof(exit) == 'undefined') { - /** - * Exit the shell program. - * - * @param exitCode integer code returned to OS shell. - * optional, defaults to 0 - */ - this.exit = function (code) { - if (code) { - java.lang.System.exit(code + 0); - } else { - java.lang.System.exit(0); - } - } -} - -if (typeof(quit) == 'undefined') { - /** - * synonym for exit - */ - this.quit = function (code) { - exit(code); - } -} - -// XML utilities - -/** - * Converts input to DOM Document object - * - * @param inp file or reader. optional, without this param, - * this function returns a new DOM Document. - * @return returns a DOM Document object - */ -function XMLDocument(inp) { - var factory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); - var builder = factory.newDocumentBuilder(); - if (inp) { - if (typeof(inp) == "string") { - return builder.parse(pathToFile(inp)); - } else { - return builder.parse(inp); - } - } else { - return builder.newDocument(); - } -} - -/** - * Converts arbitrary stream, file, URL to XMLSource - * - * @param inp input stream or file or URL - * @return XMLSource object - */ -function XMLSource(inp) { - if (inp instanceof javax.xml.transform.Source) { - return inp; - } else if (inp instanceof Packages.org.w3c.dom.Document) { - return new javax.xml.transform.dom.DOMSource(inp); - } else { - inp = new BufferedInputStream(inStream(inp)); - return new javax.xml.transform.stream.StreamSource(inp); - } -} - -/** - * Converts arbitrary stream, file to XMLResult - * - * @param inp output stream or file - * @return XMLResult object - */ -function XMLResult(out) { - if (out instanceof javax.xml.transform.Result) { - return out; - } else if (out instanceof Packages.org.w3c.dom.Document) { - return new javax.xml.transform.dom.DOMResult(out); - } else { - out = new BufferedOutputStream(outStream(out)); - return new javax.xml.transform.stream.StreamResult(out); - } -} - -/** - * Perform XSLT transform - * - * @param inp Input XML to transform (URL, File or InputStream) - * @param style XSL Stylesheet to be used (URL, File or InputStream). optional. - * @param out Output XML (File or OutputStream - */ -function XSLTransform(inp, style, out) { - switch (arguments.length) { - case 2: - inp = arguments[0]; - out = arguments[1]; - break; - case 3: - inp = arguments[0]; - style = arguments[1]; - out = arguments[2]; - break; - default: - println("XSL transform requires 2 or 3 arguments"); - return; - } - - var factory = javax.xml.transform.TransformerFactory.newInstance(); - var transformer; - if (style) { - transformer = factory.newTransformer(XMLSource(style)); - } else { - transformer = factory.newTransformer(); - } - var source = XMLSource(inp); - var result = XMLResult(out); - transformer.transform(source, result); - if (source.getInputStream) { - streamClose(source.getInputStream()); - } - if (result.getOutputStream) { - streamClose(result.getOutputStream()); - } -} - -// miscellaneous utilities - -/** - * Prints which command is selected from PATH - * - * @param cmd name of the command searched from PATH - */ -function which(cmd) { - var st = new java.util.StringTokenizer(env.PATH, File.pathSeparator); - while (st.hasMoreTokens()) { - var file = new File(st.nextToken(), cmd); - if (file.exists()) { - println(file.getAbsolutePath()); - return; - } - } -} - -/** - * Prints IP addresses of given domain name - * - * @param name domain name - */ -function ip(name) { - var addrs = InetAddress.getAllByName(name); - for (var i in addrs) { - println(addrs[i]); - } -} - -/** - * Prints current date in current locale - */ -function date() { - println(new Date().toLocaleString()); -} - -/** - * Echoes the given string arguments - */ -function echo(x) { - for (var i = 0; i < arguments.length; i++) { - println(arguments[i]); - } -} - -if (typeof(printf) == 'undefined') { - /** - * This is C-like printf - * - * @param format string to format the rest of the print items - * @param args variadic argument list - */ - this.printf = function (format, args/*, more args*/) { - var array = java.lang.reflect.Array.newInstance(java.lang.Object, - arguments.length - 1); - for (var i = 0; i < array.length; i++) { - array[i] = arguments[i+1]; - } - java.lang.System.out.printf(format, array); - } -} - -/** - * Reads one or more lines from stdin after printing prompt - * - * @param prompt optional, default is '>' - * @param multiline to tell whether to read single line or multiple lines - */ -function read(prompt, multiline) { - if (!prompt) { - prompt = '>'; - } - var inp = java.lang.System["in"]; - var reader = new BufferedReader(new InputStreamReader(inp)); - if (multiline) { - var line = ''; - while (true) { - java.lang.System.err.print(prompt); - java.lang.System.err.flush(); - var tmp = reader.readLine(); - if (tmp == '' || tmp == null) break; - line += tmp + '\n'; - } - return line; - } else { - java.lang.System.err.print(prompt); - java.lang.System.err.flush(); - return reader.readLine(); - } -} - -if (typeof(println) == 'undefined') { - // just synonym to print - this.println = print; -} - diff --git a/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties b/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties deleted file mode 100644 index a6c7762ed34..00000000000 --- a/src/java.scripting/share/classes/com/sun/tools/script/shell/messages.properties +++ /dev/null @@ -1,68 +0,0 @@ -# -# Copyright (c) 2005, 2024, 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. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# 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. -# - -string.script.error=\ - script error: {0} - -file.script.error=\ - script error in file {0} : {1} - -file.not.found=\ - script file {0} is not found - -engine.not.found=\ - script engine for language {0} can not be found - -engine.info=\ - Language {0} {1} implementation "{2}" {3} - -encoding.unsupported=\ - encoding {0} is not supported - -main.usage=\ -Usage: {0} [options] [arguments...]\n\ -\n\ -where [options] include:\n\ -\ \-classpath Specify where to find user class files \n\ -\ \-cp Specify where to find user class files \n\ -\ \-D= Set a system property \n\ -\ \-J Pass directly to the runtime system \n\ -\ \-l Use specified scripting language \n\ -\ \-e """, """ const pathtoroot = "./"; - loadScripts(document, 'script');""", + loadScripts(); + initTheme();""", "
    ", """
  • Search
  • """, diff --git a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java index 413de03d23b..26030629738 100644 --- a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java +++ b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -23,7 +23,7 @@ /* * @test - * @bug 8266666 8275788 8276964 8299080 + * @bug 8266666 8275788 8276964 8299080 8276966 * @summary Implementation for snippets * @library /tools/lib ../../lib * @modules jdk.compiler/com.sun.tools.javac.api @@ -2166,6 +2166,7 @@ public class TestSnippetTag extends SnippetTester { Hello, Snippet! ----------------- external ----------------- Hello, Snippet!...more + --------------------------------------------
    """); @@ -2207,8 +2208,16 @@ public class TestSnippetTag extends SnippetTester { "pkg"); checkExit(Exit.ERROR); checkOutput(Output.OUT, true, - """ - A.java:4: error: contents mismatch"""); + """ + A.java:4: error: contents mismatch""", + """ + ----------------- inline ------------------- + Hello, Snippet! ...more + \s\s + ----------------- external ----------------- + Hello, Snippet! + \s\s + --------------------------------------------"""); checkOutput("pkg/A.html", true, """
    invalid @snippet @@ -2219,6 +2228,7 @@ public class TestSnippetTag extends SnippetTester { ----------------- external ----------------- Hello, Snippet! + --------------------------------------------
    """); diff --git a/test/langtools/jdk/javadoc/tool/api/basic/APITest.java b/test/langtools/jdk/javadoc/tool/api/basic/APITest.java index e4e3c6baa87..71908f34e99 100644 --- a/test/langtools/jdk/javadoc/tool/api/basic/APITest.java +++ b/test/langtools/jdk/javadoc/tool/api/basic/APITest.java @@ -215,6 +215,7 @@ class APITest { "resource-files/stylesheet.css", "resource-files/sun.svg", "resource-files/x.svg", + "resource-files/sort-a-z.svg", "resource-files/fonts/dejavu.css", "resource-files/fonts/DejaVuLGCSans-Bold.woff", "resource-files/fonts/DejaVuLGCSans-Bold.woff2", @@ -265,4 +266,3 @@ class APITest { && !s.equals("system-properties.html")) .collect(Collectors.toSet()); } - diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 19f7b89a1b3..7907fa4d027 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -941,4 +941,14 @@ public class CompletionSuggestionTest extends KullaTesting { assertEval("import static java.lang.annotation.RetentionPolicy.*;"); assertCompletion("@AnnA(C|", true, "CLASS"); } + + @Test + public void testMultiSnippet() { + assertCompletion("String s = \"\"; s.len|", true, "length()"); + assertCompletion("String s() { return \"\"; } s().len|", true, "length()"); + assertCompletion("String s() { return \"\"; } import java.util.List; List.o|", true, "of("); + assertCompletion("String s() { return \"\"; } import java.ut| ", true, "util."); + assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()"); + assertSignature("void f() { } f(|", "void f()"); + } } diff --git a/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java b/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java index 422c6c18b75..130913df8d4 100644 --- a/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java +++ b/test/langtools/jdk/jshell/JdiBadOptionListenExecutionControlTest.java @@ -47,7 +47,7 @@ public class JdiBadOptionListenExecutionControlTest { // turn on logging of launch failures Logger.getLogger("jdk.jshell.execution").setLevel(Level.ALL); JShell.builder() - .executionEngine("jdi") + .executionEngine(Presets.TEST_JDI_EXECUTION) .remoteVMOptions("-BadBadOption") .build(); } catch (IllegalStateException ex) { diff --git a/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java b/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java index 3cc2a9785a3..a941845ba49 100644 --- a/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java +++ b/test/langtools/jdk/jshell/JdiListeningExecutionControlTest.java @@ -39,6 +39,6 @@ public class JdiListeningExecutionControlTest extends ExecutionControlTestBase { @BeforeEach @Override public void setUp() { - setUp(builder -> builder.executionEngine("jdi")); + setUp(builder -> builder.executionEngine(Presets.TEST_JDI_EXECUTION)); } } diff --git a/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java new file mode 100644 index 00000000000..682c1814c66 --- /dev/null +++ b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* + * @test + * @bug 8366926 + * @summary Verify the instrumenation class hierarchy resolution works properly in local execution mode + * @library /tools/lib + * @modules + * jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * @build KullaTesting + * @run junit/othervm LocalExecutionInstrumentationCHRTest + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LocalExecutionInstrumentationCHRTest extends ReplToolTesting { + + @Test + public void verifyMyClassFoundOnClassPath() { + test(new String[] { "--execution", "local" }, + a -> assertCommand(a, "public interface TestInterface {}", "| created interface TestInterface"), + a -> assertCommand(a, + "public class TestClass {" + + "public TestInterface foo(boolean b) {" + + "TestInterface test; " + + "if (b) {" + + "test = new TestInterfaceImpl1();" + + "} else {" + + "test = new TestInterfaceImpl2();" + + "}" + + "return test;" + + "}" + + "private class TestInterfaceImpl1 implements TestInterface {}" + + "private class TestInterfaceImpl2 implements TestInterface {}" + + "}", "| created class TestClass"), + a -> assertCommand(a, "new TestClass().foo(true).getClass();", "$3 ==> class TestClass$TestInterfaceImpl1"), + a -> assertCommand(a, "new TestClass().foo(false).getClass();", "$4 ==> class TestClass$TestInterfaceImpl2") + ); + } +} diff --git a/test/langtools/jdk/jshell/Presets.java b/test/langtools/jdk/jshell/Presets.java index b9a93c967dc..afe193ee5c3 100644 --- a/test/langtools/jdk/jshell/Presets.java +++ b/test/langtools/jdk/jshell/Presets.java @@ -25,16 +25,23 @@ import java.net.InetAddress; import java.util.*; public class Presets { + private static final int TEST_TIMEOUT = 20_000; + public static final String TEST_DEFAULT_EXECUTION; public static final String TEST_STANDARD_EXECUTION; + public static final String TEST_JDI_EXECUTION; static { String loopback = InetAddress.getLoopbackAddress().getHostAddress(); TEST_DEFAULT_EXECUTION = "failover:0(jdi:hostname(" + loopback + "))," + - "1(jdi:launch(true)), 2(jdi), 3(local)"; + "1(jdi:launch(true))," + + "2(jdi:timeout(" + TEST_TIMEOUT + "))," + + "3(local)"; TEST_STANDARD_EXECUTION = "failover:0(jdi:hostname(" + loopback + "))," + - "1(jdi:launch(true)), 2(jdi)"; + "1(jdi:launch(true))," + + "2(jdi:timeout(" + TEST_TIMEOUT + "))"; + TEST_JDI_EXECUTION = "jdi:timeout(" + TEST_TIMEOUT + ")"; } public static String[] addExecutionIfMissing(String[] args) { diff --git a/test/langtools/tools/javac/HexThree.java b/test/langtools/tools/javac/HexThree.java index a89db6e99f8..81c69a4f532 100644 --- a/test/langtools/tools/javac/HexThree.java +++ b/test/langtools/tools/javac/HexThree.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 4920023 * @summary Test hex floating-point literals - * @author darcy */ public class HexThree { diff --git a/test/langtools/tools/javac/OverrideChecks/Private.out b/test/langtools/tools/javac/OverrideChecks/Private.out index f49ef46a255..2ccbecd6d3a 100644 --- a/test/langtools/tools/javac/OverrideChecks/Private.out +++ b/test/langtools/tools/javac/OverrideChecks/Private.out @@ -1,2 +1,2 @@ -Private.java:14:5: compiler.err.method.does.not.override.superclass +Private.java:14:5: compiler.err.method.does.not.override.superclass: m(), Bar 1 error diff --git a/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java b/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java index 160c3a3093d..58dfc69ee2c 100644 --- a/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java +++ b/test/langtools/tools/javac/StringsInSwitch/OneCaseSwitches.java @@ -4,7 +4,6 @@ * @summary Positive tests for strings in switch with few alternatives. * @compile OneCaseSwitches.java * @run main OneCaseSwitches - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java b/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java index 2e6737e0c05..cd775a1bcb3 100644 --- a/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java +++ b/test/langtools/tools/javac/StringsInSwitch/StringSwitches.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6827009 7071246 * @summary Positive tests for strings in switch. - * @author Joseph D. Darcy */ public class StringSwitches { diff --git a/test/langtools/tools/javac/TryWithResources/BadTwr.java b/test/langtools/tools/javac/TryWithResources/BadTwr.java index 6043c527af1..a38300b7036 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwr.java +++ b/test/langtools/tools/javac/TryWithResources/BadTwr.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Verify bad TWRs don't compile * @compile/fail/ref=BadTwr.out -XDrawDiagnostics BadTwr.java */ diff --git a/test/langtools/tools/javac/TryWithResources/BadTwr.out b/test/langtools/tools/javac/TryWithResources/BadTwr.out index ed8c2524c1b..b4e951305a5 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwr.out +++ b/test/langtools/tools/javac/TryWithResources/BadTwr.out @@ -1,5 +1,5 @@ -BadTwr.java:12:46: compiler.err.already.defined: kindname.variable, r1, kindname.method, meth(java.lang.String...) -BadTwr.java:17:20: compiler.err.already.defined: kindname.variable, args, kindname.method, meth(java.lang.String...) -BadTwr.java:20:13: compiler.err.cant.assign.val.to.var: final, thatsIt -BadTwr.java:25:24: compiler.err.already.defined: kindname.variable, name, kindname.method, meth(java.lang.String...) +BadTwr.java:11:46: compiler.err.already.defined: kindname.variable, r1, kindname.method, meth(java.lang.String...) +BadTwr.java:16:20: compiler.err.already.defined: kindname.variable, args, kindname.method, meth(java.lang.String...) +BadTwr.java:19:13: compiler.err.cant.assign.val.to.var: final, thatsIt +BadTwr.java:24:24: compiler.err.already.defined: kindname.variable, name, kindname.method, meth(java.lang.String...) 4 errors diff --git a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java index 988249cef20..fc31895315c 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java +++ b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Verify bad TWRs don't compile * @compile/fail/ref=BadTwrSyntax.out -XDrawDiagnostics BadTwrSyntax.java */ diff --git a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out index 0d7263710c5..44b283dc611 100644 --- a/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out +++ b/test/langtools/tools/javac/TryWithResources/BadTwrSyntax.out @@ -1,2 +1,2 @@ -BadTwrSyntax.java:13:43: compiler.err.illegal.start.of.expr +BadTwrSyntax.java:12:43: compiler.err.illegal.start.of.expr 1 error diff --git a/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java b/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java index e77de854f47..472b9089c1e 100644 --- a/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java +++ b/test/langtools/tools/javac/TryWithResources/ExplicitFinal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -24,7 +24,6 @@ /* * @test * @bug 7013420 - * @author Joseph D. Darcy * @summary Test that resource variables are accepted as explicitly final. */ diff --git a/test/langtools/tools/javac/TryWithResources/PlainTry.java b/test/langtools/tools/javac/TryWithResources/PlainTry.java index edbb233578b..52c05a1ff7c 100644 --- a/test/langtools/tools/javac/TryWithResources/PlainTry.java +++ b/test/langtools/tools/javac/TryWithResources/PlainTry.java @@ -1,9 +1,8 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Test error messages for an unadorned try - * @compile/fail/ref=PlainTry.out -XDrawDiagnostics PlainTry.java + * @compile/fail/ref=PlainTry.out -XDrawDiagnostics PlainTry.java */ public class PlainTry { public static void meth() { diff --git a/test/langtools/tools/javac/TryWithResources/PlainTry.out b/test/langtools/tools/javac/TryWithResources/PlainTry.out index 9357cd8cdd7..0f8f16ae1e8 100644 --- a/test/langtools/tools/javac/TryWithResources/PlainTry.out +++ b/test/langtools/tools/javac/TryWithResources/PlainTry.out @@ -1,2 +1,2 @@ -PlainTry.java:10:9: compiler.err.try.without.catch.finally.or.resource.decls +PlainTry.java:9:9: compiler.err.try.without.catch.finally.or.resource.decls 1 error diff --git a/test/langtools/tools/javac/TryWithResources/TwrFlow.java b/test/langtools/tools/javac/TryWithResources/TwrFlow.java index 8ea54244570..366648fb42e 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrFlow.java +++ b/test/langtools/tools/javac/TryWithResources/TwrFlow.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Test exception analysis of try-with-resources blocks * @compile/fail/ref=TwrFlow.out -XDrawDiagnostics TwrFlow.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrFlow.out b/test/langtools/tools/javac/TryWithResources/TwrFlow.out index 23db6517c43..758fb778aae 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrFlow.out +++ b/test/langtools/tools/javac/TryWithResources/TwrFlow.out @@ -1,3 +1,3 @@ -TwrFlow.java:14:11: compiler.err.except.never.thrown.in.try: java.io.IOException -TwrFlow.java:12:21: compiler.err.unreported.exception.implicit.close: CustomCloseException, twrFlow +TwrFlow.java:13:11: compiler.err.except.never.thrown.in.try: java.io.IOException +TwrFlow.java:11:21: compiler.err.unreported.exception.implicit.close: CustomCloseException, twrFlow 2 errors diff --git a/test/langtools/tools/javac/TryWithResources/TwrLint.java b/test/langtools/tools/javac/TryWithResources/TwrLint.java index 39c5ed67f8f..ca45efc1e0a 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrLint.java +++ b/test/langtools/tools/javac/TryWithResources/TwrLint.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 6965277 6967065 - * @author Joseph D. Darcy * @summary Check that -Xlint:twr warnings are generated as expected * @compile/ref=TwrLint.out -Xlint:try,deprecation -XDrawDiagnostics TwrLint.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrLint.out b/test/langtools/tools/javac/TryWithResources/TwrLint.out index 6adfc5122b3..28778b0e3a5 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrLint.out +++ b/test/langtools/tools/javac/TryWithResources/TwrLint.out @@ -1,3 +1,3 @@ -TwrLint.java:14:15: compiler.warn.try.explicit.close.call -TwrLint.java:13:21: compiler.warn.try.resource.not.referenced: r3 +TwrLint.java:13:15: compiler.warn.try.explicit.close.call +TwrLint.java:12:21: compiler.warn.try.resource.not.referenced: r3 2 warnings diff --git a/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java b/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java index dbbb633efe1..002e30277e3 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java +++ b/test/langtools/tools/javac/TryWithResources/TwrMultiCatch.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -24,7 +24,6 @@ /* * @test * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Test that TWR and multi-catch play well together * @compile TwrMultiCatch.java * @run main TwrMultiCatch diff --git a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java index ac0a72645ba..0d6c2f4bc44 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java +++ b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 7013420 - * @author Joseph D. Darcy * @summary Verify invalid TWR block is not accepted. * @compile/fail/ref=TwrOnNonResource.out -XDrawDiagnostics TwrOnNonResource.java */ diff --git a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out index 07488a78071..9a01e28e105 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out +++ b/test/langtools/tools/javac/TryWithResources/TwrOnNonResource.out @@ -1,4 +1,4 @@ -TwrOnNonResource.java:11:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) -TwrOnNonResource.java:14:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) -TwrOnNonResource.java:17:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:10:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:13:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) +TwrOnNonResource.java:16:30: compiler.err.prob.found.req: (compiler.misc.try.not.applicable.to.type: (compiler.misc.inconvertible.types: TwrOnNonResource, java.lang.AutoCloseable)) 3 errors diff --git a/test/langtools/tools/javac/TryWithResources/TwrSuppression.java b/test/langtools/tools/javac/TryWithResources/TwrSuppression.java index 9834d5a17b2..0c511d28cbc 100644 --- a/test/langtools/tools/javac/TryWithResources/TwrSuppression.java +++ b/test/langtools/tools/javac/TryWithResources/TwrSuppression.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -24,7 +24,6 @@ /* * @test * @bug 6971877 - * @author Joseph D. Darcy * @summary Verify a primary exception suppresses all throwables */ diff --git a/test/langtools/tools/javac/TryWithResources/WeirdTwr.java b/test/langtools/tools/javac/TryWithResources/WeirdTwr.java index 6331507097c..fbfbbe6ecd8 100644 --- a/test/langtools/tools/javac/TryWithResources/WeirdTwr.java +++ b/test/langtools/tools/javac/TryWithResources/WeirdTwr.java @@ -1,7 +1,6 @@ /* * @test /nodynamiccopyright/ * @bug 6911256 6964740 - * @author Joseph D. Darcy * @summary Strange TWRs * @compile WeirdTwr.java * @run main WeirdTwr diff --git a/test/langtools/tools/javac/annotations/6359949/T6359949a.out b/test/langtools/tools/javac/annotations/6359949/T6359949a.out index f179ca1bc1a..2c2aed1da27 100644 --- a/test/langtools/tools/javac/annotations/6359949/T6359949a.out +++ b/test/langtools/tools/javac/annotations/6359949/T6359949a.out @@ -1,2 +1,2 @@ -T6359949a.java:15:5: compiler.err.static.methods.cannot.be.annotated.with.override +T6359949a.java:15:5: compiler.err.static.methods.cannot.be.annotated.with.override: example(), Test 1 error diff --git a/test/langtools/tools/javac/annotations/crash_empty_enum_const/CrashEmptyEnumConstructorTest.java b/test/langtools/tools/javac/annotations/crash_empty_enum_const/CrashEmptyEnumConstructorTest.java index 29340dd720e..865c53a19c3 100644 --- a/test/langtools/tools/javac/annotations/crash_empty_enum_const/CrashEmptyEnumConstructorTest.java +++ b/test/langtools/tools/javac/annotations/crash_empty_enum_const/CrashEmptyEnumConstructorTest.java @@ -92,7 +92,7 @@ public class CrashEmptyEnumConstructorTest extends TestRunner { """); List expected = List.of( - "E.java:3: error: missing method body, or declare abstract", + "E.java:3: error: method E(String) in E is missing a method body, or should be declared abstract", " E(String one);", " ^", "1 error"); diff --git a/test/langtools/tools/javac/annotations/neg/OverrideNo.out b/test/langtools/tools/javac/annotations/neg/OverrideNo.out index 2af05a13fc3..34f4fe08e2a 100644 --- a/test/langtools/tools/javac/annotations/neg/OverrideNo.out +++ b/test/langtools/tools/javac/annotations/neg/OverrideNo.out @@ -1,2 +1,2 @@ -OverrideNo.java:16:5: compiler.err.method.does.not.override.superclass +OverrideNo.java:16:5: compiler.err.method.does.not.override.superclass: f(), overrideNo.B 1 error diff --git a/test/langtools/tools/javac/annotations/pos/TrailingComma.java b/test/langtools/tools/javac/annotations/pos/TrailingComma.java index d6601ce419d..0fa4bdb3546 100644 --- a/test/langtools/tools/javac/annotations/pos/TrailingComma.java +++ b/test/langtools/tools/javac/annotations/pos/TrailingComma.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6337964 * @summary javac incorrectly disallows trailing comma in annotation arrays - * @author darcy * @compile TrailingComma.java */ diff --git a/test/langtools/tools/javac/annotations/typeAnnotations/NewClassTypeAnnotation.java b/test/langtools/tools/javac/annotations/typeAnnotations/NewClassTypeAnnotation.java index f99cb1c1a34..1f8f700fd82 100644 --- a/test/langtools/tools/javac/annotations/typeAnnotations/NewClassTypeAnnotation.java +++ b/test/langtools/tools/javac/annotations/typeAnnotations/NewClassTypeAnnotation.java @@ -31,7 +31,6 @@ * @run main NewClassTypeAnnotation */ import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.Tree; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; import com.sun.source.util.TreePathScanner; @@ -91,6 +90,7 @@ public class NewClassTypeAnnotation extends TestRunner { public void testMethod() { new Test<@TypeAnnotation String>(); + new Test<@TypeAnnotation String>() {}; } } """); @@ -112,11 +112,7 @@ public class NewClassTypeAnnotation extends TestRunner { @Override public Void visitNewClass(final NewClassTree node, final Void unused) { TypeMirror type = trees.getTypeMirror(getCurrentPath()); - System.err.println(">>> " + type); - for (Tree t : getCurrentPath()) { - System.err.println(t); - } - actual.add(String.format("Expression: %s, Type: %s", node, type)); + actual.add(String.format("Type: %s", type)); return null; } } @@ -144,8 +140,8 @@ public class NewClassTypeAnnotation extends TestRunner { List expected = List.of( - "Expression: new Test<@TypeAnnotation String>(), Type:" - + " test.Test"); + "Type: test.Test", + "Type: >"); if (!expected.equals(actual)) { throw new AssertionError("expected: " + expected + ", actual: " + actual); } diff --git a/test/langtools/tools/javac/boxing/BoxingCaching.java b/test/langtools/tools/javac/boxing/BoxingCaching.java index 2d7acb817f0..f4c1337f240 100644 --- a/test/langtools/tools/javac/boxing/BoxingCaching.java +++ b/test/langtools/tools/javac/boxing/BoxingCaching.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 4990346 8200478 * @summary Verify autoboxed values are cached as required. - * @author Joseph D. Darcy */ public class BoxingCaching { diff --git a/test/langtools/tools/javac/defaultMethods/private/Private02.out b/test/langtools/tools/javac/defaultMethods/private/Private02.out index cc61d11113f..ba6fb69fdbc 100644 --- a/test/langtools/tools/javac/defaultMethods/private/Private02.out +++ b/test/langtools/tools/javac/defaultMethods/private/Private02.out @@ -1,4 +1,4 @@ Private02.java:13:31: compiler.err.illegal.combination.of.modifiers: abstract, private Private02.java:16:22: compiler.err.already.defined: kindname.method, foo(int), kindname.interface, Private02.I -Private02.java:12:22: compiler.err.missing.meth.body.or.decl.abstract +Private02.java:12:22: compiler.err.missing.meth.body.or.decl.abstract: foo(java.lang.String), Private02.I 3 errors diff --git a/test/langtools/tools/javac/defaultMethods/private/Private08.out b/test/langtools/tools/javac/defaultMethods/private/Private08.out index f259aae76ba..bf1e68c5e73 100644 --- a/test/langtools/tools/javac/defaultMethods/private/Private08.out +++ b/test/langtools/tools/javac/defaultMethods/private/Private08.out @@ -2,7 +2,7 @@ Private08.java:13:28: compiler.err.illegal.combination.of.modifiers: public, pri Private08.java:14:30: compiler.err.illegal.combination.of.modifiers: abstract, private Private08.java:15:29: compiler.err.illegal.combination.of.modifiers: private, default Private08.java:16:24: compiler.err.mod.not.allowed.here: protected -Private08.java:17:22: compiler.err.missing.meth.body.or.decl.abstract +Private08.java:17:22: compiler.err.missing.meth.body.or.decl.abstract: missingBody(), Private08.I Private08.java:22:33: compiler.err.report.access: foo(), private, Private08.I Private08.java:27:21: compiler.err.override.weaker.access: (compiler.misc.clashes.with: doo(), Private08_01.J, doo(), Private08.I), public Private08.java:25:13: compiler.err.non-static.cant.be.ref: kindname.variable, super diff --git a/test/langtools/tools/javac/enum/6350057/T6350057.java b/test/langtools/tools/javac/enum/6350057/T6350057.java index ff96b710af6..8eb0aa997b6 100644 --- a/test/langtools/tools/javac/enum/6350057/T6350057.java +++ b/test/langtools/tools/javac/enum/6350057/T6350057.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6350057 7025809 * @summary Test that parameters on implicit enum methods have the right kind - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/enum/AbstractEmptyEnum.java b/test/langtools/tools/javac/enum/AbstractEmptyEnum.java index 38c13bde828..7f5792167e8 100644 --- a/test/langtools/tools/javac/enum/AbstractEmptyEnum.java +++ b/test/langtools/tools/javac/enum/AbstractEmptyEnum.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary empty enum cannot be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=AbstractEmptyEnum.out -XDrawDiagnostics AbstractEmptyEnum.java */ diff --git a/test/langtools/tools/javac/enum/AbstractEmptyEnum.out b/test/langtools/tools/javac/enum/AbstractEmptyEnum.out index 933363a3fe6..b6b2460bb68 100644 --- a/test/langtools/tools/javac/enum/AbstractEmptyEnum.out +++ b/test/langtools/tools/javac/enum/AbstractEmptyEnum.out @@ -1,2 +1,2 @@ -AbstractEmptyEnum.java:10:8: compiler.err.does.not.override.abstract: AbstractEmptyEnum, m(), AbstractEmptyEnum +AbstractEmptyEnum.java:9:8: compiler.err.does.not.override.abstract: AbstractEmptyEnum, m(), AbstractEmptyEnum 1 error diff --git a/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java b/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java index 36c090668c4..8ed9e148978 100644 --- a/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java +++ b/test/langtools/tools/javac/enum/EnumImplicitPrivateConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 5009601 5010455 5005748 * @summary enum constructors can be declared private - * @author Joseph D. Darcy */ import java.util.*; diff --git a/test/langtools/tools/javac/enum/EnumPrivateConstructor.java b/test/langtools/tools/javac/enum/EnumPrivateConstructor.java index 84a28a81976..412f79945c8 100644 --- a/test/langtools/tools/javac/enum/EnumPrivateConstructor.java +++ b/test/langtools/tools/javac/enum/EnumPrivateConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 5009601 * @summary enum constructors can be declared private - * @author Joseph D. Darcy * * @compile EnumPrivateConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumProtectedConstructor.java b/test/langtools/tools/javac/enum/EnumProtectedConstructor.java index 481de39319c..298dcc738c9 100644 --- a/test/langtools/tools/javac/enum/EnumProtectedConstructor.java +++ b/test/langtools/tools/javac/enum/EnumProtectedConstructor.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum constructors cannot be declared public or protected - * @author Joseph D. Darcy * * @compile/fail/ref=EnumProtectedConstructor.out -XDrawDiagnostics EnumProtectedConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumProtectedConstructor.out b/test/langtools/tools/javac/enum/EnumProtectedConstructor.out index 71826a2d331..5cc734020ac 100644 --- a/test/langtools/tools/javac/enum/EnumProtectedConstructor.out +++ b/test/langtools/tools/javac/enum/EnumProtectedConstructor.out @@ -1,2 +1,2 @@ -EnumProtectedConstructor.java:16:15: compiler.err.mod.not.allowed.here: protected +EnumProtectedConstructor.java:15:15: compiler.err.mod.not.allowed.here: protected 1 error diff --git a/test/langtools/tools/javac/enum/EnumPublicConstructor.java b/test/langtools/tools/javac/enum/EnumPublicConstructor.java index 31360597035..b5c622208ef 100644 --- a/test/langtools/tools/javac/enum/EnumPublicConstructor.java +++ b/test/langtools/tools/javac/enum/EnumPublicConstructor.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum constructors cannot be declared public or protected - * @author Joseph D. Darcy * * @compile/fail/ref=EnumPublicConstructor.out -XDrawDiagnostics EnumPublicConstructor.java */ diff --git a/test/langtools/tools/javac/enum/EnumPublicConstructor.out b/test/langtools/tools/javac/enum/EnumPublicConstructor.out index 56c099f4330..89ba98d1d81 100644 --- a/test/langtools/tools/javac/enum/EnumPublicConstructor.out +++ b/test/langtools/tools/javac/enum/EnumPublicConstructor.out @@ -1,2 +1,2 @@ -EnumPublicConstructor.java:16:12: compiler.err.mod.not.allowed.here: public +EnumPublicConstructor.java:15:12: compiler.err.mod.not.allowed.here: public 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java index 5af53768684..d04a2558715 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared abstract - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyAbstractEnum1.out -XDrawDiagnostics ExplicitlyAbstractEnum1.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out index 827e331a491..902c9e5730f 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum1.out @@ -1,2 +1,2 @@ -ExplicitlyAbstractEnum1.java:9:10: compiler.err.mod.not.allowed.here: abstract +ExplicitlyAbstractEnum1.java:8:10: compiler.err.mod.not.allowed.here: abstract 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java index cbd9bd512d6..293c36a867d 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared abstract even if they are abstract - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyAbstractEnum2.out -XDrawDiagnostics ExplicitlyAbstractEnum2.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out index 8a35cec3180..f2c25ca5d4e 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out +++ b/test/langtools/tools/javac/enum/ExplicitlyAbstractEnum2.out @@ -1,2 +1,2 @@ -ExplicitlyAbstractEnum2.java:9:10: compiler.err.mod.not.allowed.here: abstract +ExplicitlyAbstractEnum2.java:8:10: compiler.err.mod.not.allowed.here: abstract 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java index 1dba3d0dff5..1e2bf650ca5 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared final even if they are - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyFinalEnum1.out -XDrawDiagnostics ExplicitlyFinalEnum1.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out index 023d00e265f..11bbdb690b6 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum1.out @@ -1,2 +1,2 @@ -ExplicitlyFinalEnum1.java:9:7: compiler.err.mod.not.allowed.here: final +ExplicitlyFinalEnum1.java:8:7: compiler.err.mod.not.allowed.here: final 1 error diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java index 73d3b8acb7b..0c02799c3d3 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary enum's cannot be explicitly declared final - * @author Joseph D. Darcy * @compile/fail/ref=ExplicitlyFinalEnum2.out -XDrawDiagnostics ExplicitlyFinalEnum2.java */ diff --git a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out index 09cbf1461ad..8d67c888864 100644 --- a/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out +++ b/test/langtools/tools/javac/enum/ExplicitlyFinalEnum2.out @@ -1,2 +1,2 @@ -ExplicitlyFinalEnum2.java:9:7: compiler.err.mod.not.allowed.here: final +ExplicitlyFinalEnum2.java:8:7: compiler.err.mod.not.allowed.here: final 1 error diff --git a/test/langtools/tools/javac/enum/FauxEnum1.java b/test/langtools/tools/javac/enum/FauxEnum1.java index 5ae6a8b090c..7565633032f 100644 --- a/test/langtools/tools/javac/enum/FauxEnum1.java +++ b/test/langtools/tools/javac/enum/FauxEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009574 * @summary verify java.lang.Enum can't be directly subclassed - * @author Joseph D. Darcy * * @compile/fail/ref=FauxEnum1.out -XDrawDiagnostics FauxEnum1.java */ diff --git a/test/langtools/tools/javac/enum/FauxEnum1.out b/test/langtools/tools/javac/enum/FauxEnum1.out index ff1e8fa7315..7cae5443e25 100644 --- a/test/langtools/tools/javac/enum/FauxEnum1.out +++ b/test/langtools/tools/javac/enum/FauxEnum1.out @@ -1,4 +1,4 @@ -FauxEnum1.java:10:8: compiler.err.enum.no.subclassing +FauxEnum1.java:9:8: compiler.err.enum.no.subclassing - compiler.note.unchecked.filename: FauxEnum1.java - compiler.note.unchecked.recompile 1 error diff --git a/test/langtools/tools/javac/enum/FauxEnum3.java b/test/langtools/tools/javac/enum/FauxEnum3.java index ec53aeb5951..6cf41cfa889 100644 --- a/test/langtools/tools/javac/enum/FauxEnum3.java +++ b/test/langtools/tools/javac/enum/FauxEnum3.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009574 * @summary verify an enum type can't be directly subclassed - * @author Joseph D. Darcy * * @compile/fail/ref=FauxEnum3.out -XDrawDiagnostics FauxEnum3.java */ diff --git a/test/langtools/tools/javac/enum/FauxEnum3.out b/test/langtools/tools/javac/enum/FauxEnum3.out index 885154d073a..33596c87b51 100644 --- a/test/langtools/tools/javac/enum/FauxEnum3.out +++ b/test/langtools/tools/javac/enum/FauxEnum3.out @@ -1,2 +1,2 @@ -FauxEnum3.java:10:14: compiler.err.enum.types.not.extensible +FauxEnum3.java:9:14: compiler.err.enum.types.not.extensible 1 error diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum1.java b/test/langtools/tools/javac/enum/FauxSpecialEnum1.java index 5b427bb110d..578864061d0 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum1.java +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum1.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary verify specialized enum classes can't be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=FauxSpecialEnum1.out -XDrawDiagnostics FauxSpecialEnum1.java */ diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum1.out b/test/langtools/tools/javac/enum/FauxSpecialEnum1.out index 4530bf40a88..ed9a5feaf1f 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum1.out +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum1.out @@ -1,2 +1,2 @@ -FauxSpecialEnum1.java:14:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum1$2, test(), compiler.misc.anonymous.class: FauxSpecialEnum1$2 +FauxSpecialEnum1.java:13:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum1$2, test(), compiler.misc.anonymous.class: FauxSpecialEnum1$2 1 error diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum2.java b/test/langtools/tools/javac/enum/FauxSpecialEnum2.java index 5c0cc610fa2..6bf9a97f29c 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum2.java +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum2.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 5009601 * @summary verify specialized enum classes can't be abstract - * @author Joseph D. Darcy * * @compile/fail/ref=FauxSpecialEnum2.out -XDrawDiagnostics FauxSpecialEnum2.java */ diff --git a/test/langtools/tools/javac/enum/FauxSpecialEnum2.out b/test/langtools/tools/javac/enum/FauxSpecialEnum2.out index 81de8f41726..aa1cbc8e9cc 100644 --- a/test/langtools/tools/javac/enum/FauxSpecialEnum2.out +++ b/test/langtools/tools/javac/enum/FauxSpecialEnum2.out @@ -1,2 +1,2 @@ -FauxSpecialEnum2.java:12:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum2$1, test(), compiler.misc.anonymous.class: FauxSpecialEnum2$1 +FauxSpecialEnum2.java:11:5: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: FauxSpecialEnum2$1, test(), compiler.misc.anonymous.class: FauxSpecialEnum2$1 1 error diff --git a/test/langtools/tools/javac/file/FSInfoTest.java b/test/langtools/tools/javac/file/FSInfoTest.java index bbd9da9bc97..e9b0c9f55bf 100644 --- a/test/langtools/tools/javac/file/FSInfoTest.java +++ b/test/langtools/tools/javac/file/FSInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -23,7 +23,6 @@ import com.sun.tools.javac.file.FSInfo; import com.sun.tools.javac.util.Context; -import org.testng.annotations.Test; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +30,7 @@ import java.nio.file.Path; import java.util.Locale; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; /* * @test @@ -38,7 +38,7 @@ import java.util.jar.Manifest; * @summary Test com.sun.tools.javac.file.FSInfo * @modules jdk.compiler/com.sun.tools.javac.util * jdk.compiler/com.sun.tools.javac.file - * @run testng FSInfoTest + * @run junit FSInfoTest */ public class FSInfoTest { diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java index 54519aa6afa..291e8386fe3 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -30,14 +30,9 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox - * @run testng MultiReleaseJarAwareSJFM + * @run junit MultiReleaseJarAwareSJFM */ -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import javax.tools.FileObject; import javax.tools.JavaFileManager; @@ -50,11 +45,18 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarAwareSJFM { private static final int CURRENT_VERSION = Runtime.version().major(); @@ -113,7 +115,7 @@ public class MultiReleaseJarAwareSJFM { } }; - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9", @@ -140,7 +142,7 @@ public class MultiReleaseJarAwareSJFM { .run(); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "classes/META-INF/versions/" + CURRENT_VERSION + "/version/Version.class", @@ -159,7 +161,6 @@ public class MultiReleaseJarAwareSJFM { ); } - @DataProvider(name = "versions") public Object[][] data() { return new Object[][] { {"", 8}, @@ -169,7 +170,8 @@ public class MultiReleaseJarAwareSJFM { }; } - @Test(dataProvider = "versions") + @ParameterizedTest + @MethodSource("data") public void test(String version, int expected) throws Throwable { StandardJavaFileManager jfm = ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null); jfm.setLocation(jloc, List.of(new File("multi-release.jar"))); @@ -183,7 +185,7 @@ public class MultiReleaseJarAwareSJFM { MethodType mt = MethodType.methodType(int.class); MethodHandle mh = MethodHandles.lookup().findVirtual(versionClass, "getVersion", mt); int v = (int)mh.invoke(versionClass.newInstance()); - Assert.assertEquals(v, expected); + Assertions.assertEquals(expected, v); jfm.close(); } diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java index 09209e14c9d..1c1704510ca 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java @@ -30,13 +30,14 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask - * @run testng/timeout=480 MultiReleaseJarTest + * @run junit/timeout=480 MultiReleaseJarTest */ -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; @@ -44,6 +45,7 @@ import toolbox.Task; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarTest { private final String main1 = @@ -84,7 +86,7 @@ public class MultiReleaseJarTest { private final ToolBox tb = new ToolBox(); - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9"); new JavacTask(tb) @@ -111,7 +113,7 @@ public class MultiReleaseJarTest { ); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "multi-release.jar", @@ -120,8 +122,9 @@ public class MultiReleaseJarTest { ); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -134,8 +137,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main1Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -148,8 +152,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -162,8 +167,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -176,8 +182,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main2Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -190,8 +197,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -204,7 +212,6 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @DataProvider(name="modes") public Object[][] createModes() { return new Object[][] { new Object[] {Task.Mode.API}, diff --git a/test/langtools/tools/javac/generics/InheritanceConflict3.java b/test/langtools/tools/javac/generics/InheritanceConflict3.java index ae725d6c1e2..d9f3c76b06e 100644 --- a/test/langtools/tools/javac/generics/InheritanceConflict3.java +++ b/test/langtools/tools/javac/generics/InheritanceConflict3.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 4984158 * @summary two inherited methods with same signature - * @author darcy * * @compile/fail/ref=InheritanceConflict3.out -XDrawDiagnostics InheritanceConflict3.java */ diff --git a/test/langtools/tools/javac/generics/InheritanceConflict3.out b/test/langtools/tools/javac/generics/InheritanceConflict3.out index 60cfe5d2148..c168d4b2b86 100644 --- a/test/langtools/tools/javac/generics/InheritanceConflict3.out +++ b/test/langtools/tools/javac/generics/InheritanceConflict3.out @@ -1,3 +1,3 @@ -InheritanceConflict3.java:14:10: compiler.err.name.clash.same.erasure: f(java.lang.Object), f(T) -InheritanceConflict3.java:17:1: compiler.err.concrete.inheritance.conflict: f(java.lang.Object), inheritance.conflict3.X1, f(T), inheritance.conflict3.X1, inheritance.conflict3.X1 +InheritanceConflict3.java:13:10: compiler.err.name.clash.same.erasure: f(java.lang.Object), f(T) +InheritanceConflict3.java:16:1: compiler.err.concrete.inheritance.conflict: f(java.lang.Object), inheritance.conflict3.X1, f(T), inheritance.conflict3.X1, inheritance.conflict3.X1 2 errors diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java index 91ba372bcb3..1a560125e23 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,11 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InInterface + * @run junit InInterface */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; interface LTII { @@ -51,12 +51,12 @@ interface LTII { } -@Test public class InInterface implements LTII { + @Test public void testLambdaInDefaultMethod() { - assertEquals(t1().m(), "yo"); - assertEquals(t2().m("p"), "snurp"); + assertEquals("yo", t1().m()); + assertEquals("snurp", t2().m("p")); } } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java index d5282afc9df..c8322668059 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InnerConstructor + * @run junit InnerConstructor */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class InnerConstructor { + @Test public void testLambdaWithInnerConstructor() { - assertEquals(seq1().m().toString(), "Cbl:nada"); - assertEquals(seq2().m("rats").toString(), "Cbl:rats"); + assertEquals("Cbl:nada", seq1().m().toString()); + assertEquals("Cbl:rats", seq2().m("rats").toString()); } Ib1 seq1() { diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java index b207fd1a7d5..fb602badb25 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,14 +25,12 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 */ -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -import static org.testng.Assert.assertEquals; - -@Test public class LambdaTranslationTest1 extends LT1Sub { String cntxt = "blah"; @@ -43,7 +41,7 @@ public class LambdaTranslationTest1 extends LT1Sub { private static void appendResult(Object s) { result.set(result.get().toString() + s); } private static void assertResult(String expected) { - assertEquals(result.get().toString(), expected); + assertEquals(expected, result.get().toString()); } static Integer count(String s) { @@ -66,6 +64,7 @@ public class LambdaTranslationTest1 extends LT1Sub { setResult(String.format("d:%f", d)); } + @Test public void testLambdas() { TBlock b = t -> {setResult("Sink0::" + t);}; b.apply("Howdy"); @@ -127,6 +126,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertResult("b11: *999*"); } + @Test public void testMethodRefs() { LT1IA ia = LambdaTranslationTest1::eye; ia.doit(1234); @@ -147,6 +147,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertEquals((Integer) 6, a.doit("shower")); } + @Test public void testInner() throws Exception { (new In()).doInner(); } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java index e7e484730b2..7eec78b55d2 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,26 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 */ -import org.testng.annotations.Test; import java.util.ArrayList; import java.util.List; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; /** * LambdaTranslationTest2 -- end-to-end smoke tests for lambda evaluation */ -@Test public class LambdaTranslationTest2 { final String dummy = "dummy"; + @Test public void testLambdas() { TPredicate isEmpty = s -> s.isEmpty(); assertTrue(isEmpty.test("")); @@ -100,6 +100,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testBridges() { Factory of = () -> "y"; Factory ef = () -> "z"; @@ -112,6 +113,7 @@ public class LambdaTranslationTest2 { assertEquals("z", ((Factory) ef).make()); } + @Test public void testBridgesImplicitSpecialization() { StringFactory sf = () -> "x"; @@ -121,6 +123,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testBridgesExplicitSpecialization() { StringFactory2 sf = () -> "x"; @@ -130,6 +133,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testSuperCapture() { class A { String make() { return "x"; } @@ -201,6 +205,7 @@ public class LambdaTranslationTest2 { return String.format("f%f d%f", a0, a1); } + @Test public void testPrimitiveWidening() { WidenS ws1 = LambdaTranslationTest2::pwS1; assertEquals("b1 s2", ws1.m((byte) 1, (short) 2)); @@ -225,11 +230,13 @@ public class LambdaTranslationTest2 { return String.format("b%d s%d c%c i%d j%d z%b f%f d%f", a0, a1, a2, a3, a4, a5, a6, a7); } + @Test public void testUnboxing() { Unbox u = LambdaTranslationTest2::pu; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", u.m((byte)1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); } + @Test public void testBoxing() { Box b = LambdaTranslationTest2::pb; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", b.m((byte) 1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); @@ -239,6 +246,7 @@ public class LambdaTranslationTest2 { return ((String) o).equals("foo"); } + @Test public void testArgCastingAdaptation() { TPredicate p = LambdaTranslationTest2::cc; assertTrue(p.test("foo")); @@ -247,12 +255,14 @@ public class LambdaTranslationTest2 { interface SonOfPredicate extends TPredicate { } + @Test public void testExtendsSAM() { SonOfPredicate p = s -> s.isEmpty(); assertTrue(p.test("")); assertTrue(!p.test("foo")); } + @Test public void testConstructorRef() { Factory> lf = ArrayList::new; List list = lf.make(); @@ -266,6 +276,7 @@ public class LambdaTranslationTest2 { return "private"; } + @Test public void testPrivateMethodRef() { Factory sf = LambdaTranslationTest2::privateMethod; assertEquals("private", sf.make()); @@ -275,6 +286,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testPrivateIntf() { PrivateIntf p = () -> "foo"; assertEquals("foo", p.make()); @@ -284,11 +296,12 @@ public class LambdaTranslationTest2 { public T op(T a, T b); } + @Test public void testBoxToObject() { Op maxer = Math::max; for (int i=-100000; i < 100000; i += 100) for (int j=-100000; j < 100000; j += 99) { - assertEquals((int) maxer.op(i,j), Math.max(i,j)); + assertEquals(Math.max(i,j), (int) maxer.op(i,j)); } } @@ -296,6 +309,7 @@ public class LambdaTranslationTest2 { return "protected"; } + @Test public void testProtectedMethodRef() { Factory sf = LambdaTranslationTest2::protectedMethod; assertEquals("protected", sf.make()); @@ -331,6 +345,7 @@ public class LambdaTranslationTest2 { } } + @Test public void testInnerClassMethodRef() { Factory fs = new Inner1()::m1; assertEquals("Inner1.m1()", fs.make()); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java index a0ecb7c370c..9238b1d2e4e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,27 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestFDCCE + * @run junit MethodReferenceTestFDCCE */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; /** * Method references and raw types. * @author Robert Field */ -@Test @SuppressWarnings({"rawtypes", "unchecked"}) public class MethodReferenceTestFDCCE { static void assertCCE(Throwable t) { - assertEquals(t.getClass().getName(), "java.lang.ClassCastException"); + assertEquals("java.lang.ClassCastException", t.getClass().getName()); } interface Pred { boolean accept(T x); } @@ -79,18 +78,21 @@ public class MethodReferenceTestFDCCE { return 123; } + @Test public void testMethodReferenceFDPrim1() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((Byte)(byte)15)); } + @Test public void testMethodReferenceFDPrim2() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimICCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -102,6 +104,7 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimOCCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -113,12 +116,14 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDRef() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; assertTrue(p2.accept(new B())); } + @Test public void testMethodReferenceFDRefCCE() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; @@ -130,23 +135,27 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimPrim() { Ps p = MethodReferenceTestFDCCE::isMinor; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimBoxed() { Ps p = MethodReferenceTestFDCCE::stst; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimRef() { Oo p = MethodReferenceTestFDCCE::otst; - assertEquals(p.too(15).getClass().getName(), "java.lang.Integer"); + assertEquals("java.lang.Integer", p.too(15).getClass().getName()); } + @Test public void testMethodReferenceFDRet1() { Reto p = MethodReferenceTestFDCCE::ritst; - assertEquals(p.m(), (Short)(short)123); + assertEquals((Short)(short)123, p.m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java index d2a9763a201..f3cce62e7e2 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerDefault + * @run junit MethodReferenceTestInnerDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -60,9 +59,9 @@ interface InDefB extends InDefA { } } -@Test public class MethodReferenceTestInnerDefault implements InDefB { + @Test public void testMethodReferenceInnerDefault() { (new In()).testMethodReferenceInnerDefault(); } @@ -73,13 +72,13 @@ public class MethodReferenceTestInnerDefault implements InDefB { IDSs q; q = MethodReferenceTestInnerDefault.this::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java index 76a82891c3a..326f8057a76 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,24 +25,24 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerInstance + * @run junit MethodReferenceTestInnerInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerInstance { + @Test public void testMethodReferenceInnerInstance() { cia().cib().testMethodReferenceInstance(); } + @Test public void testMethodReferenceInnerExternal() { cia().cib().testMethodReferenceExternal(); } @@ -63,14 +63,14 @@ public class MethodReferenceTestInnerInstance { SI q; q = CIA.this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } public void testMethodReferenceExternal() { SI q; q = (new E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java index a9cc399c27e..ef7eae72429 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerVarArgsThis + * @run junit MethodReferenceTestInnerVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerVarArgsThis { interface NsII { @@ -134,62 +133,62 @@ public class MethodReferenceTestInnerVarArgsThis { NsII q; q = CIA.this::xvO; - assertEquals(q.m(55, 66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55, 66)); } public void testVarArgsNsArray() { Nsai q; q = CIA.this::xvO; - assertEquals(q.m(new int[]{55, 66}), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[]{55, 66})); } public void testVarArgsNsII() { NsII q; q = CIA.this::xvI; - assertEquals(q.m(33, 7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33, 7)); q = CIA.this::xIvI; - assertEquals(q.m(50, 40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50, 40)); q = CIA.this::xvi; - assertEquals(q.m(100, 23), "xvi:123"); + assertEquals("xvi:123", q.m(100, 23)); q = CIA.this::xIvi; - assertEquals(q.m(9, 21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9, 21)); } public void testVarArgsNsiii() { Nsiii q; q = CIA.this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = CIA.this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = CIA.this::xvi; - assertEquals(q.m(900, 80, 7), "xvi:987"); + assertEquals("xvi:987", q.m(900, 80, 7)); q = CIA.this::xIvi; - assertEquals(q.m(333, 27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333, 27, 72)); } public void testVarArgsNsi() { Nsi q; q = CIA.this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = CIA.this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = CIA.this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = CIA.this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args @@ -197,7 +196,7 @@ public class MethodReferenceTestInnerVarArgsThis { NsaO q; q = CIA.this::xvO; - assertEquals(q.m(new String[]{"yo", "there", "dude"}), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[]{"yo", "there", "dude"})); } } @@ -218,28 +217,34 @@ public class MethodReferenceTestInnerVarArgsThis { } // These should be processed as var args + @Test public void testVarArgsNsSuperclass() { cia().cib().testVarArgsNsSuperclass(); } + @Test public void testVarArgsNsArray() { cia().cib().testVarArgsNsArray(); } + @Test public void testVarArgsNsII() { cia().cib().testVarArgsNsII(); } + @Test public void testVarArgsNsiii() { cia().cib().testVarArgsNsiii(); } + @Test public void testVarArgsNsi() { cia().cib().testVarArgsNsi(); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { cia().cib().testVarArgsNsaO(); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java index 15b4a481995..52f650c4c52 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInstance + * @run junit MethodReferenceTestInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -42,7 +41,6 @@ class MethodReferenceTestInstance_E { } } -@Test public class MethodReferenceTestInstance { interface SI { String m(Integer a); } @@ -51,18 +49,20 @@ public class MethodReferenceTestInstance { return "xI:" + i; } + @Test public void testMethodReferenceInstance() { SI q; q = this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } + @Test public void testMethodReferenceExternal() { SI q; q = (new MethodReferenceTestInstance_E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java index e67ed379d13..2b255b3da3e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestKinds + * @run junit MethodReferenceTestKinds */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { interface S0 { String get(); } @@ -65,76 +63,86 @@ public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { static String staticMethod0() { return "SM:0"; } static String staticMethod1(MethodReferenceTestKinds x) { return "SM:1-" + x; } - MethodReferenceTestKinds(String val) { - super(val); - } - MethodReferenceTestKinds() { super("blank"); } MethodReferenceTestKinds inst(String val) { - return new MethodReferenceTestKinds(val); + var inst = new MethodReferenceTestKinds(); + inst.val = val; // simulate `MethodReferenceTestKinds(String val)` constructor + return inst; } + @Test public void testMRBound() { S0 var = this::instanceMethod0; - assertEquals(var.get(), "IM:0-MethodReferenceTestKinds(blank)"); + assertEquals("IM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRBoundArg() { S1 var = this::instanceMethod1; - assertEquals(var.get(inst("arg")), "IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRUnbound() { S1 var = MethodReferenceTestKinds::instanceMethod0; - assertEquals(var.get(inst("rcvr")), "IM:0-MethodReferenceTestKinds(rcvr)"); + assertEquals("IM:0-MethodReferenceTestKinds(rcvr)", var.get(inst("rcvr"))); } + @Test public void testMRUnboundArg() { S2 var = MethodReferenceTestKinds::instanceMethod1; - assertEquals(var.get(inst("rcvr"), inst("arg")), "IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)", var.get(inst("rcvr"), inst("arg"))); } + @Test public void testMRSuper() { S0 var = super::instanceMethod0; - assertEquals(var.get(), "SIM:0-MethodReferenceTestKinds(blank)"); + assertEquals("SIM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRSuperArg() { S1 var = super::instanceMethod1; - assertEquals(var.get(inst("arg")), "SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRStatic() { S0 var = MethodReferenceTestKinds::staticMethod0; - assertEquals(var.get(), "SM:0"); + assertEquals("SM:0", var.get()); } + @Test public void testMRStaticArg() { S1 var = MethodReferenceTestKinds::staticMethod1; - assertEquals(var.get(inst("arg")), "SM:1-MethodReferenceTestKinds(arg)"); + assertEquals("SM:1-MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRTopLevel() { SN0 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make().toString(), "MethodReferenceTestKindsBase(blank)"); + assertEquals("MethodReferenceTestKindsBase(blank)", var.make().toString()); } + @Test public void testMRTopLevelArg() { SN1 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make("name").toString(), "MethodReferenceTestKindsBase(name)"); + assertEquals("MethodReferenceTestKindsBase(name)", var.make("name").toString()); } + @Test public void testMRImplicitInner() { SN0 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make().toString(), "In(blank)"); + assertEquals("In(blank)", var.make().toString()); } + @Test public void testMRImplicitInnerArg() { SN1 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make("name").toString(), "In(name)"); + assertEquals("In(name)", var.make("name").toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java index 05a9f9f3997..003ca2b4781 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -25,16 +25,15 @@ * @test * @bug 8028739 * @summary javac generates incorrect descriptor for MethodHandle::invoke - * @run testng MethodReferenceTestMethodHandle + * @run junit MethodReferenceTestMethodHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestMethodHandle { MethodHandles.Lookup lookup = MethodHandles.lookup(); @@ -51,6 +50,7 @@ public class MethodReferenceTestMethodHandle { void apply(List st, int idx, Object v) throws Throwable; } + @Test public void testVirtual() throws Throwable { MethodType mt = MethodType.methodType(String.class, char.class, char.class); @@ -69,6 +69,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("oome otring to oearch", ((ReplaceItf) ms::invoke).apply("some string to search", 's', 'o')); } + @Test public void testStatic() throws Throwable { MethodType fmt = MethodType.methodType(String.class, String.class, (new Object[1]).getClass()); MethodHandle fms = lookup.findStatic(String.class, "format", fmt); @@ -83,6 +84,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("Testing One 2 3 four", ff2.apply("Testing %s %d %x %s", "One", new Integer(2), 3, "four")); } + @Test public void testVoid() throws Throwable { MethodType pmt = MethodType.methodType(void.class, int.class, Object.class); MethodHandle pms = lookup.findVirtual(List.class, "add", pmt); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java index 15a35a7d0b3..26cf3ffd61d 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNew + * @run junit MethodReferenceTestNew */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNew { interface M0 { @@ -105,32 +103,36 @@ public class MethodReferenceTestNew { } } + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "N2(7,hi)"); + assertEquals("N2(7,hi)", q.m(7, "hi").toString()); } + @Test public void testConstructorReferenceVarArgs() { MV q; q = NV::new; - assertEquals(q.m(5, 45).toString(), "NV(50)"); + assertEquals("NV(50)", q.m(5, 45).toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java index 1f61de4da8f..324428653da 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNewInner + * @run junit MethodReferenceTestNewInner */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNewInner { String note = "NO NOTE"; @@ -120,26 +118,29 @@ public class MethodReferenceTestNewInner { assertEquals(q.m(new MethodReferenceTestNewInner()).getClass().getSimpleName(), "N0"); } */ + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; note = "T2"; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "T2:N2(7,hi)"); + assertEquals("T2:N2(7,hi)", q.m(7, "hi").toString()); } /*** diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java index ae3d7d6cc83..a172dbd8ce0 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8011591 * @summary BootstrapMethodError when capturing constructor ref to local classes - * @run testng MethodReferenceTestNewInnerImplicitArgs + * @run junit MethodReferenceTestNewInnerImplicitArgs */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * Test the case that a constructor has implicit parameters added to @@ -39,7 +38,6 @@ import static org.testng.Assert.assertEquals; * @author Robert Field */ -@Test public class MethodReferenceTestNewInnerImplicitArgs { @@ -56,7 +54,8 @@ public class MethodReferenceTestNewInnerImplicitArgs { S m(int i, int j); } - public static void testConstructorReferenceImplicitParameters() { + @Test + public void testConstructorReferenceImplicitParameters() { String title = "Hey"; String a2 = "!!!"; class MS extends S { @@ -66,7 +65,7 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I result = MS::new; - assertEquals(result.m().b, "Hey!!!"); + assertEquals("Hey!!!", result.m().b); class MS2 extends S { MS2(int x, int y) { @@ -75,6 +74,6 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I2 result2 = MS2::new; - assertEquals(result2.m(8, 4).b, "Hey8!!!4"); + assertEquals("Hey8!!!4", result2.m(8, 4).b); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java index 02fca01ee4a..10aeed014df 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase1 + * @run junit MethodReferenceTestSueCase1 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase1 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase1 { String m() { return var.get(new MethodReferenceTestSueCase1(), ""); } + @Test public void testSueCase1() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java index 4d558484337..f69fb1e4778 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase2 + * @run junit MethodReferenceTestSueCase2 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase2 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase2 { String m() { return var.get(new MethodReferenceTestSueCase2(), ""); } + @Test public void testSueCase2() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java index 9824163d5a3..abe86b37aad 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase4 + * @run junit MethodReferenceTestSueCase4 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase4 { public interface Sam2 { public String get(T target, String s); } @@ -51,7 +49,8 @@ public class MethodReferenceTestSueCase4 { String instanceMethod(String s) { return "2"; } } + @Test public void testSueCase4() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java index 1c1afcbd70d..29bf0e56469 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuper + * @run junit MethodReferenceTestSuper */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -77,7 +76,6 @@ class SPRB extends SPRA { } -@Test public class MethodReferenceTestSuper extends SPRB { String xsA_M(String s) { @@ -93,26 +91,27 @@ public class MethodReferenceTestSuper extends SPRB { return "_BMxsM:" + s; } + @Test public void testMethodReferenceSuper() { SPRI q; q = super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = super::xsA_M; - assertEquals(q.m("*"), "A_MxsA:*"); + assertEquals("A_MxsA:*", q.m("*")); q = super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = super::xsABM; - assertEquals(q.m("*"), "ABMxsB:*"); + assertEquals("ABMxsB:*", q.m("*")); q = super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); q = super::xs_BM; - assertEquals(q.m("*"), "_BMxsB:*"); + assertEquals("_BMxsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java index 316f876e5f4..3db1119f8c5 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuperDefault + * @run junit MethodReferenceTestSuperDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -61,20 +60,20 @@ interface DSPRB extends DSPRA { } -@Test public class MethodReferenceTestSuperDefault implements DSPRB { + @Test public void testMethodReferenceSuper() { DSPRI q; q = DSPRB.super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = DSPRB.super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = DSPRB.super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java index 46962975582..f0f66756a3f 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestTypeConversion + * @run junit MethodReferenceTestTypeConversion */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -40,21 +39,22 @@ class MethodReferenceTestTypeConversion_E { T xI(T t) { return t; } } -@Test public class MethodReferenceTestTypeConversion { interface ISi { int m(Short a); } interface ICc { char m(Character a); } + @Test public void testUnboxObjectToNumberWiden() { ISi q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m((short)77), (short)77); + assertEquals((short)77, q.m((short)77)); } + @Test public void testUnboxObjectToChar() { ICc q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m('@'), '@'); + assertEquals('@', q.m('@')); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java index c430af2ef24..ef0af7ac1af 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgs + * @run junit MethodReferenceTestVarArgs */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestVarArgs { interface SII { @@ -127,75 +126,81 @@ public class MethodReferenceTestVarArgs { return sb.toString(); } + @Test public void testVarArgsSuperclass() { SII q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsArray() { Sai q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsII() { SII q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsiii() { Siii q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsi() { Si q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsaO() { SaO q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java index 989ed97aa6a..1a84660f592 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsExt + * @run junit MethodReferenceTestVarArgsExt */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,80 +49,85 @@ interface NXai { String m(int[] a); } interface NXvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsExt { // These should be processed as var args + @Test public void testVarArgsNXSuperclass() { NXII q; q = (new Ext())::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNXArray() { NXai q; q = (new Ext())::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNXII() { NXII q; q = (new Ext())::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = (new Ext())::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = (new Ext())::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = (new Ext())::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNXiii() { NXiii q; q = (new Ext())::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = (new Ext())::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = (new Ext())::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = (new Ext())::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNXi() { NXi q; q = (new Ext())::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = (new Ext())::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = (new Ext())::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = (new Ext())::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNXaO() { NXaO q; q = (new Ext())::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java index 575cb2b2a43..7eebb7c0772 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuper + * @run junit MethodReferenceTestVarArgsSuper */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ class MethodReferenceTestVarArgsSuper_Sub { } } -@Test public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsSuper_Sub { interface SPRII { String m(Integer a, Integer b); } @@ -132,74 +131,80 @@ public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsS // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { SPRII q; q = super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { SPRai q; q = super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { SPRII q; q = super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { SPRiii q; q = super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { SPRi q; q = super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { SPRaO q; q = super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java index 81ea6cbee66..e3b8ce395cb 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuperDefault + * @run junit MethodReferenceTestVarArgsSuperDefault */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ interface MethodReferenceTestVarArgsSuperDefault_I { } } -@Test public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTestVarArgsSuperDefault_I { interface DSPRII { String m(Integer a, Integer b); } @@ -112,75 +111,81 @@ public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTe // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { DSPRai q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { DSPRiii q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { DSPRi q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { DSPRaO q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java index 5066758ee10..6369cf387b6 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsThis + * @run junit MethodReferenceTestVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,7 +49,6 @@ interface Nsai { String m(int[] a); } interface Nsvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsThis { // These should be processed as var args @@ -109,75 +108,81 @@ public class MethodReferenceTestVarArgsThis { return sb.toString(); } + @Test public void testVarArgsNsSuperclass() { NsII q; q = this::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNsArray() { Nsai q; q = this::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNsII() { NsII q; q = this::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = this::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = this::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = this::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNsiii() { Nsiii q; q = this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = this::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = this::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNsi() { Nsi q; q = this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { NsaO q; q = this::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java index aaf9df950d4..fa835995e0a 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -24,16 +24,15 @@ /** * @test * @summary test for VarHandle signature polymorphic methods - * @run testng MethodReferenceTestVarHandle + * @run junit MethodReferenceTestVarHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestVarHandle { interface Setter { @@ -44,6 +43,7 @@ public class MethodReferenceTestVarHandle { int apply(int[] arr, int idx); } + @Test public void testSet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); @@ -54,6 +54,7 @@ public class MethodReferenceTestVarHandle { assertEquals(42, data[0]); } + @Test public void testGet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); diff --git a/test/langtools/tools/javac/lambdaShapes/TEST.properties b/test/langtools/tools/javac/lambdaShapes/TEST.properties index 8a7db8dbee7..07bc537f116 100644 --- a/test/langtools/tools/javac/lambdaShapes/TEST.properties +++ b/test/langtools/tools/javac/lambdaShapes/TEST.properties @@ -1,4 +1,4 @@ -TestNG.dirs = tools/javac/lambdaShapes +JUnit.dirs = tools/javac/lambdaShapes modules = \ jdk.compiler/com.sun.tools.javac.util diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java index 813c588349f..2ecf714092f 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -42,12 +42,15 @@ import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.testng.annotations.AfterSuite; -import org.testng.annotations.Test; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.DataProvider; -import static org.testng.Assert.*; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; +@ParameterizedClass +@MethodSource("caseGenerator") public class FDTest { public enum TestKind { @@ -63,7 +66,7 @@ public class FDTest { public static JavaCompiler comp; public static StandardJavaFileManager fm; - @BeforeSuite + @BeforeAll static void init() { // create default shared JavaCompiler - reused across multiple // compilations @@ -72,7 +75,7 @@ public class FDTest { fm = comp.getStandardFileManager(null, null, null); } - @AfterSuite + @AfterAll static void teardown() throws IOException { fm.close(); } @@ -87,14 +90,13 @@ public class FDTest { teardown(); } - @Test(dataProvider = "fdCases") - public void testOneCase(TestKind tk, Hierarchy hs) + @Test + public void testOneCase() throws Exception { FDTest.runTest(tk, hs, comp, fm); } - @DataProvider(name = "fdCases") - public Object[][] caseGenerator() { + public static Object[][] caseGenerator() { List> cases = generateCases(); Object[][] fdCases = new Object[cases.size()][]; for (int i = 0; i < cases.size(); ++i) { @@ -127,8 +129,6 @@ public class FDTest { DefenderTestSource source; DiagnosticChecker diagChecker; - public FDTest() {} - FDTest(TestKind tk, Hierarchy hs) { this.tk = tk; this.hs = hs; diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java index a1a37559e6d..dcfff28949e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -23,17 +23,16 @@ package org.openjdk.tests.separate; -import org.testng.ITestResult; -import org.testng.annotations.AfterMethod; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import org.junit.jupiter.api.AfterEach; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TestHarness { @@ -69,7 +68,7 @@ public class TestHarness { verboseLocal.set(Boolean.TRUE); } - @AfterMethod + @AfterEach public void reset() { if (!this.verbose) { verboseLocal.set(Boolean.FALSE); @@ -87,16 +86,6 @@ public class TestHarness { return flags.toArray(new Compiler.Flags[0]); } - @AfterMethod - public void printError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - String clsName = result.getTestClass().getName(); - clsName = clsName.substring(clsName.lastIndexOf(".") + 1); - System.out.println("Test " + clsName + "." + - result.getName() + " FAILED"); - } - } - private static final ConcreteMethod stdCM = ConcreteMethod.std("-1"); private static final AbstractMethod stdAM = new AbstractMethod("int", stdMethodName); @@ -193,7 +182,7 @@ public class TestHarness { Object res = m.invoke(null); assertNotNull(res); if (value != null) { - assertEquals(res, value); + assertEquals(value, res); } } catch (InvocationTargetException | IllegalAccessException e) { fail("Unexpected exception thrown: " + e.getCause(), e.getCause()); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java index 93629947f3a..b876a2538e0 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,7 +25,6 @@ package org.openjdk.tests.vm; import org.openjdk.tests.separate.Compiler; import org.openjdk.tests.separate.TestHarness; -import org.testng.annotations.Test; import static org.openjdk.tests.separate.SourceModel.AbstractMethod; import static org.openjdk.tests.separate.SourceModel.AccessFlag; @@ -36,11 +35,11 @@ import static org.openjdk.tests.separate.SourceModel.Extends; import static org.openjdk.tests.separate.SourceModel.Interface; import static org.openjdk.tests.separate.SourceModel.MethodParameter; import static org.openjdk.tests.separate.SourceModel.TypeParameter; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; -@Test(groups = "vm") public class DefaultMethodsTest extends TestHarness { public DefaultMethodsTest() { super(false, false); @@ -51,6 +50,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 22 */ + @Test public void testHarnessInvokeVirtual() { Class C = new Class("C", ConcreteMethod.std("22")); assertInvokeVirtualEquals(22, C); @@ -62,6 +62,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m() == 33; */ + @Test public void testHarnessInvokeInterface() { Interface I = new Interface("I", AbstractMethod.std()); Class C = new Class("C", I, ConcreteMethod.std("33")); @@ -73,6 +74,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws NoSuchMethod */ + @Test public void testHarnessThrows() { Class C = new Class("C"); assertThrows(NoSuchMethodError.class, C); @@ -85,6 +87,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testBasicDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I); @@ -102,6 +105,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testFarDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Interface J = new Interface("J", I); @@ -121,6 +125,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: K k = new C(); k.m() == 44; */ + @Test public void testOverrideAbstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", I, DefaultMethod.std("44")); @@ -138,6 +143,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 55; * TEST: I i = new C(); i.m() == 55; */ + @Test public void testExisting() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I, ConcreteMethod.std("55")); @@ -154,6 +160,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -171,6 +178,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 11; * TEST: I i = new D(); i.m() == 11; */ + @Test public void testExistingInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", ConcreteMethod.std("11")); @@ -188,6 +196,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 22; * TEST: I i = new D(); i.m() == 22; */ + @Test public void testExistingInheritedOverride() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", I, ConcreteMethod.std("11")); @@ -207,6 +216,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: E e = new E(); e.m() == 22; * TEST: J j = new E(); j.m() == 22; */ + @Test public void testExistingInheritedPlusDefault() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -226,6 +236,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 77; * TEST: I i = new C(); i.m() == 77; */ + @Test public void testInheritedWithConcrete() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -243,6 +254,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 66; * TEST: I i = new C(); i.m() == 66; */ + @Test public void testInheritedWithConcreteAndImpl() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -259,6 +271,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws ICCE */ + @Test public void testConflict() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -274,6 +287,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 88 */ + @Test public void testAmbiguousReabstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -293,6 +307,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: K k = new C(); k.m() == 99 * TEST: I i = new C(); i.m() == 99 */ + @Test public void testDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -320,6 +335,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: L l = new C(); l.m() == 99 * TEST: M m = new C(); m.m() == 99 */ + @Test public void testExpandedDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -343,6 +359,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws AME */ + @Test public void testReabstract() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, AbstractMethod.std()); @@ -360,6 +377,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testShadow() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -379,6 +397,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testDisqualified() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -396,6 +415,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m("string") == 88; * TEST: I i = new C(); i.m("string") == 88; */ + @Test public void testSelfFill() { // This test ensures that a concrete method overrides a default method // that matches at the language-level, but has a different method @@ -426,6 +446,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C.class.getMethod("m").invoke(new C()) == 99 */ + @Test public void testReflectCall() { Interface I = new Interface("I", DefaultMethod.std("99")); //workaround accessibility issue when loading C with DirectedClassLoader @@ -468,7 +489,7 @@ public class DefaultMethodsTest extends TestHarness { } assertNotNull(res); - assertEquals(res.intValue(), 99); + assertEquals(99, res.intValue()); compiler.cleanup(); } @@ -485,6 +506,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m("A","B","C") == 88; * TEST: K k = new C(); k.m("A","B","C") == 88; */ + @Test public void testBridges() { DefaultMethod dm = new DefaultMethod("int", stdMethodName, "return 99;", new MethodParameter("T", "t"), new MethodParameter("V", "v"), @@ -538,6 +560,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 88; * TEST: I i = new C(); i.m() == 88; */ + @Test public void testSuperBasic() { Interface J = new Interface("J", DefaultMethod.std("88")); Interface I = new Interface("I", J, new DefaultMethod( @@ -559,6 +582,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws ICCE * TODO: add case for K k = new C(); k.m() throws ICCE */ + @Test public void testSuperConflict() { Interface K = new Interface("K", DefaultMethod.std("99")); Interface L = new Interface("L", DefaultMethod.std("101")); @@ -581,6 +605,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99 * TODO: add case for J j = new C(); j.m() == ??? */ + @Test public void testSuperDisqual() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, DefaultMethod.std("55")); @@ -600,6 +625,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws AME * TODO: add case for I i = new C(); i.m() throws AME */ + @Test public void testSuperNull() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, new DefaultMethod( @@ -621,6 +647,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m("") == 88; */ + @Test public void testSuperGeneric() { Interface J = new Interface("J", new TypeParameter("T"), new DefaultMethod("int", stdMethodName, "return 88;", @@ -646,6 +673,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m("string") == 44 */ + @Test public void testSuperGenericDisqual() { MethodParameter t = new MethodParameter("T", "t"); MethodParameter s = new MethodParameter("String", "s"); @@ -672,6 +700,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(99) */ + @Test public void testCovarBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -700,6 +729,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(88) */ + @Test public void testNoCovarNoBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -732,6 +762,7 @@ public class DefaultMethodsTest extends TestHarness { * It verifies that default method analysis occurs when mirandas have been * inherited and the supertypes don't have any overpass methods. */ + @Test public void testNoNewMiranda() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, DefaultMethod.std("99")); @@ -755,6 +786,7 @@ public class DefaultMethodsTest extends TestHarness { * Test that a erased-signature-matching method does not implement * non-language-level matching methods */ + @Test public void testNonConcreteFill() { AbstractMethod ipm = new AbstractMethod("int", "m", new MethodParameter("T", "t"), @@ -809,6 +841,7 @@ public class DefaultMethodsTest extends TestHarness { assertInvokeInterfaceEquals(99, C, I.with("String", "String", "String"), ipm, a, a, a); } + @Test public void testStrictfpDefault() { try { java.lang.Class.forName("org.openjdk.tests.vm.StrictfpDefault"); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java index 4fef2df615d..904883a0c3e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -28,11 +28,8 @@ package org.openjdk.tests.vm; import java.util.*; -import org.testng.ITestResult; -import org.testng.annotations.Test; -import org.testng.annotations.DataProvider; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.AfterSuite; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.openjdk.tests.separate.*; import org.openjdk.tests.separate.Compiler; @@ -41,7 +38,10 @@ import org.openjdk.tests.shapegen.Hierarchy; import org.openjdk.tests.shapegen.HierarchyGenerator; import org.openjdk.tests.shapegen.ClassCase; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.openjdk.tests.separate.SourceModel.*; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.Method; @@ -55,7 +55,6 @@ public class FDSeparateCompilationTest extends TestHarness { super(false, true); } - @DataProvider(name = "allShapes", parallel = true) public Object[][] hierarchyGenerator() { ArrayList allCases = new ArrayList<>(); @@ -92,7 +91,9 @@ public class FDSeparateCompilationTest extends TestHarness { private static final ConcreteMethod canonicalMethod = new ConcreteMethod( "String", "m", "returns " + EMPTY + ";", AccessFlag.PUBLIC); - @Test(enabled = false, groups = "vm", dataProvider = "allShapes") + @Disabled + @ParameterizedTest + @MethodSource("hierarchyGenerator") public void separateCompilationTest(Hierarchy hs) { ClassCase cc = hs.root; Type type = sourceTypeFrom(hs.root); @@ -118,17 +119,8 @@ public class FDSeparateCompilationTest extends TestHarness { } } - @AfterMethod - public void printCaseError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - Hierarchy hs = (Hierarchy)result.getParameters()[0]; - System.out.println("Separate compilation case " + hs); - printCaseDetails(hs); - } - } - - @AfterSuite - public void cleanupCompilerCache() { + @AfterAll + public static void cleanupCompilerCache() { Compiler.purgeCache(); } diff --git a/test/langtools/tools/javac/lvti/BadLocalVarInferenceTest.out b/test/langtools/tools/javac/lvti/BadLocalVarInferenceTest.out index 9e7cab9633d..d9e7ce5a552 100644 --- a/test/langtools/tools/javac/lvti/BadLocalVarInferenceTest.out +++ b/test/langtools/tools/javac/lvti/BadLocalVarInferenceTest.out @@ -5,7 +5,7 @@ BadLocalVarInferenceTest.java:22:13: compiler.err.cant.infer.local.var.type: g, BadLocalVarInferenceTest.java:23:13: compiler.err.cant.infer.local.var.type: d, (compiler.misc.local.self.ref) BadLocalVarInferenceTest.java:24:13: compiler.err.cant.infer.local.var.type: k, (compiler.misc.local.array.missing.target) BadLocalVarInferenceTest.java:25:29: compiler.err.does.not.override.abstract: compiler.misc.anonymous.class: BadLocalVarInferenceTest$1, m(java.lang.Object), BadLocalVarInferenceTest.Foo -BadLocalVarInferenceTest.java:26:13: compiler.err.method.does.not.override.superclass +BadLocalVarInferenceTest.java:26:13: compiler.err.method.does.not.override.superclass: m(java.lang.String), compiler.misc.anonymous.class: BadLocalVarInferenceTest$1 BadLocalVarInferenceTest.java:29:27: compiler.err.cant.resolve.location.args: kindname.method, charAt, , int, (compiler.misc.location.1: kindname.variable, x, java.lang.Object) BadLocalVarInferenceTest.java:30:13: compiler.err.cant.infer.local.var.type: t, (compiler.misc.local.cant.infer.void) 10 errors diff --git a/test/langtools/tools/javac/multicatch/Neg01.java b/test/langtools/tools/javac/multicatch/Neg01.java index 378e4f285cb..86a2ae952a1 100644 --- a/test/langtools/tools/javac/multicatch/Neg01.java +++ b/test/langtools/tools/javac/multicatch/Neg01.java @@ -3,7 +3,6 @@ * @bug 6943289 * * @summary Project Coin: Improved Exception Handling for Java (aka 'multicatch') - * @author darcy * @compile/fail/ref=Neg01.out -XDrawDiagnostics Neg01.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg01.out b/test/langtools/tools/javac/multicatch/Neg01.out index 56eb1958a52..f0ddda9bce6 100644 --- a/test/langtools/tools/javac/multicatch/Neg01.out +++ b/test/langtools/tools/javac/multicatch/Neg01.out @@ -1,2 +1,2 @@ -Neg01.java:22:19: compiler.err.except.never.thrown.in.try: Neg01.B2 +Neg01.java:21:19: compiler.err.except.never.thrown.in.try: Neg01.B2 1 error diff --git a/test/langtools/tools/javac/multicatch/Neg01eff_final.java b/test/langtools/tools/javac/multicatch/Neg01eff_final.java index e2c78f5e055..933d7d1599d 100644 --- a/test/langtools/tools/javac/multicatch/Neg01eff_final.java +++ b/test/langtools/tools/javac/multicatch/Neg01eff_final.java @@ -3,7 +3,6 @@ * @bug 6943289 * * @summary Project Coin: Improved Exception Handling for Java (aka 'multicatch') - * @author darcy * @compile/fail/ref=Neg01eff_final.out -XDrawDiagnostics Neg01eff_final.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg01eff_final.out b/test/langtools/tools/javac/multicatch/Neg01eff_final.out index 4327ef45b9b..b110d4468a1 100644 --- a/test/langtools/tools/javac/multicatch/Neg01eff_final.out +++ b/test/langtools/tools/javac/multicatch/Neg01eff_final.out @@ -1,2 +1,2 @@ -Neg01eff_final.java:22:19: compiler.err.except.never.thrown.in.try: Neg01eff_final.B2 +Neg01eff_final.java:21:19: compiler.err.except.never.thrown.in.try: Neg01eff_final.B2 1 error diff --git a/test/langtools/tools/javac/multicatch/Neg07.java b/test/langtools/tools/javac/multicatch/Neg07.java index e5869e6a2cc..a2c1704c6b2 100644 --- a/test/langtools/tools/javac/multicatch/Neg07.java +++ b/test/langtools/tools/javac/multicatch/Neg07.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 7039822 * @summary Verify typing of lub of exception parameter w.r.t getClass - * @author Joseph D. Darcy * @compile/fail/ref=Neg07.out -XDrawDiagnostics Neg07.java */ diff --git a/test/langtools/tools/javac/multicatch/Neg07.out b/test/langtools/tools/javac/multicatch/Neg07.out index 32030e17695..0acdba265da 100644 --- a/test/langtools/tools/javac/multicatch/Neg07.out +++ b/test/langtools/tools/javac/multicatch/Neg07.out @@ -1,2 +1,2 @@ -Neg07.java:14:56: compiler.err.prob.found.req: (compiler.misc.inconvertible.types: java.lang.Class, java.lang.Class) +Neg07.java:13:56: compiler.err.prob.found.req: (compiler.misc.inconvertible.types: java.lang.Class, java.lang.Class) 1 error diff --git a/test/langtools/tools/javac/multicatch/Pos10.java b/test/langtools/tools/javac/multicatch/Pos10.java index c66fa5334cd..d108c8f9671 100644 --- a/test/langtools/tools/javac/multicatch/Pos10.java +++ b/test/langtools/tools/javac/multicatch/Pos10.java @@ -25,7 +25,6 @@ * @test * @bug 7039822 * @summary Verify lub of an exception parameter can be an intersection type - * @author Joseph D. Darcy */ public class Pos10 { diff --git a/test/langtools/tools/javac/platform/CompilationTest.java b/test/langtools/tools/javac/platform/CompilationTest.java new file mode 100644 index 00000000000..93fb2fb2678 --- /dev/null +++ b/test/langtools/tools/javac/platform/CompilationTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, 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. + */ + +/** + * @test + * @bug 8365060 + * @summary Verify javac can compile given snippets with --release + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.platform + * jdk.compiler/com.sun.tools.javac.util:+open + * @build toolbox.ToolBox CompilationTest + * @run main CompilationTest + */ + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.ToolBox; + +public class CompilationTest { + + private final ToolBox tb = new ToolBox(); + public static void main(String... args) throws IOException, URISyntaxException { + CompilationTest t = new CompilationTest(); + + t.testJdkNet(); + } + + void testJdkNet() throws IOException { + Path root = Paths.get("."); + Path classes = root.resolve("classes"); + Files.createDirectories(classes); + + new JavacTask(tb) + .outdir(classes) + .options("--release", "8", + "-XDrawDiagnostics") + .sources(""" + import jdk.net.ExtendedSocketOptions; + public class Test { + } + """) + .run() + .writeAll() + .getOutputLines(Task.OutputKind.DIRECT); + } + +} diff --git a/test/langtools/tools/javac/processing/6365040/T6365040.java b/test/langtools/tools/javac/processing/6365040/T6365040.java index 4413ff1375c..c538609800d 100644 --- a/test/langtools/tools/javac/processing/6365040/T6365040.java +++ b/test/langtools/tools/javac/processing/6365040/T6365040.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6365040 6358129 * @summary Test -processor foo,bar,baz - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler @@ -13,7 +12,7 @@ * @compile -processor ProcFoo,ProcBar,T6365040 -proc:only T6365040.java * @compile -processor T6365040 -proc:only T6365040.java * @compile -processor T6365040,NotThere, -proc:only T6365040.java - * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere -proc:only T6365040.java + * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere -proc:only T6365040.java * @compile/fail/ref=T6365040.out -XDrawDiagnostics -processor NotThere,T6365040 -proc:only T6365040.java */ @@ -23,7 +22,6 @@ import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.element.TypeElement; - public class T6365040 extends JavacTestingAbstractProcessor { public boolean process(Set annotations, RoundEnvironment roundEnvironment) { diff --git a/test/langtools/tools/javac/processing/6378728/T6378728.java b/test/langtools/tools/javac/processing/6378728/T6378728.java index 9ee62051027..791296fb754 100644 --- a/test/langtools/tools/javac/processing/6378728/T6378728.java +++ b/test/langtools/tools/javac/processing/6378728/T6378728.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -24,7 +24,6 @@ /* @test * @bug 6378728 * @summary Verify -proc:only doesn't produce class files - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler * @compile T6378728.java diff --git a/test/langtools/tools/javac/processing/6634138/T6634138.java b/test/langtools/tools/javac/processing/6634138/T6634138.java index 872192d51ec..baf9bff3b63 100644 --- a/test/langtools/tools/javac/processing/6634138/T6634138.java +++ b/test/langtools/tools/javac/processing/6634138/T6634138.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -24,7 +24,6 @@ /* * @test * @bug 6634138 - * @author Joseph D. Darcy * @summary Verify source files output after processing is over are compiled * @library /tools/javac/lib * @modules java.compiler diff --git a/test/langtools/tools/javac/processing/completion/TestCompletions.java b/test/langtools/tools/javac/processing/completion/TestCompletions.java index 87d947f173e..c181b0fc84d 100644 --- a/test/langtools/tools/javac/processing/completion/TestCompletions.java +++ b/test/langtools/tools/javac/processing/completion/TestCompletions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6341177 * @summary Some simple tests of the methods in Completions - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/environment/TestSourceVersion.java b/test/langtools/tools/javac/processing/environment/TestSourceVersion.java index e31664b8a93..473b14ad39c 100644 --- a/test/langtools/tools/javac/processing/environment/TestSourceVersion.java +++ b/test/langtools/tools/javac/processing/environment/TestSourceVersion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6402506 8028545 8028543 * @summary Test that getSourceVersion works properly - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java b/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java index 33b19543ef7..ddcaaf7b60d 100644 --- a/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java +++ b/test/langtools/tools/javac/processing/environment/round/TestElementsAnnotatedWith.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6397298 6400986 6425592 6449798 6453386 6508401 6498938 6911854 8030049 8038080 8032230 8190886 * @summary Tests that getElementsAnnotatedWith[Any] methods work properly. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java b/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java index 300afaad524..3a7ee877c1f 100644 --- a/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java +++ b/test/langtools/tools/javac/processing/errors/TestFatalityOfParseErrors.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6403459 * @summary Test that generating programs with syntax errors is a fatal condition - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java b/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java index 8c397b20f62..bd1dea81a40 100644 --- a/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java +++ b/test/langtools/tools/javac/processing/errors/TestOptionSyntaxErrors.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6406212 * @summary Test that annotation processor options with illegal syntax are rejected - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler/com.sun.tools.javac.main * @build JavacTestingAbstractProcessor CompileFail diff --git a/test/langtools/tools/javac/processing/errors/TestReturnCode.java b/test/langtools/tools/javac/processing/errors/TestReturnCode.java index 749044edd6f..5ca6a50e3c6 100644 --- a/test/langtools/tools/javac/processing/errors/TestReturnCode.java +++ b/test/langtools/tools/javac/processing/errors/TestReturnCode.java @@ -25,7 +25,6 @@ * @test * @bug 6403468 * @summary Test that an erroneous return code results from raising an error. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler/com.sun.tools.javac.main * @build JavacTestingAbstractProcessor CompileFail diff --git a/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java b/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java index e522afa9ce8..6eac5475c0b 100644 --- a/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java +++ b/test/langtools/tools/javac/processing/filer/TestFilerConstraints.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6380018 6453386 6457283 * @summary Test that the constraints guaranteed by the Filer and maintained - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/filer/TestGetResource.java b/test/langtools/tools/javac/processing/filer/TestGetResource.java index 073757d1217..cf8ac2aafeb 100644 --- a/test/langtools/tools/javac/processing/filer/TestGetResource.java +++ b/test/langtools/tools/javac/processing/filer/TestGetResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6380018 6449798 * @summary Test Filer.getResource - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/filer/TestPackageInfo.java b/test/langtools/tools/javac/processing/filer/TestPackageInfo.java index 1e0659c9b12..1b11c75acde 100644 --- a/test/langtools/tools/javac/processing/filer/TestPackageInfo.java +++ b/test/langtools/tools/javac/processing/filer/TestPackageInfo.java @@ -25,7 +25,6 @@ * @test * @bug 6380018 6392177 6993311 8193462 * @summary Test the ability to create and process package-info.java files - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/messager/MessagerBasics.java b/test/langtools/tools/javac/processing/messager/MessagerBasics.java index 35c3b64ca2d..896f7b579b5 100644 --- a/test/langtools/tools/javac/processing/messager/MessagerBasics.java +++ b/test/langtools/tools/javac/processing/messager/MessagerBasics.java @@ -2,7 +2,6 @@ * @test /nodynamiccopyright/ * @bug 6341173 6341072 * @summary Test presence of Messager methods - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/TestExceptions.java b/test/langtools/tools/javac/processing/model/TestExceptions.java index 993e084e157..e9f5846a7ca 100644 --- a/test/langtools/tools/javac/processing/model/TestExceptions.java +++ b/test/langtools/tools/javac/processing/model/TestExceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6794071 * @summary Test that exceptions have a proper parent class - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/TestSourceVersion.java b/test/langtools/tools/javac/processing/model/TestSourceVersion.java index 3a56bf76007..d53368d3b6d 100644 --- a/test/langtools/tools/javac/processing/model/TestSourceVersion.java +++ b/test/langtools/tools/javac/processing/model/TestSourceVersion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 7025809 8028543 6415644 8028544 8029942 8187951 8193291 8196551 8233096 8275308 * @summary Test latest, latestSupported, underscore as keyword, etc. - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java b/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java index c7504da4f89..de575841d12 100644 --- a/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java +++ b/test/langtools/tools/javac/processing/model/element/TestAnonClassNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6449781 6930508 * @summary Test that reported names of anonymous classes are non-null. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestAnonSourceNames diff --git a/test/langtools/tools/javac/processing/model/element/TestElement.java b/test/langtools/tools/javac/processing/model/element/TestElement.java index 9c878319110..4bfe6280fd5 100644 --- a/test/langtools/tools/javac/processing/model/element/TestElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6453386 * @summary Test basic properties of javax.lang.element.Element - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java b/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java index 4a37be9d7d7..2296214555f 100644 --- a/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestExecutableElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 8005046 8011052 8025087 * @summary Test basic properties of javax.lang.element.ExecutableElement - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestNames.java b/test/langtools/tools/javac/processing/model/element/TestNames.java index adbbf87c8e4..7e9ed193afd 100644 --- a/test/langtools/tools/javac/processing/model/element/TestNames.java +++ b/test/langtools/tools/javac/processing/model/element/TestNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6380016 * @summary Test that the constraints guaranteed by the Filer and maintained - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestPackageElement.java b/test/langtools/tools/javac/processing/model/element/TestPackageElement.java index f3749f6c5ff..83d7515677b 100644 --- a/test/langtools/tools/javac/processing/model/element/TestPackageElement.java +++ b/test/langtools/tools/javac/processing/model/element/TestPackageElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6449798 6399404 8173776 8163989 * @summary Test basic workings of PackageElement - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java b/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java index a0f133779ab..9a51613358a 100644 --- a/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java +++ b/test/langtools/tools/javac/processing/model/element/TestResourceVariable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6911256 6964740 6967842 6961571 7025809 * @summary Test that the resource variable kind is appropriately set - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestResourceVariable diff --git a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java index 1e252c01cc0..849f8df7b71 100644 --- a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java +++ b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/NpeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6593082 * @summary MirroredTypeException constructor should not accept null - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java index 38c2db7ca13..2487ac77910 100644 --- a/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java +++ b/test/langtools/tools/javac/processing/model/type/MirroredTypeEx/Plurality.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -31,7 +31,6 @@ * @build JavacTestingAbstractProcessor * @compile Plurality.java * @compile -processor Plurality -proc:only Plurality.java - * @author Joseph D. Darcy */ import java.lang.annotation.*; import java.math.BigDecimal; diff --git a/test/langtools/tools/javac/processing/model/type/TestTypeKind.java b/test/langtools/tools/javac/processing/model/type/TestTypeKind.java index fc1ed62e2a5..9bc2e7e7cb6 100644 --- a/test/langtools/tools/javac/processing/model/type/TestTypeKind.java +++ b/test/langtools/tools/javac/processing/model/type/TestTypeKind.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6347716 * @summary Test TypeKind.isPrimitive - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler */ diff --git a/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java b/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java index d90b1ad34bf..fe8e5262be4 100644 --- a/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java +++ b/test/langtools/tools/javac/processing/model/util/deprecation/TestDeprecation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6392818 * @summary Tests Elements.isDeprecated(Element) - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java b/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java index a9827b2aacc..b0b15cf9226 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestGetConstantExpression.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6471577 6517779 * @summary Test Elements.getConstantExpression - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java b/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java index d82addd8b1f..96c84db5bb7 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestGetPackageOf.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6453386 8216404 8230337 * @summary Test Elements.getPackageOf - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java b/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java index f8292afd815..ae048af45cc 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java +++ b/test/langtools/tools/javac/processing/model/util/elements/TestIsFunctionalInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 8007574 * @summary Test Elements.isFunctionalInterface - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java b/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java index 2d16a878625..6269d5c7334 100644 --- a/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java +++ b/test/langtools/tools/javac/processing/model/util/elements/VacuousEnum.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6937417 * @summary Test -Xprint on enum type with no constants - * @author Joseph D. Darcy * @compile -Xprint VacuousEnum.java */ public enum VacuousEnum { diff --git a/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java b/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java index a9bf8ecf8e6..6e9539b7a9e 100644 --- a/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java +++ b/test/langtools/tools/javac/processing/model/util/filter/TestIterables.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 6406164 * @summary Test that ElementFilter iterable methods behave properly. - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules java.compiler * jdk.compiler diff --git a/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java b/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java index 5359d26e53f..b0898a30ca9 100644 --- a/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java +++ b/test/langtools/tools/javac/processing/model/util/types/TestPseudoTypeHandling.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, 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 @@ -25,7 +25,6 @@ * @test * @bug 8175335 * @summary Test Types methods on module and package TypeMirrors - * @author Joseph D. Darcy * @library /tools/javac/lib * @modules jdk.compiler * @build JavacTestingAbstractProcessor TestPseudoTypeHandling diff --git a/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java b/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java index 54fe613acf1..24abf89ee02 100644 --- a/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java +++ b/test/langtools/tools/javac/processing/warnings/TestSourceVersionWarnings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, 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 @@ -21,11 +21,11 @@ * questions. */ + /* * @test * @bug 6376083 6376084 6458819 7025784 7025786 7025789 * @summary Test that warnings about source versions are output as expected. - * @author Joseph D. Darcy * @modules java.compiler * jdk.compiler * @compile TestSourceVersionWarnings.java diff --git a/test/langtools/tools/javac/records/BigRecordsToStringTest.java b/test/langtools/tools/javac/records/BigRecordsToStringTest.java index 87e269a9532..1e42cf03ebc 100644 --- a/test/langtools/tools/javac/records/BigRecordsToStringTest.java +++ b/test/langtools/tools/javac/records/BigRecordsToStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, 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 @@ -25,7 +25,7 @@ * @test * @bug 8261847 * @summary test the output of the toString method of records with a large number of components - * @run testng BigRecordsToStringTest + * @run junit BigRecordsToStringTest */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class BigRecordsToStringTest { record BigInt( int i1,int i2,int i3,int i4,int i5,int i6,int i7,int i8,int i9,int i10, @@ -164,6 +163,7 @@ public class BigRecordsToStringTest { "i111=111, i112=112, i113=113, i114=114, i115=115, i116=116, i117=117, i118=118, i119=119, i120=120, i121=121, i122=122, i123=123, " + "i124=124, i125=125, i126=126, i127=127]"; + @Test public void testToStringOutput() { assertTrue(bigInt.toString().equals(BIG_INT_TO_STRING_OUTPUT)); assertTrue(bigLong.toString().equals(BIG_LONG_TO_STRING_OUTPUT)); diff --git a/test/langtools/tools/javac/records/RecordMemberTests.java b/test/langtools/tools/javac/records/RecordMemberTests.java index 7d358c21953..f7291e70cb0 100644 --- a/test/langtools/tools/javac/records/RecordMemberTests.java +++ b/test/langtools/tools/javac/records/RecordMemberTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -25,7 +25,7 @@ * @test * @bug 8246774 * @summary test several assertions on record classes members - * @run testng RecordMemberTests + * @run junit RecordMemberTests */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class RecordMemberTests { public record R1(int i, int j) {} @@ -63,43 +62,47 @@ public class RecordMemberTests { R3 r3 = new R3(1, 2); R4 r4 = new R4(1, 2); + @Test public void testConstruction() { for (int i : new int[] { r1.i, r2.i, r3.i, r1.i(), r2.i(), r3.i() }) - assertEquals(i, 1); + assertEquals(1, i); for (int j : new int[] { r1.j, r2.j, r3.j, r1.j(), r2.j(), r3.j() }) - assertEquals(j, 2); + assertEquals(2, j); - assertEquals(r4.i, 0); - assertEquals(r4.j, 0); + assertEquals(0, r4.i); + assertEquals(0, r4.j); } + @Test public void testConstructorParameterNames() throws ReflectiveOperationException { for (Class cl : List.of(R1.class, R2.class, R3.class, R4.class)) { Constructor c = cl.getConstructor(int.class, int.class); assertNotNull(c); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 2); - assertEquals(parameters[0].getName(), "i"); - assertEquals(parameters[1].getName(), "j"); + assertEquals(2, parameters.length); + assertEquals("i", parameters[0].getName()); + assertEquals("j", parameters[1].getName()); } } + @Test public void testSuperclass() { - assertEquals(R1.class.getSuperclass(), Record.class); + assertEquals(Record.class, R1.class.getSuperclass()); // class is final assertTrue((R1.class.getModifiers() & Modifier.FINAL) != 0); } + @Test public void testMandatedMembersPresent() throws ReflectiveOperationException { // fields are present, of the right type, final and private - assertEquals(R1.class.getDeclaredFields().length, 2); + assertEquals(2, R1.class.getDeclaredFields().length); for (String s : List.of("i", "j")) { Field iField = R1.class.getDeclaredField(s); - assertEquals(iField.getType(), int.class); - assertEquals((iField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int.class, iField.getType()); + assertEquals(0, (iField.getModifiers() & Modifier.STATIC)); assertTrue((iField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((iField.getModifiers() & Modifier.FINAL) != 0); } @@ -107,15 +110,15 @@ public class RecordMemberTests { // methods are present, of the right descriptor, and public/instance/concrete for (String s : List.of("i", "j")) { Method iMethod = R1.class.getDeclaredMethod(s); - assertEquals(iMethod.getReturnType(), int.class); - assertEquals(iMethod.getParameterCount(), 0); - assertEquals((iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); + assertEquals(int.class, iMethod.getReturnType()); + assertEquals(0, iMethod.getParameterCount()); + assertEquals(0, (iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); } Constructor c = R1.class.getConstructor(int.class, int.class); R1 r1 = (R1) c.newInstance(1, 2); - assertEquals(r1.i(), 1); - assertEquals(r1.j(), 2); + assertEquals(1, r1.i()); + assertEquals(2, r1.j()); } record OrdinaryMembers(int x) { @@ -124,21 +127,23 @@ public class RecordMemberTests { public String sf () { return "instance"; } } + @Test public void testOrdinaryMembers() { OrdinaryMembers.ss = "foo"; - assertEquals(OrdinaryMembers.ssf(), "foo"); + assertEquals("foo", OrdinaryMembers.ssf()); OrdinaryMembers o = new OrdinaryMembers(3); - assertEquals(o.sf(), "instance"); + assertEquals("instance", o.sf()); } class LocalRecordHelper { Class m(int x) { record R (int x) { } - assertEquals(new R(x).x(), x); + assertEquals(x, new R(x).x()); return R.class; } } + @Test public void testLocalRecordsStatic() { Class c = new LocalRecordHelper().m(3); String message = c.toGenericString(); @@ -181,6 +186,7 @@ public class RecordMemberTests { } } + @Test public void testNestedRecordsStatic() { NestedRecordHelper n = new NestedRecordHelper(); for (Class c : List.of(NestedRecordHelper.R1.class, diff --git a/test/langtools/tools/javac/records/VarargsRecordsTest.java b/test/langtools/tools/javac/records/VarargsRecordsTest.java index 14b9a9481d2..60c6e70d3ad 100644 --- a/test/langtools/tools/javac/records/VarargsRecordsTest.java +++ b/test/langtools/tools/javac/records/VarargsRecordsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -27,16 +27,15 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; -import org.testng.annotations.Test; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; /** * @test * @bug 8246774 * @summary test for varargs record components - * @run testng VarargsRecordsTest + * @run junit VarargsRecordsTest */ -@Test public class VarargsRecordsTest { public record RI(int... xs) { } public record RII(int x, int... xs) { } @@ -49,49 +48,52 @@ public class VarargsRecordsTest { RII r5 = new RII(1, 2); RII r6 = new RII(1, 2, 3); + @Test public void assertVarargsInstances() { - assertEquals(r1.xs.length, 0); - assertEquals(r2.xs.length, 1); - assertEquals(r3.xs.length, 2); - assertEquals(r4.xs.length, 0); - assertEquals(r5.xs.length, 1); - assertEquals(r6.xs.length, 2); + assertEquals(0, r1.xs.length); + assertEquals(1, r2.xs.length); + assertEquals(2, r3.xs.length); + assertEquals(0, r4.xs.length); + assertEquals(1, r5.xs.length); + assertEquals(2, r6.xs.length); - assertEquals(r2.xs[0], 1); - assertEquals(r3.xs[0], 1); - assertEquals(r3.xs[1], 2); + assertEquals(1, r2.xs[0]); + assertEquals(1, r3.xs[0]); + assertEquals(2, r3.xs[1]); - assertEquals(r5.xs[0], 2); - assertEquals(r6.xs[0], 2); - assertEquals(r6.xs[1], 3); + assertEquals(2, r5.xs[0]); + assertEquals(2, r6.xs[0]); + assertEquals(3, r6.xs[1]); } + @Test public void testMembers() throws ReflectiveOperationException { Constructor c = RI.class.getConstructor(int[].class); assertNotNull(c); assertTrue(c.isVarArgs()); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 1); - assertEquals(parameters[0].getName(), "xs"); + assertEquals(1, parameters.length); + assertEquals("xs", parameters[0].getName()); RI ri = (RI) c.newInstance(new int[]{1, 2}); - assertEquals(ri.xs()[0], 1); - assertEquals(ri.xs()[1], 2); + assertEquals(1, ri.xs()[0]); + assertEquals(2, ri.xs()[1]); Field xsField = RI.class.getDeclaredField("xs"); - assertEquals(xsField.getType(), int[].class); - assertEquals((xsField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int[].class, xsField.getType()); + assertEquals(0, (xsField.getModifiers() & Modifier.STATIC)); assertTrue((xsField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((xsField.getModifiers() & Modifier.FINAL) != 0); - assertEquals(((int[]) xsField.get(ri))[0], 1); + assertEquals(1, ((int[]) xsField.get(ri))[0]); Method xsMethod = RI.class.getDeclaredMethod("xs"); - assertEquals(xsMethod.getReturnType(), int[].class); - assertEquals(xsMethod.getParameterCount(), 0); - assertEquals((xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); - assertEquals(((int[]) xsMethod.invoke(ri))[0], 1); + assertEquals(int[].class, xsMethod.getReturnType()); + assertEquals(0, xsMethod.getParameterCount()); + assertEquals(0, (xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); + assertEquals(1, ((int[]) xsMethod.invoke(ri))[0]); } + @Test public void testNotVarargs() throws ReflectiveOperationException { Constructor c = RX.class.getConstructor(int[].class); assertFalse(c.isVarArgs()); diff --git a/test/langtools/tools/javac/sym/ElementStructureTest.java b/test/langtools/tools/javac/sym/ElementStructureTest.java index f1cc3cd91fe..14fa194501a 100644 --- a/test/langtools/tools/javac/sym/ElementStructureTest.java +++ b/test/langtools/tools/javac/sym/ElementStructureTest.java @@ -132,10 +132,10 @@ public class ElementStructureTest { (byte) 0x3D, (byte) 0xC1, (byte) 0xFE, (byte) 0xCB }; static final byte[] hash8 = new byte[] { - (byte) 0x10, (byte) 0xE6, (byte) 0xE8, (byte) 0x11, - (byte) 0xC8, (byte) 0x02, (byte) 0x63, (byte) 0x9B, - (byte) 0xAB, (byte) 0x11, (byte) 0x9E, (byte) 0x4F, - (byte) 0xFA, (byte) 0x00, (byte) 0x6D, (byte) 0x81 + (byte) 0x07, (byte) 0xAB, (byte) 0x0A, (byte) 0x8D, + (byte) 0x1C, (byte) 0x44, (byte) 0x6D, (byte) 0x71, + (byte) 0x07, (byte) 0x53, (byte) 0x59, (byte) 0x74, + (byte) 0x5B, (byte) 0x49, (byte) 0x60, (byte) 0xAD }; final static Map version2Hash = new HashMap<>(); @@ -283,7 +283,7 @@ public class ElementStructureTest { } JavaFileObject file = new ByteArrayJavaFileObject(data.toByteArray()); try (InputStream in = new ByteArrayInputStream(data.toByteArray())) { - String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue(); + String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue().replace("/", "."); className2File.put(name, file); file2ClassName.put(file, name); } catch (IOException ex) { @@ -514,6 +514,8 @@ public class ElementStructureTest { out.write(Double.toHexString((Double) value)); } else if (value instanceof Float) { out.write(Float.toHexString((Float) value)); + } else if (value instanceof Character ch && Character.isSurrogate(ch)) { + out.write(Integer.toHexString(ch)); } else { out.write(String.valueOf(value)); } diff --git a/test/langtools/tools/javac/tree/T8024415.java b/test/langtools/tools/javac/tree/T8024415.java index ef4b0429a81..a869f63f079 100644 --- a/test/langtools/tools/javac/tree/T8024415.java +++ b/test/langtools/tools/javac/tree/T8024415.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -29,16 +29,15 @@ * @modules jdk.compiler/com.sun.tools.javac.file * jdk.compiler/com.sun.tools.javac.tree * jdk.compiler/com.sun.tools.javac.util - * @run testng T8024415 + * @run junit T8024415 */ -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.io.StringWriter; -import org.testng.annotations.Test; import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.tree.JCTree; @@ -47,13 +46,13 @@ import com.sun.tools.javac.tree.Pretty; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Names; +import org.junit.jupiter.api.Test; /* * Test verifies that the precedence rules of conditional expressions * (JCConditional) are correct. */ -@Test public class T8024415 { TreeMaker maker; @@ -72,6 +71,7 @@ public class T8024415 { // JLS 15.25: The conditional operator is syntactically right-associative // (it groups right-to-left). Thus, a?b:c?d:e?f:g means the same as // a?b:(c?d:(e?f:g)). + @Test public void testAssociativity() throws IOException { JCTree left = maker.Conditional(maker.Conditional(x, x, x), x, x); @@ -80,8 +80,8 @@ public class T8024415 { String prettyLeft = prettyPrint(left); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x?x:x)?x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:x?x:x"); + assertEquals("(x?x:x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x:x?x:x", prettyRight.replaceAll("\\s", "")); } @@ -89,6 +89,7 @@ public class T8024415 { // The true-part of a conditional expression is surrounded by ? and : // and can thus always be parsed unambiguously without surrounding // parentheses. + @Test public void testPrecedence() throws IOException { JCTree left = maker.Conditional(maker.Assign(x, x), x, x); @@ -99,9 +100,9 @@ public class T8024415 { String prettyMiddle = prettyPrint(middle); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x=x)?x:x"); - assertEquals(prettyMiddle.replaceAll("\\s", ""), "x?x=x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:(x=x)"); + assertEquals("(x=x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x=x:x", prettyMiddle.replaceAll("\\s", "")); + assertEquals("x?x:(x=x)", prettyRight.replaceAll("\\s", "")); } diff --git a/test/langtools/tools/javac/tree/TreePosTest.java b/test/langtools/tools/javac/tree/TreePosTest.java index 0ae62ca940d..9e6dcf61306 100644 --- a/test/langtools/tools/javac/tree/TreePosTest.java +++ b/test/langtools/tools/javac/tree/TreePosTest.java @@ -38,16 +38,11 @@ import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.nio.charset.Charset; +import java.io.UncheckedIOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; @@ -71,7 +66,7 @@ import javax.tools.StandardJavaFileManager; import com.sun.source.tree.CaseTree.CaseKind; import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; +import com.sun.tools.javac.api.JavacTaskPool; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.EndPosTable; @@ -80,7 +75,6 @@ import com.sun.tools.javac.tree.JCTree.JCAnnotatedType; import com.sun.tools.javac.tree.JCTree.JCCase; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; -import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.tree.JCTree.JCImportBase; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCNewClass; @@ -90,7 +84,6 @@ import com.sun.tools.javac.tree.TreeScanner; import static com.sun.tools.javac.tree.JCTree.Tag.*; import static com.sun.tools.javac.util.Position.NOPOS; -import java.util.stream.Stream; /** * Utility and test program to check validity of tree positions for tree nodes. @@ -275,6 +268,7 @@ public class TreePosTest { PrintWriter pw = new PrintWriter(sw); Reporter r = new Reporter(pw); JavacTool tool = JavacTool.create(); + JavacTaskPool pool = new JavacTaskPool(1); StandardJavaFileManager fm = tool.getStandardFileManager(r, null, null); /** @@ -285,21 +279,25 @@ public class TreePosTest { * @throws TreePosTest.ParseException if any errors occur while parsing the file */ JCCompilationUnit read(File file) throws IOException, ParseException { - JavacTool tool = JavacTool.create(); r.errors = 0; Iterable files = fm.getJavaFileObjects(file); - JavacTask task = tool.getTask(pw, fm, r, List.of("-proc:none"), null, files); - Iterable trees = task.parse(); - pw.flush(); - if (r.errors > 0) - throw new ParseException(sw.toString()); - Iterator iter = trees.iterator(); - if (!iter.hasNext()) - throw new Error("no trees found"); - JCCompilationUnit t = (JCCompilationUnit) iter.next(); - if (iter.hasNext()) - throw new Error("too many trees found"); - return t; + return pool.getTask(pw, fm, r, List.of("-proc:none"), null, files, task -> { + try { + Iterable trees = task.parse(); + pw.flush(); + if (r.errors > 0) + throw new ParseException(sw.toString()); + Iterator iter = trees.iterator(); + if (!iter.hasNext()) + throw new Error("no trees found"); + JCCompilationUnit t = (JCCompilationUnit) iter.next(); + if (iter.hasNext()) + throw new Error("too many trees found"); + return t; + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); } /** @@ -546,7 +544,7 @@ public class TreePosTest { /** * Thrown when errors are found parsing a java file. */ - private static class ParseException extends Exception { + private static class ParseException extends RuntimeException { ParseException(String msg) { super(msg); } diff --git a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java index eb7953a0baf..929d2c58685 100644 --- a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java +++ b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -24,14 +24,14 @@ /* * @test * @bug 8209022 - * @run testng TypeVariableCastTest + * @run junit TypeVariableCastTest * @summary Missing checkcast when casting to type parameter bounded by intersection type */ import java.util.*; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -@Test public class TypeVariableCastTest { static & Runnable> void f() { Runnable r = (T) new ArrayList<>(); @@ -41,13 +41,17 @@ public class TypeVariableCastTest { Runnable r = (T) new ArrayList<>(); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodF() { - f(); + @Test + void testMethodF() { + Assertions.assertThrows(ClassCastException.class, () -> { + f(); + }); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodG() { - g(); + @Test + void testMethodG() { + Assertions.assertThrows(ClassCastException.class, () -> { + g(); + }); } } diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e1.out b/test/langtools/tools/javac/warnings/WerrorLint.e1.out new file mode 100644 index 00000000000..ae99cfa5056 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e1.out @@ -0,0 +1,4 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +- compiler.err.warnings.and.werror +1 error +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e2.out b/test/langtools/tools/javac/warnings/WerrorLint.e2.out new file mode 100644 index 00000000000..1c9bd4d54f8 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e2.out @@ -0,0 +1,5 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +- compiler.err.warnings.and.werror +1 error +2 warnings diff --git a/test/langtools/tools/javac/warnings/WerrorLint.java b/test/langtools/tools/javac/warnings/WerrorLint.java new file mode 100644 index 00000000000..3331a664d55 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.java @@ -0,0 +1,23 @@ +/* + * @test /nodynamiccopyright/ + * @bug 8349847 + * + * @compile -XDrawDiagnostics -Xlint:none WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:empty WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w2.out -XDrawDiagnostics -Xlint:all WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror:strictfp WerrorLint.java + */ + +class WerrorLint { + strictfp void m() { // [strictfp] - this category is enabled by default + if (hashCode() == 1) ; // [empty] - this category is disabled by default + } +} diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w1.out b/test/langtools/tools/javac/warnings/WerrorLint.w1.out new file mode 100644 index 00000000000..3e19de51033 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w1.out @@ -0,0 +1,2 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w2.out b/test/langtools/tools/javac/warnings/WerrorLint.w2.out new file mode 100644 index 00000000000..bac258706a6 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w2.out @@ -0,0 +1,3 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +2 warnings diff --git a/test/lib-test/TEST.ROOT b/test/lib-test/TEST.ROOT index 162e6e15ec2..ebdf3f1a334 100644 --- a/test/lib-test/TEST.ROOT +++ b/test/lib-test/TEST.ROOT @@ -29,7 +29,7 @@ keys=randomness # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8.1+1 # Allow querying of various System properties in @requires clauses requires.extraPropDefns = ../jtreg-ext/requires/VMProps.java diff --git a/test/lib/jdk/test/lib/Utils.java b/test/lib/jdk/test/lib/Utils.java index c4a42dc61ba..2f46ed87340 100644 --- a/test/lib/jdk/test/lib/Utils.java +++ b/test/lib/jdk/test/lib/Utils.java @@ -38,7 +38,6 @@ import java.net.ServerSocket; import java.net.URL; import java.net.URLClassLoader; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.CopyOption; import java.nio.file.Files; @@ -50,8 +49,6 @@ import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.FileAttribute; import java.nio.channels.SocketChannel; import java.nio.file.attribute.PosixFilePermissions; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -71,6 +68,7 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.zip.CRC32C; import static java.lang.System.lineSeparator; import static jdk.test.lib.Asserts.assertTrue; @@ -170,16 +168,11 @@ public final class Utils { var v = Runtime.version(); // promotable builds have build number, and it's greater than 0 if (v.build().orElse(0) > 0) { - // promotable build -> use 1st 8 bytes of md5($version) - try { - var md = MessageDigest.getInstance("MD5"); - var bytes = v.toString() - .getBytes(StandardCharsets.UTF_8); - bytes = md.digest(bytes); - SEED = ByteBuffer.wrap(bytes).getLong(); - } catch (NoSuchAlgorithmException e) { - throw new Error(e); - } + // promotable build -> generate a seed based on the version string + var bytes = v.toString().getBytes(StandardCharsets.UTF_8); + var crc = new CRC32C(); + crc.update(bytes); + SEED = crc.getValue(); } else { // "personal" build -> use random seed SEED = new Random().nextLong(); diff --git a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java index 83e381356a0..fb67ce83bbe 100644 --- a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java +++ b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java @@ -23,8 +23,7 @@ package jdk.test.lib.artifacts; -import jtreg.SkippedException; - +import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -90,15 +89,15 @@ public class ArtifactResolver { * @return the local path to the artifact. If the artifact is a compressed * file that gets unpacked, this path will point to the root * directory of the uncompressed file(s). - * @throws SkippedException thrown if the artifact cannot be found + * @throws IOException thrown if the artifact cannot be found */ - public static Path fetchOne(Class klass) { + public static Path fetchOne(Class klass) throws IOException { try { return ArtifactResolver.resolve(klass).entrySet().stream() .findAny().get().getValue(); } catch (ArtifactResolverException e) { Artifact artifact = klass.getAnnotation(Artifact.class); - throw new SkippedException("Cannot find the artifact " + artifact.name(), e); + throw new IOException("Cannot find the artifact " + artifact.name(), e); } } diff --git a/test/lib/jdk/test/lib/containers/cgroup/MetricsTesterCgroupV2.java b/test/lib/jdk/test/lib/containers/cgroup/MetricsTesterCgroupV2.java index a3723e2eda2..2a756102ded 100644 --- a/test/lib/jdk/test/lib/containers/cgroup/MetricsTesterCgroupV2.java +++ b/test/lib/jdk/test/lib/containers/cgroup/MetricsTesterCgroupV2.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Red Hat Inc. + * Copyright (c) 2025, 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 @@ -447,13 +448,13 @@ public class MetricsTesterCgroupV2 implements CgroupMetricsTester { Metrics metrics = Metrics.systemMetrics(); long oldVal = metrics.getBlkIOServiceCount(); long newVal = getIoStatAccumulate(new String[] { "rios", "wios" }); - if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { + if (newVal < oldVal) { fail("io.stat->rios/wios: ", oldVal, newVal); } oldVal = metrics.getBlkIOServiced(); newVal = getIoStatAccumulate(new String[] { "rbytes", "wbytes" }); - if (!CgroupMetricsTester.compareWithErrorMargin(oldVal, newVal)) { + if (newVal < oldVal) { fail("io.stat->rbytes/wbytes: ", oldVal, newVal); } } diff --git a/test/lib/jdk/test/lib/net/SimpleHttpServer.java b/test/lib/jdk/test/lib/net/SimpleHttpServer.java deleted file mode 100644 index 1905091eac6..00000000000 --- a/test/lib/jdk/test/lib/net/SimpleHttpServer.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2017, 2024, 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. - */ -package jdk.test.lib.net; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -/** - * A simple HTTP Server. - **/ -public class SimpleHttpServer { - private final HttpServer httpServer; - private ExecutorService executor; - private String address; - private final String context; - private final String docRoot; - private final InetSocketAddress inetSocketAddress; - - public SimpleHttpServer(final InetSocketAddress inetSocketAddress, final String context, final String docRoot) - throws IOException { - this.inetSocketAddress = inetSocketAddress; - this.context = context; - this.docRoot = docRoot; - httpServer = HttpServer.create(); - } - - public void start() throws IOException, URISyntaxException { - MyHttpHandler handler = new MyHttpHandler(docRoot); - httpServer.bind(inetSocketAddress, 0); - httpServer.createContext(context, handler); - executor = Executors.newCachedThreadPool(); - httpServer.setExecutor(executor); - httpServer.start(); - address = "http:" + URIBuilder.newBuilder().host(httpServer.getAddress().getAddress()). - port(httpServer.getAddress().getPort()).build().toString(); - } - - public void stop() { - httpServer.stop(0); - executor.shutdown(); - } - - public String getAddress() { - return address; - } - - public int getPort() { - return httpServer.getAddress().getPort(); - } - - class MyHttpHandler implements HttpHandler { - private final URI rootUri; - - MyHttpHandler(final String docroot) { - rootUri = Path.of(docroot).toUri().normalize(); - } - - public void handle(final HttpExchange t) throws IOException { - try (InputStream is = t.getRequestBody()) { - is.readAllBytes(); - Headers rMap = t.getResponseHeaders(); - try (OutputStream os = t.getResponseBody()) { - URI uri = t.getRequestURI(); - String path = uri.getRawPath(); - assert path.isEmpty() || path.startsWith("/"); - Path fPath; - try { - uri = URI.create("file://" + rootUri.getRawPath() + path).normalize(); - fPath = Path.of(uri); - } catch (IllegalArgumentException | FileSystemNotFoundException ex) { - ex.printStackTrace(); - notfound(t, path); - return; - } - byte[] bytes = Files.readAllBytes(fPath); - String method = t.getRequestMethod(); - if (method.equals("HEAD")) { - rMap.set("Content-Length", Long.toString(bytes.length)); - t.sendResponseHeaders(200, -1); - t.close(); - } else if (!method.equals("GET")) { - t.sendResponseHeaders(405, -1); - t.close(); - return; - } - if (path.endsWith(".html") || path.endsWith(".htm")) { - rMap.set("Content-Type", "text/html"); - } else { - rMap.set("Content-Type", "text/plain"); - } - t.sendResponseHeaders(200, bytes.length); - os.write(bytes); - } - } - } - void moved(final HttpExchange t) throws IOException { - Headers req = t.getRequestHeaders(); - Headers map = t.getResponseHeaders(); - URI uri = t.getRequestURI(); - String host = req.getFirst("Host"); - String location = "http://" + host + uri.getPath() + "/"; - map.set("Content-Type", "text/html"); - map.set("Location", location); - t.sendResponseHeaders(301, -1); - t.close(); - } - void notfound(final HttpExchange t, final String p) throws IOException { - t.getResponseHeaders().set("Content-Type", "text/html"); - t.sendResponseHeaders(404, 0); - try (OutputStream os = t.getResponseBody()) { - String s = "

    File not found

    "; - s = s + p + "

    "; - os.write(s.getBytes()); - } - t.close(); - } - } -} diff --git a/test/lib/jdk/test/lib/os/linux/Smaps.java b/test/lib/jdk/test/lib/os/linux/Smaps.java new file mode 100644 index 00000000000..11cdfe34319 --- /dev/null +++ b/test/lib/jdk/test/lib/os/linux/Smaps.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2025, 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. + */ + +package jdk.test.lib.os.linux; + +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Smaps { + + // List of memory ranges + private List ranges; + + protected Smaps(List ranges) { + this.ranges = ranges; + } + + // Search for a range including the given address. + public Range getRange(String addr) { + BigInteger laddr = new BigInteger(addr.substring(2), 16); + for (Range range : ranges) { + if (range.includes(laddr)) { + return range; + } + } + + return null; + } + + public static Smaps parseSelf() throws Exception { + return parse(Path.of("/proc/self/smaps")); + } + + public static Smaps parse(Path smaps) throws Exception { + return new Parser(smaps).parse(); + } + + // This is a simple smaps parser; it will recognize smaps section start lines + // (e.g. "40fa00000-439b80000 rw-p 00000000 00:00 0 ") and look for keywords inside the section. + // Section will be finished and written into a RangeWithPageSize when either the next section is found + // or the end of file is encountered. + private static class Parser { + + private static final Pattern SECTION_START_PATT = Pattern.compile("^([a-f0-9]+)-([a-f0-9]+) [\\-rwpsx]{4}.*"); + private static final Pattern KERNEL_PAGESIZE_PATT = Pattern.compile("^KernelPageSize:\\s*(\\d*) kB"); + private static final Pattern THP_ELIGIBLE_PATT = Pattern.compile("^THPeligible:\\s+(\\d*)"); + private static final Pattern VMFLAGS_PATT = Pattern.compile("^VmFlags: ([\\w\\? ]*)"); + + String start; + String end; + String ps; + String thpEligible; + String vmFlags; + + List ranges; + Path smaps; + + Parser(Path smaps) { + this.ranges = new LinkedList(); + this.smaps = smaps; + reset(); + } + + void reset() { + start = null; + end = null; + ps = null; + thpEligible = null; + vmFlags = null; + } + + public void finish() { + if (start != null) { + Range range = new Range(start, end, ps, thpEligible, vmFlags); + ranges.add(range); + reset(); + } + } + + public void eatNext(String line) { + // For better debugging experience call finish here before the debug() call. + Matcher matSectionStart = SECTION_START_PATT.matcher(line); + if (matSectionStart.matches()) { + finish(); + } + + if (matSectionStart.matches()) { + start = matSectionStart.group(1); + end = matSectionStart.group(2); + ps = null; + vmFlags = null; + return; + } else { + Matcher matKernelPageSize = KERNEL_PAGESIZE_PATT.matcher(line); + if (matKernelPageSize.matches()) { + ps = matKernelPageSize.group(1); + return; + } + Matcher matTHPEligible = THP_ELIGIBLE_PATT.matcher(line); + if (matTHPEligible.matches()) { + thpEligible = matTHPEligible.group(1); + return; + } + Matcher matVmFlags = VMFLAGS_PATT.matcher(line); + if (matVmFlags.matches()) { + vmFlags = matVmFlags.group(1); + return; + } + } + } + + // Copy smaps locally + // (To minimize chances of concurrent modification when parsing, as well as helping with error analysis) + private Path copySmaps() throws Exception { + Path copy = Paths.get("smaps-copy-" + ProcessHandle.current().pid() + "-" + System.nanoTime() + ".txt"); + Files.copy(smaps, copy, StandardCopyOption.REPLACE_EXISTING); + return copy; + } + + // Parse /proc/self/smaps + public Smaps parse() throws Exception { + Path smapsCopy = copySmaps(); + Files.lines(smapsCopy).forEach(this::eatNext); + + // Finish up the last range + this.finish(); + + // Return a Smaps object with the parsed ranges + return new Smaps(ranges); + } + } + + // Class used to store information about memory ranges parsed + // from /proc/self/smaps. The file contain a lot of information + // about the different mappings done by an application, but the + // lines we care about are: + // 700000000-73ea00000 rw-p 00000000 00:00 0 + // ... + // KernelPageSize: 4 kB + // ... + // THPeligible: 0 + // ... + // VmFlags: rd wr mr mw me ac sd + // + // We use the VmFlags to know what kind of huge pages are used. + // For transparent huge pages the KernelPageSize field will not + // report the large page size. + public static class Range { + + private BigInteger start; + private BigInteger end; + private long pageSize; + private boolean thpEligible; + private boolean vmFlagHG; + private boolean vmFlagHT; + private boolean isTHP; + + public Range(String start, String end, String pageSize, String thpEligible, String vmFlags) { + // Note: since we insist on kernels >= 3.8, all the following information should be present + // (none of the input strings be null). + this.start = new BigInteger(start, 16); + this.end = new BigInteger(end, 16); + this.pageSize = Long.parseLong(pageSize); + this.thpEligible = thpEligible == null ? false : (Integer.parseInt(thpEligible) == 1); + + vmFlagHG = false; + vmFlagHT = false; + // Check if the vmFlags line include: + // * ht - Meaning the range is mapped using explicit huge pages. + // * hg - Meaning the range is madvised huge. + for (String flag : vmFlags.split(" ")) { + if (flag.equals("ht")) { + vmFlagHT = true; + } else if (flag.equals("hg")) { + vmFlagHG = true; + } + } + + // When the THP policy is 'always' instead of 'madvise, the vmFlagHG property is false, + // therefore also check thpEligible. If this is still causing problems in the future, + // we might have to check the AnonHugePages field. + + isTHP = vmFlagHG || this.thpEligible; + } + + public BigInteger getStart() { + return start; + } + + public BigInteger getEnd() { + return end; + } + + public long getPageSize() { + return pageSize; + } + + public boolean isTransparentHuge() { + return isTHP; + } + + public boolean isExplicitHuge() { + return vmFlagHT; + } + + public boolean includes(BigInteger addr) { + boolean isGreaterThanOrEqualStart = start.compareTo(addr) <= 0; + boolean isLessThanEnd = addr.compareTo(end) < 0; + + return isGreaterThanOrEqualStart && isLessThanEnd; + } + + public String toString() { + return "[" + start.toString(16) + ", " + end.toString(16) + ") " + + "pageSize=" + pageSize + "KB isTHP=" + isTHP + " isHUGETLB=" + vmFlagHT; + } + } +} diff --git a/test/lib/jdk/test/lib/process/OutputAnalyzer.java b/test/lib/jdk/test/lib/process/OutputAnalyzer.java index 3c5be74558b..fa5e30dd815 100644 --- a/test/lib/jdk/test/lib/process/OutputAnalyzer.java +++ b/test/lib/jdk/test/lib/process/OutputAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -354,6 +354,27 @@ public final class OutputAnalyzer { return this; } + /** + * Returns true if stdout matches the given pattern + */ + public boolean stdoutMatches(String regexp) { + return getStdout().matches(regexp); + } + + /** + * Returns true if stderr matches the given pattern + */ + public boolean stderrMatches(String regexp) { + return getStderr().matches(regexp); + } + + /** + * Returns true if either stdout or stderr matches the given pattern + */ + public boolean matches(String regexp) { + return stdoutMatches(regexp) || stderrMatches(regexp); + } + /** * Verify that the stdout and stderr contents of output buffer matches * the pattern diff --git a/test/lib/jdk/test/lib/security/CertificateBuilder.java b/test/lib/jdk/test/lib/security/CertificateBuilder.java index e5044d46b0f..d35a21e7ab5 100644 --- a/test/lib/jdk/test/lib/security/CertificateBuilder.java +++ b/test/lib/jdk/test/lib/security/CertificateBuilder.java @@ -53,6 +53,7 @@ import sun.security.x509.KeyUsageExtension; import sun.security.x509.SubjectAlternativeNameExtension; import sun.security.x509.URIName; import sun.security.x509.KeyIdentifier; +import sun.security.x509.X500Name; /** @@ -90,7 +91,7 @@ import sun.security.x509.KeyIdentifier; public class CertificateBuilder { private final CertificateFactory factory; - private X500Principal subjectName = null; + private X500Name subjectName = null; private BigInteger serialNumber = null; private PublicKey publicKey = null; private Date notBefore = null; @@ -199,7 +200,7 @@ public class CertificateBuilder { * on this certificate. */ public CertificateBuilder setSubjectName(X500Principal name) { - subjectName = name; + subjectName = X500Name.asX500Name(name); return this; } @@ -209,7 +210,23 @@ public class CertificateBuilder { * @param name The subject name in RFC 2253 format */ public CertificateBuilder setSubjectName(String name) { - subjectName = new X500Principal(name); + try { + subjectName = new X500Name(name); + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + return this; + } + + /** + * Set the subject name for the certificate. This method is useful when + * you need more control over the contents of the subject name. + * + * @param name an {@code X500Name} to be used as the subject name + * on this certificate + */ + public CertificateBuilder setSubjectName(X500Name name) { + subjectName = name; return this; } diff --git a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java index 82252000154..6fa8cb2e408 100644 --- a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java +++ b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java @@ -23,6 +23,7 @@ package jdk.test.lib.security; +import java.io.IOException; import java.nio.file.Path; import jdk.test.lib.Platform; import jdk.test.lib.process.ProcessTools; @@ -49,42 +50,40 @@ public class OpensslArtifactFetcher { * * @return openssl binary path of the current version * @throws SkippedException if a valid version of OpenSSL cannot be found + * or if OpenSSL is not available on the target platform */ public static String getOpensslPath() { String path = getOpensslFromSystemProp(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system property."); return path; } + path = getDefaultSystemOpensslPath(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system."); return path; } + if (Platform.isX64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_X64.class); + return fetchOpenssl(LINUX_X64.class); } else if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_X64.class); + return fetchOpenssl(MACOSX_X64.class); } else if (Platform.isWindows()) { - path = fetchOpenssl(WINDOWS_X64.class); + return fetchOpenssl(WINDOWS_X64.class); } } else if (Platform.isAArch64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_AARCH64.class); + return fetchOpenssl(LINUX_AARCH64.class); } if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_AARCH64.class); + return fetchOpenssl(MACOSX_AARCH64.class); } } - if (!verifyOpensslVersion(path, OPENSSL_BUNDLE_VERSION)) { - String exMsg = "Can't find the version: " - + OpensslArtifactFetcher.getTestOpensslBundleVersion() - + " of openssl binary on this machine, please install" - + " and set openssl path with property 'test.openssl.path'"; - throw new SkippedException(exMsg); - } else { - return path; - } + throw new SkippedException(String.format("No OpenSSL %s found for %s/%s", + OPENSSL_BUNDLE_VERSION, Platform.getOsName(), Platform.getOsArch())); } private static String getOpensslFromSystemProp(String version) { @@ -120,9 +119,13 @@ public class OpensslArtifactFetcher { } private static String fetchOpenssl(Class clazz) { - return ArtifactResolver.fetchOne(clazz) - .resolve("openssl", "bin", "openssl") - .toString(); + try { + return ArtifactResolver.fetchOne(clazz) + .resolve("openssl", "bin", "openssl") + .toString(); + } catch (IOException exc) { + throw new SkippedException("Could not find openssl", exc); + } } // retrieve the provider directory path from /bin/openssl diff --git a/test/lib/jdk/test/lib/threaddump/ThreadDump.java b/test/lib/jdk/test/lib/threaddump/ThreadDump.java index f4964a9521f..ca728e625fc 100644 --- a/test/lib/jdk/test/lib/threaddump/ThreadDump.java +++ b/test/lib/jdk/test/lib/threaddump/ThreadDump.java @@ -296,6 +296,16 @@ public final class ThreadDump { return getStringProperty("parkBlocker", "object"); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + public OptionalLong parkBlockerOwner() { + String owner = getStringProperty("parkBlocker", "owner"); + return (owner != null) + ? OptionalLong.of(Long.parseLong(owner)) + : OptionalLong.empty(); + } + /** * Returns the object that the thread is blocked entering its monitor. */ diff --git a/test/lib/jdk/test/whitebox/WhiteBox.java b/test/lib/jdk/test/whitebox/WhiteBox.java index 5adb7bf5127..e989b0aca88 100644 --- a/test/lib/jdk/test/whitebox/WhiteBox.java +++ b/test/lib/jdk/test/whitebox/WhiteBox.java @@ -78,6 +78,8 @@ public class WhiteBox { public native long getHeapSpaceAlignment(); public native long getHeapAlignment(); + public native boolean hasExternalSymbolsStripped(); + private native boolean isObjectInOldGen0(Object o); public boolean isObjectInOldGen(Object o) { Objects.requireNonNull(o); @@ -490,6 +492,12 @@ public class WhiteBox { Objects.requireNonNull(method); return getNMethod0(method, isOsr); } + private native void relocateNMethodFromMethod0(Executable method, int type); + public void relocateNMethodFromMethod(Executable method, int type) { + Objects.requireNonNull(method); + relocateNMethodFromMethod0(method, type); + } + public native void relocateNMethodFromAddr(long address, int type); public native long allocateCodeBlob(int size, int type); public long allocateCodeBlob(long size, int type) { int intSize = (int) size; @@ -841,13 +849,12 @@ public class WhiteBox { public native void waitUnsafe(int time_ms); - public native void busyWait(int cpuTimeMs); + public native void busyWaitCPUTime(int cpuTimeMs); + // returns true if supported, false if not public native boolean cpuSamplerSetOutOfStackWalking(boolean enable); - public native long cpuSamplerOutOfStackWalkingIterations(); - public native void pinObject(Object o); public native void unpinObject(Object o); diff --git a/test/lib/jdk/test/whitebox/code/CodeBlob.java b/test/lib/jdk/test/whitebox/code/CodeBlob.java index c6c23fdff0c..fd95b5a7e7d 100644 --- a/test/lib/jdk/test/whitebox/code/CodeBlob.java +++ b/test/lib/jdk/test/whitebox/code/CodeBlob.java @@ -46,18 +46,22 @@ public class CodeBlob { return new CodeBlob(obj); } protected CodeBlob(Object[] obj) { - assert obj.length == 4; + assert obj.length == 6; name = (String) obj[0]; size = (Integer) obj[1]; int blob_type_index = (Integer) obj[2]; code_blob_type = BlobType.values()[blob_type_index]; assert code_blob_type.id == (Integer) obj[2]; address = (Long) obj[3]; + code_begin = (Long) obj[4]; + isNMethod = (Boolean) obj[5]; } public final String name; public final int size; public final BlobType code_blob_type; public final long address; + public final long code_begin; + public final boolean isNMethod; @Override public String toString() { return "CodeBlob{" @@ -65,6 +69,7 @@ public class CodeBlob { + ", size=" + size + ", code_blob_type=" + code_blob_type + ", address=" + address + + ", code_begin=" + code_begin + '}'; } } diff --git a/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java b/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java new file mode 100644 index 00000000000..614b3fe7b20 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/ClassLoaderDefineClass.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025, 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. + */ +package org.openjdk.bench.java.lang; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.lang.foreign.Arena; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import java.util.HexFormat; + +/** + * Tests java.lang.ClassLoader.defineClass(ByteBuffer) + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +public class ClassLoaderDefineClass { + + private byte[] classBytes; + private ByteBuffer directBuffer; + private ByteBuffer heapBuffer; + + @Setup(Level.Iteration) + public void setup() throws Exception { + classBytes = getTestClassBytes(); + directBuffer = Arena.ofConfined() + .allocate(classBytes.length) + .asByteBuffer() + .put(classBytes) + .flip(); + heapBuffer = ByteBuffer.wrap(classBytes); + } + + @Benchmark + public void testDefineClassByteBufferHeap(Blackhole bh) throws Exception { + bh.consume(new DummyClassLoader().defineClassFromHeapBuffer(heapBuffer)); + } + + @Benchmark + public void testDefineClassByteBufferDirect(Blackhole bh) throws Exception { + bh.consume(new DummyClassLoader().defineClassFromDirectBuffer(directBuffer)); + } + + private static final class DummyClassLoader extends ClassLoader { + + Class defineClassFromHeapBuffer(ByteBuffer bb) throws Exception { + bb.rewind(); + return defineClass(null, bb, null); + } + + Class defineClassFromDirectBuffer(ByteBuffer bb) throws Exception { + bb.rewind(); + return defineClass(null, bb, null); + } + } + + private static byte[] getTestClassBytes() throws Exception { + final String source = """ + public class Greeting { + public String hello() { + return "Hello"; + } + } + """; + // (externally) compiled content of the above source, represented as hex + final String classBytesHex = """ + cafebabe0000004600110a000200030700040c000500060100106a617661 + 2f6c616e672f4f626a6563740100063c696e69743e010003282956080008 + 01000548656c6c6f07000a0100084772656574696e67010004436f646501 + 000f4c696e654e756d6265725461626c6501000568656c6c6f0100142829 + 4c6a6176612f6c616e672f537472696e673b01000a536f7572636546696c + 6501000d4772656574696e672e6a61766100210009000200000000000200 + 01000500060001000b0000001d00010001000000052ab70001b100000001 + 000c000000060001000000010001000d000e0001000b0000001b00010001 + 000000031207b000000001000c000000060001000000030001000f000000 + 020010 + """; + return HexFormat.of().parseHex(classBytesHex.replaceAll("\n", "")); + } +} diff --git a/test/micro/org/openjdk/bench/java/lang/FloatingDecimal.java b/test/micro/org/openjdk/bench/java/lang/Doubles.java similarity index 89% rename from test/micro/org/openjdk/bench/java/lang/FloatingDecimal.java rename to test/micro/org/openjdk/bench/java/lang/Doubles.java index b8d29aabc84..50c295900af 100644 --- a/test/micro/org/openjdk/bench/java/lang/FloatingDecimal.java +++ b/test/micro/org/openjdk/bench/java/lang/Doubles.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Alibaba Group Holding Limited. 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 @@ -47,7 +48,7 @@ import java.util.concurrent.TimeUnit; @Warmup(iterations = 10, time = 1) @Measurement(iterations = 5, time = 2) @Fork(3) -public class FloatingDecimal { +public class Doubles { private double[] randomArray, twoDecimalsArray, integerArray; private static final int TESTSIZE = 1000; @@ -65,6 +66,14 @@ public class FloatingDecimal { } } + @Benchmark + @OperationsPerInvocation(TESTSIZE) + public void toHexString(Blackhole bh) { + for (double d : randomArray) { + bh.consume(Double.toHexString(d)); + } + } + /** Tests Double.toString on double values generated from Random.nextDouble() */ @Benchmark @OperationsPerInvocation(TESTSIZE) diff --git a/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java b/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java new file mode 100644 index 00000000000..c896c7504a6 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/PopCountValueTransform.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025, 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. + */ +package org.openjdk.bench.java.lang; + +import org.openjdk.jmh.annotations.*; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class PopCountValueTransform { + public int lower_bound = 0; + public int upper_bound = 10000; + + @Benchmark + public int LogicFoldingKerenlInt() { + int res = 0; + for (int i = lower_bound; i < upper_bound; i++) { + int constrained_i = i & 0xFFFF; + if (Integer.bitCount(constrained_i) > 16) { + throw new AssertionError("Uncommon trap"); + } + res += constrained_i; + } + return res; + } + + @Benchmark + public long LogicFoldingKerenLong() { + long res = 0; + for (int i = lower_bound; i < upper_bound; i++) { + long constrained_i = i & 0xFFFFFFL; + if (Long.bitCount(constrained_i) > 24) { + throw new AssertionError("Uncommon trap"); + } + res += constrained_i; + } + return res; + } +} diff --git a/test/micro/org/openjdk/bench/java/lang/runtime/RecordMethodsBenchmark.java b/test/micro/org/openjdk/bench/java/lang/runtime/RecordMethodsBenchmark.java new file mode 100644 index 00000000000..91d26601383 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/runtime/RecordMethodsBenchmark.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2025, 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. + */ + +package org.openjdk.bench.java.lang.runtime; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +/// Tests the generated equals and hashCode for records. +/// There are 4 types of methods: +/// - distinct: distinct sites for type profiling +/// - polluted: megamorphic site that blocks type profiling +/// - generated: actual body generated by ObjectMethods::bootstrap +/// - specialized: generated body for non-extensible types +/// The result of generated compared to the other distinct/polluted shows +/// whether the generated code could perform type profiling. +/// Specialized is the result of distinct without trap, should be even faster. +@Fork(3) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 5, time = 2) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.Throughput) +public class RecordMethodsBenchmark { + + record One(int a) {} + + @State(Scope.Thread) + public static class BenchmarkState { + Key k1 = new Key(new One(1), "a"); + Key k2 = new Key(new One(1), new String("a")); + SpecializedKey sk1 = new SpecializedKey(new One(1), "a"); + SpecializedKey sk2 = new SpecializedKey(new One(1), new String("a")); + } + + @Benchmark + public int hashCodeDistinct(BenchmarkState state) { + return state.k1.hashCodeDistinct(); + } + + @Benchmark + public int hashCodePolluted(BenchmarkState state) { + return state.k1.hashCodePolluted(); + } + + @Benchmark + public int hashCodeGenerated(BenchmarkState state) { + return state.k1.hashCode(); + } + + @Benchmark + public int hashCodeSpecial(BenchmarkState state) { + return state.sk1.hashCode(); + } + + @Benchmark + public boolean equalsDistinct(BenchmarkState state) { + return state.k1.equalsDistinct(state.k2); + } + + @Benchmark + public boolean equalsPolluted(BenchmarkState state) { + return state.k1.equalsPolluted(state.k2); + } + + @Benchmark + public boolean equalsGenerated(BenchmarkState state) { + return state.k1.equals(state.k2); + } + + @Benchmark + public boolean equalsSpecial(BenchmarkState state) { + return state.sk1.equals(state.sk2); + } + + /// A key object. + /// + /// Having both field as Object pollutes Object.equals for record object + /// method MH tree. We must verify the leaf Object.equals calls don't + /// share the same profile in generated code. + record Key(Object key1, Object key2) { + /// A hashCode method which has distinct hashCode invocations + /// in bytecode for each field for type profiling. + public int hashCodeDistinct() { + final int prime = 31; + int result = 1; + result = prime * result + ((key1 == null) ? 0 : key1.hashCode()); + result = prime * result + ((key2 == null) ? 0 : key2.hashCode()); + return result; + } + + /// A hashCode method which uses a megamorphic polluted + /// Object.hashCode virtual invocation in Objects.hashCode. + public int hashCodePolluted() { + final int prime = 31; + int result = 1; + result = prime * result + Objects.hashCode(key1); + result = prime * result + Objects.hashCode(key2); + return result; + } + + /// An equals method which has distinct equals invocations + /// in bytecode for each field for type profiling. + public boolean equalsDistinct(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + if (key1 == null) { + if (other.key1 != null) + return false; + } + else if (!key1.equals(other.key1)) + return false; + if (key2 == null) { + if (other.key2 != null) + return false; + } + else if (!key2.equals(other.key2)) + return false; + return true; + } + + /// An equals method which uses a megamorphic polluted + /// Object.equals virtual invocation in Objects.equals. + public boolean equalsPolluted(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + return Objects.equals(key1, other.key1) && Objects.equals(key2, other.key2); + } + } + + record SpecializedKey(One key1, String key2) {} +} diff --git a/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java b/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java new file mode 100644 index 00000000000..23dba3858ba --- /dev/null +++ b/test/micro/org/openjdk/bench/java/nio/file/ToRealPath.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, 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. + */ +package org.openjdk.bench.java.nio.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.FileVisitResult; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Benchmark) +public class ToRealPath { + + static Random RND = new Random(17_000_126); + + static final String NAME = "RealPath"; + static final int LEN = NAME.length(); + + Path root; + Path[] files; + + @Setup + public void init() throws IOException { + // root the test files at CWD/NAME + root = Path.of(System.getProperty("user.dir")).resolve(NAME); + + // populate files array + StringBuilder sb = new StringBuilder(); + files = new Path[100]; + for (int i = 0; i < files.length; i++) { + // create directories up to a depth of 9, inclusive + sb.setLength(0); + int depth = RND.nextInt(10); + for (int j = 0; j < depth; j++) { + sb.append("dir"); + sb.append(j); + sb.append(File.separatorChar); + } + Path dir = root.resolve(sb.toString()); + Files.createDirectories(dir); + + // set the file prefix with random case conversion + String prefix; + if (RND.nextBoolean()) { + sb.setLength(0); + for (int k = 0; k < LEN; k++) { + char c = NAME.charAt(k); + sb.append(RND.nextBoolean() + ? Character.toLowerCase(c) + : Character.toUpperCase(c)); + } + prefix = sb.append(i).toString(); + } else { + prefix = NAME + i; + } + + // create the file + Path tmpFile = Files.createTempFile(dir, prefix, ".tmp"); + + // set the array path to a version with a lower case name + String tmpName = tmpFile.getFileName().toString().toLowerCase(); + files[i] = tmpFile.getParent().resolve(tmpName); + } + } + + @TearDown + public void cleanup() throws IOException { + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException + { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException e) + throws IOException + { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + @Benchmark + public Path noFollowLinks() throws IOException { + int i = RND.nextInt(0, files.length); + return files[i].toRealPath(LinkOption.NOFOLLOW_LINKS); + } +} diff --git a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java index 1da18cc97a0..cd49469c15d 100644 --- a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java +++ b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -22,13 +22,16 @@ */ package org.openjdk.bench.java.text; +import java.math.BigDecimal; import java.text.NumberFormat; import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.stream.DoubleStream; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OperationsPerInvocation; @@ -50,25 +53,52 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) public class DefFormatterBench { + public static final int VALUES_SIZE = 13; public double[] values; + public BigDecimal[] bdLargeValues; + public BigDecimal[] bdSmallValues; - @Setup + @Setup(Level.Invocation) public void setup() { values = new double[] { 1.23, 1.49, 1.80, 1.7, 0.0, -1.49, -1.50, 9999.9123, 1.494, 1.495, 1.03, 25.996, -25.996 }; + + bdLargeValues = DoubleStream.of(values) + .mapToObj(BigDecimal::new) + .toArray(BigDecimal[]::new); + + bdSmallValues = DoubleStream.of(values) + .mapToObj(BigDecimal::valueOf) + .toArray(BigDecimal[]::new); } private DefNumberFormat dnf = new DefNumberFormat(); @Benchmark - @OperationsPerInvocation(13) + @OperationsPerInvocation(VALUES_SIZE) public void testDefNumberFormatter(final Blackhole blackhole) { for (double value : values) { blackhole.consume(this.dnf.format(value)); } } + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testSmallBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdSmallValues) { + blackhole.consume(this.dnf.format(value)); + } + } + + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testLargeBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdLargeValues) { + blackhole.consume(this.dnf.format(value)); + } + } + public static void main(String... args) throws Exception { Options opts = new OptionsBuilder().include(DefFormatterBench.class.getSimpleName()).shouldDoGC(true).build(); new Runner(opts).run(); @@ -88,5 +118,9 @@ public class DefFormatterBench { public String format(final double d) { return this.n.format(d); } + + public String format(final BigDecimal bd) { + return this.n.format(bd); + } } } diff --git a/test/micro/org/openjdk/bench/javax/crypto/AESDecrypt.java b/test/micro/org/openjdk/bench/javax/crypto/AESDecrypt.java new file mode 100644 index 00000000000..0514a6d25f3 --- /dev/null +++ b/test/micro/org/openjdk/bench/javax/crypto/AESDecrypt.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025, 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. + */ +package org.openjdk.bench.javax.crypto; + +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class AESDecrypt { + + @Param("10000000") + private int count; + + private Cipher cipher; + private byte[] src; + private byte[] ct; + + @Setup + public void setup() throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(new byte[]{-80, -103, -1, 68, -29, -94, 61, -52, 93, -59, -128, 105, 110, 88, 44, 105}, "AES"); + IvParameterSpec iv = new IvParameterSpec(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); + + src = new byte[count]; + new Random(1).nextBytes(src); + + ct = cipher.doFinal(src); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); + } + + @Benchmark + @Fork(jvmArgs = {"-XX:+UnlockDiagnosticVMOptions", "-XX:-UseAES", "-XX:-UseAESIntrinsics"}) + public byte[] testBaseline() throws Exception { + return cipher.doFinal(ct); + } + + @Benchmark + @Fork(jvmArgs = {"-XX:+UnlockDiagnosticVMOptions", "-XX:+UseAES", "-XX:-UseAESIntrinsics"}) + public byte[] testUseAes() throws Exception { + return cipher.doFinal(ct); + } + + @Benchmark + @Fork(jvmArgs = {"-XX:+UnlockDiagnosticVMOptions", "-XX:+UseAES", "-XX:+UseAESIntrinsics"}) + public byte[] testUseAesIntrinsics() throws Exception { + return cipher.doFinal(ct); + } + +} diff --git a/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java new file mode 100644 index 00000000000..d83bc126a1d --- /dev/null +++ b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION & 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. + */ + +package org.openjdk.bench.jdk.incubator.vector; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.*; + +import jdk.incubator.vector.*; +import java.lang.invoke.*; +import java.util.concurrent.TimeUnit; +import java.util.Random; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 2, jvmArgs = { "--add-modules=jdk.incubator.vector" }) +public abstract class MaskCompareNotBenchmark { + @Param({"4096"}) + protected int ARRAYLEN; + + // Abstract method to get comparison operator from subclasses + protected abstract String getComparisonOperatorName(); + + // To get compile-time constants for comparison operation + static final MutableCallSite MUTABLE_COMPARISON_CONSTANT = new MutableCallSite(MethodType.methodType(VectorOperators.Comparison.class)); + static final MethodHandle MUTABLE_COMPARISON_CONSTANT_HANDLE = MUTABLE_COMPARISON_CONSTANT.dynamicInvoker(); + + private static Random r = new Random(); + + protected static final VectorSpecies B_SPECIES = ByteVector.SPECIES_MAX; + protected static final VectorSpecies S_SPECIES = ShortVector.SPECIES_MAX; + protected static final VectorSpecies I_SPECIES = IntVector.SPECIES_MAX; + protected static final VectorSpecies L_SPECIES = LongVector.SPECIES_MAX; + protected static final VectorSpecies F_SPECIES = FloatVector.SPECIES_MAX; + protected static final VectorSpecies D_SPECIES = DoubleVector.SPECIES_MAX; + + protected boolean[] mr; + protected byte[] ba; + protected byte[] bb; + protected short[] sa; + protected short[] sb; + protected int[] ia; + protected int[] ib; + protected long[] la; + protected long[] lb; + protected float[] fa; + protected float[] fb; + protected double[] da; + protected double[] db; + + @Setup + public void init() throws Throwable { + mr = new boolean[ARRAYLEN]; + ba = new byte[ARRAYLEN]; + bb = new byte[ARRAYLEN]; + sa = new short[ARRAYLEN]; + sb = new short[ARRAYLEN]; + ia = new int[ARRAYLEN]; + ib = new int[ARRAYLEN]; + la = new long[ARRAYLEN]; + lb = new long[ARRAYLEN]; + fa = new float[ARRAYLEN]; + fb = new float[ARRAYLEN]; + da = new double[ARRAYLEN]; + db = new double[ARRAYLEN]; + + for (int i = 0; i < ARRAYLEN; i++) { + mr[i] = r.nextBoolean(); + ba[i] = (byte) r.nextInt(); + bb[i] = (byte) r.nextInt(); + sa[i] = (short) r.nextInt(); + sb[i] = (short) r.nextInt(); + ia[i] = r.nextInt(); + ib[i] = r.nextInt(); + la[i] = r.nextLong(); + lb[i] = r.nextLong(); + fa[i] = r.nextFloat(); + fb[i] = r.nextFloat(); + da[i] = r.nextDouble(); + db[i] = r.nextDouble(); + } + + VectorOperators.Comparison comparisonOp = getComparisonOperator(getComparisonOperatorName()); + MethodHandle constant = MethodHandles.constant(VectorOperators.Comparison.class, comparisonOp); + MUTABLE_COMPARISON_CONSTANT.setTarget(constant); + } + + @CompilerControl(CompilerControl.Mode.INLINE) + private static VectorOperators.Comparison getComparisonOperator(String op) { + switch (op) { + case "EQ": return VectorOperators.EQ; + case "NE": return VectorOperators.NE; + case "LT": return VectorOperators.LT; + case "LE": return VectorOperators.LE; + case "GT": return VectorOperators.GT; + case "GE": return VectorOperators.GE; + case "ULT": return VectorOperators.ULT; + case "ULE": return VectorOperators.ULE; + case "UGT": return VectorOperators.UGT; + case "UGE": return VectorOperators.UGE; + default: throw new IllegalArgumentException("Unknown comparison operator: " + op); + } + } + + @CompilerControl(CompilerControl.Mode.INLINE) + protected VectorOperators.Comparison comparison_con() throws Throwable { + return (VectorOperators.Comparison) MUTABLE_COMPARISON_CONSTANT_HANDLE.invokeExact(); + } + + // Subclasses with different comparison operators + public static class IntegerComparisons extends MaskCompareNotBenchmark { + @Param({"EQ", "NE", "LT", "LE", "GT", "GE", "ULT", "ULE", "UGT", "UGE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotByte() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ByteVector bv = ByteVector.fromArray(B_SPECIES, bb, 0); + for (int j = 0; j < ARRAYLEN; j += B_SPECIES.length()) { + ByteVector av = ByteVector.fromArray(B_SPECIES, ba, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotShort() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ShortVector bv = ShortVector.fromArray(S_SPECIES, sb, 0); + for (int j = 0; j < ARRAYLEN; j += S_SPECIES.length()) { + ShortVector av = ShortVector.fromArray(S_SPECIES, sa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotInt() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + IntVector bv = IntVector.fromArray(I_SPECIES, ib, 0); + for (int j = 0; j < ARRAYLEN; j += I_SPECIES.length()) { + IntVector av = IntVector.fromArray(I_SPECIES, ia, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotLong() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + LongVector bv = LongVector.fromArray(L_SPECIES, lb, 0); + for (int j = 0; j < ARRAYLEN; j += L_SPECIES.length()) { + LongVector av = LongVector.fromArray(L_SPECIES, la, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } + + public static class FloatingPointComparisons extends MaskCompareNotBenchmark { + // "ULT", "ULE", "UGT", "UGE" are not supported for floating point types + @Param({"EQ", "NE", "LT", "LE", "GT", "GE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotFloat() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + FloatVector bv = FloatVector.fromArray(F_SPECIES, fb, 0); + for (int j = 0; j < ARRAYLEN; j += F_SPECIES.length()) { + FloatVector av = FloatVector.fromArray(F_SPECIES, fa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotDouble() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + DoubleVector bv = DoubleVector.fromArray(D_SPECIES, db, 0); + for (int j = 0; j < ARRAYLEN; j += D_SPECIES.length()) { + DoubleVector av = DoubleVector.fromArray(D_SPECIES, da, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } +} diff --git a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java index 4f97e12171f..1b89b510fae 100644 --- a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java +++ b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java @@ -1062,7 +1062,6 @@ public class ImageReaderBenchmark { "/modules/java.base/jdk/internal/loader/URLClassPath$FileLoader.class", "/modules/java.base/jdk/internal/loader/Resource.class", "/modules/java.base/java/io/FileCleanable.class", - "/modules/java.base/sun/nio/ByteBuffered.class", "/modules/java.base/java/security/SecureClassLoader$CodeSourceKey.class", "/modules/java.base/java/security/PermissionCollection.class", "/modules/java.base/java/security/Permissions.class", diff --git a/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java b/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java new file mode 100644 index 00000000000..965b940f417 --- /dev/null +++ b/test/micro/org/openjdk/bench/vm/compiler/SerialAdditions.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2025, Red Hat 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. + */ +package org.openjdk.bench.vm.compiler; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Tests speed of adding a series of additions of the same operand. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 4, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(value = 3) +public class SerialAdditions { + private int a = 0xBADB0BA; + private long b = 0x900dba51l; + + @Benchmark + public int addIntsTo02() { + return a + a; // baseline, still a + a + } + + @Benchmark + public int addIntsTo04() { + return a + a + a + a; // a*4 => a<<2 + } + + @Benchmark + public int addIntsTo05() { + return a + a + a + a + a; // a*5 => (a<<2) + a + } + + @Benchmark + public int addIntsTo06() { + return a + a + a + a + a + a; // a*6 => (a<<1) + (a<<2) + } + + @Benchmark + public int addIntsTo08() { + return a + a + a + a + a + a + a + a; // a*8 => a<<3 + } + + @Benchmark + public int addIntsTo16() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a; // a*16 => a<<4 + } + + @Benchmark + public int addIntsTo23() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a; // a*23 + } + + @Benchmark + public int addIntsTo32() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a; // a*32 => a<<5 + } + + @Benchmark + public int addIntsTo42() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a; // a*42 + } + + @Benchmark + public int addIntsTo64() { + return a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a + a + a + a + a + a + a // + + a + a + a + a; // 64 * a => a << 6 + } + + @Benchmark + public void addIntsMixed(Blackhole blackhole) { + blackhole.consume(addIntsTo02()); + blackhole.consume(addIntsTo04()); + blackhole.consume(addIntsTo05()); + blackhole.consume(addIntsTo06()); + blackhole.consume(addIntsTo08()); + blackhole.consume(addIntsTo16()); + blackhole.consume(addIntsTo23()); + blackhole.consume(addIntsTo32()); + blackhole.consume(addIntsTo42()); + blackhole.consume(addIntsTo64()); + } + + @Benchmark + public long addLongsTo02() { + return b + b; // baseline, still a + a + } + + @Benchmark + public long addLongsTo04() { + return b + b + b + b; // a*4 => a<<2 + } + + @Benchmark + public long addLongsTo05() { + return b + b + b + b + b; // a*5 => (a<<2) + a + } + + @Benchmark + public long addLongsTo06() { + return b + b + b + b + b + b; // a*6 => (a<<1) + (a<<2) + } + + @Benchmark + public long addLongsTo08() { + return b + b + b + b + b + b + b + b; // a*8 => a<<3 + } + + @Benchmark + public long addLongsTo16() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b; // a*16 => a<<4 + } + + @Benchmark + public long addLongsTo23() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b; // a*23 + } + + @Benchmark + public long addLongsTo32() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b; // a*32 => a<<5 + } + + @Benchmark + public long addLongsTo42() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b; // a*42 + } + + @Benchmark + public long addLongsTo64() { + return b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b + b + b + b + b + b + b // + + b + b + b + b; // 64 * a => a << 6 + } + + @Benchmark + public void addLongsMixed(Blackhole blackhole) { + blackhole.consume(addLongsTo02()); + blackhole.consume(addLongsTo04()); + blackhole.consume(addLongsTo05()); + blackhole.consume(addLongsTo06()); + blackhole.consume(addLongsTo08()); + blackhole.consume(addLongsTo16()); + blackhole.consume(addLongsTo23()); + blackhole.consume(addLongsTo32()); + blackhole.consume(addLongsTo42()); + blackhole.consume(addLongsTo64()); + } +}