You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2026/09/15 03:53:48 UTC
(httpcomponents-core) branch master updated: RFC 9651: Structured Field Values for HTTP
This is an automated email from the ASF dual-hosted git repository.
arturobernalg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git
The following commit(s) were added to refs/heads/master by this push:
new 0863485ae RFC 9651: Structured Field Values for HTTP
0863485ae is described below
commit 0863485ae85927df1e9a7f163bfbd7d7753806ca
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Wed Sep 2 18:48:28 2026 +0200
RFC 9651: Structured Field Values for HTTP
---
.../http/structured/StructuredFieldBareItem.java | 265 +
.../http/structured/StructuredFieldDictionary.java | 193 +
.../http/structured/StructuredFieldHeaders.java | 170 +
.../http/structured/StructuredFieldInnerList.java | 159 +
.../core5/http/structured/StructuredFieldItem.java | 177 +
.../core5/http/structured/StructuredFieldList.java | 138 +
.../http/structured/StructuredFieldMember.java | 42 +
.../http/structured/StructuredFieldParameters.java | 194 +
.../http/structured/StructuredFieldParser.java | 639 +
.../http/structured/StructuredFieldRules.java | 110 +
.../http/structured/StructuredFieldSerializer.java | 348 +
.../core5/http/structured/StructuredFieldType.java | 52 +
.../http/structured/StructuredFieldValue.java | 36 +
.../hc/core5/http/structured/package-info.java | 32 +
.../java/org/apache/hc/core5/net/PercentCodec.java | 31 +-
.../http/examples/StructuredFieldsExample.java | 106 +
.../structured/TestStructuredFieldConformance.java | 558 +
.../http/structured/TestStructuredFieldParser.java | 135 +
.../TestStructuredFieldRealWorldHeaders.java | 99 +
.../structured/TestStructuredFieldSerializer.java | 191 +
.../structured/TestStructuredFieldStrictness.java | 106 +
.../org/apache/hc/core5/net/TestPercentCodec.java | 10 +
.../hc/core5/http/structured/rfc9651/binary.json | 106 +
.../hc/core5/http/structured/rfc9651/boolean.json | 75 +
.../hc/core5/http/structured/rfc9651/date.json | 107 +
.../core5/http/structured/rfc9651/dictionary.json | 169 +
.../http/structured/rfc9651/display-string.json | 137 +
.../hc/core5/http/structured/rfc9651/examples.json | 208 +
.../hc/core5/http/structured/rfc9651/item.json | 34 +
.../http/structured/rfc9651/key-generated.json | 7013 +++++
.../http/structured/rfc9651/large-generated.json | 28819 +++++++++++++++++++
.../hc/core5/http/structured/rfc9651/list.json | 74 +
.../hc/core5/http/structured/rfc9651/listlist.json | 76 +
.../http/structured/rfc9651/number-generated.json | 2374 ++
.../hc/core5/http/structured/rfc9651/number.json | 255 +
.../core5/http/structured/rfc9651/param-dict.json | 113 +
.../core5/http/structured/rfc9651/param-list.json | 151 +
.../http/structured/rfc9651/param-listlist.json | 36 +
.../rfc9651/serialisation-tests/key-generated.json | 6239 ++++
.../rfc9651/serialisation-tests/number.json | 57 +
.../serialisation-tests/string-generated.json | 299 +
.../serialisation-tests/token-generated.json | 1490 +
.../http/structured/rfc9651/string-generated.json | 2335 ++
.../hc/core5/http/structured/rfc9651/string.json | 88 +
.../http/structured/rfc9651/token-generated.json | 2862 ++
.../hc/core5/http/structured/rfc9651/token.json | 38 +
46 files changed, 56938 insertions(+), 8 deletions(-)
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java
new file mode 100644
index 000000000..9d40aadea
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java
@@ -0,0 +1,265 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable Structured Field bare Item.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldBareItem {
+
+ private static final BigDecimal DECIMAL_ROUNDING_LIMIT = new BigDecimal("999999999999.9995");
+
+ private final StructuredFieldType type;
+ private final Object value;
+
+ private StructuredFieldBareItem(final StructuredFieldType type, final Object value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * Creates an Integer bare Item.
+ *
+ * @param value the integer value.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the value is outside the Structured Field range.
+ */
+ public static StructuredFieldBareItem ofInteger(final long value) {
+ checkInteger(value);
+ return new StructuredFieldBareItem(StructuredFieldType.INTEGER, Long.valueOf(value));
+ }
+
+ /**
+ * Creates a Decimal bare Item, applying the Structured Field rounding algorithm.
+ *
+ * @param value the decimal value.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the rounded value has more than 12 integer digits.
+ */
+ public static StructuredFieldBareItem ofDecimal(final BigDecimal value) {
+ Args.notNull(value, "Decimal value");
+ Args.check(value.abs().compareTo(DECIMAL_ROUNDING_LIMIT) < 0,
+ "Structured Field Decimal has more than 12 integer digits after rounding");
+ BigDecimal normalized = value.setScale(3, RoundingMode.HALF_EVEN).stripTrailingZeros();
+ if (normalized.scale() < 1) {
+ normalized = normalized.setScale(1);
+ }
+ final int integerDigits = Math.max(1, normalized.precision() - normalized.scale());
+ Args.check(integerDigits <= 12,
+ "Structured Field Decimal has more than 12 integer digits after rounding");
+ return new StructuredFieldBareItem(StructuredFieldType.DECIMAL, normalized);
+ }
+
+ /**
+ * Creates an ASCII String bare Item.
+ *
+ * @param value the printable ASCII value.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the value contains a character outside SP through {@code ~}.
+ */
+ public static StructuredFieldBareItem ofString(final String value) {
+ StructuredFieldRules.validateString(value);
+ return new StructuredFieldBareItem(StructuredFieldType.STRING, value);
+ }
+
+ /**
+ * Creates a Token bare Item.
+ *
+ * @param value the token value.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the value is not an Structured Field token.
+ */
+ public static StructuredFieldBareItem ofToken(final String value) {
+ StructuredFieldRules.validateToken(value);
+ return new StructuredFieldBareItem(StructuredFieldType.TOKEN, value);
+ }
+
+ /**
+ * Creates a Byte Sequence bare Item using a defensive copy.
+ *
+ * @param value the byte sequence.
+ * @return the bare Item.
+ */
+ public static StructuredFieldBareItem ofByteSequence(final byte[] value) {
+ Args.notNull(value, "Byte Sequence value");
+ return new StructuredFieldBareItem(StructuredFieldType.BYTE_SEQUENCE, value.clone());
+ }
+
+ /**
+ * Creates a Boolean bare Item.
+ *
+ * @param value the boolean value.
+ * @return the bare Item.
+ */
+ public static StructuredFieldBareItem ofBoolean(final boolean value) {
+ return new StructuredFieldBareItem(StructuredFieldType.BOOLEAN, Boolean.valueOf(value));
+ }
+
+ /**
+ * Creates a Date bare Item expressed as seconds since the Unix epoch.
+ *
+ * @param epochSeconds seconds since the Unix epoch.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the value is outside the Structured Field Integer range.
+ */
+ public static StructuredFieldBareItem ofDate(final long epochSeconds) {
+ checkInteger(epochSeconds);
+ return new StructuredFieldBareItem(StructuredFieldType.DATE, Long.valueOf(epochSeconds));
+ }
+
+ /**
+ * Creates a Unicode Display String bare Item.
+ *
+ * @param value the Unicode value.
+ * @return the bare Item.
+ * @throws IllegalArgumentException if the value contains an unpaired surrogate.
+ */
+ public static StructuredFieldBareItem ofDisplayString(final String value) {
+ StructuredFieldRules.validateDisplayString(value);
+ return new StructuredFieldBareItem(StructuredFieldType.DISPLAY_STRING, value);
+ }
+
+ private static void checkInteger(final long value) {
+ Args.check(value >= -StructuredFieldRules.MAX_INTEGER && value <= StructuredFieldRules.MAX_INTEGER,
+ "Structured Field Integer is outside the Structured Field range: %s", value);
+ }
+
+ /**
+ * @return the bare Item type.
+ */
+ public StructuredFieldType getType() {
+ return type;
+ }
+
+ /**
+ * Returns the value using its Java representation. Byte sequences are defensively copied.
+ *
+ * @return the value.
+ */
+ public Object getValue() {
+ return type == StructuredFieldType.BYTE_SEQUENCE ? ((byte[]) value).clone() : value;
+ }
+
+ /**
+ * @return the Integer or Date value.
+ * @throws IllegalStateException if this Item is not an Integer or Date.
+ */
+ public long getLongValue() {
+ requireType(StructuredFieldType.INTEGER, StructuredFieldType.DATE);
+ return ((Long) value).longValue();
+ }
+
+ /**
+ * @return the Decimal value.
+ * @throws IllegalStateException if this Item is not a Decimal.
+ */
+ public BigDecimal getDecimalValue() {
+ requireType(StructuredFieldType.DECIMAL);
+ return (BigDecimal) value;
+ }
+
+ /**
+ * @return the String, Token, or Display String value.
+ * @throws IllegalStateException if this Item is not one of those textual types.
+ */
+ public String getTextValue() {
+ requireType(StructuredFieldType.STRING, StructuredFieldType.TOKEN, StructuredFieldType.DISPLAY_STRING);
+ return (String) value;
+ }
+
+ /**
+ * @return a defensive copy of the Byte Sequence value.
+ * @throws IllegalStateException if this Item is not a Byte Sequence.
+ */
+ public byte[] getByteSequenceValue() {
+ requireType(StructuredFieldType.BYTE_SEQUENCE);
+ return ((byte[]) value).clone();
+ }
+
+ /**
+ * @return the Boolean value.
+ * @throws IllegalStateException if this Item is not a Boolean.
+ */
+ public boolean getBooleanValue() {
+ requireType(StructuredFieldType.BOOLEAN);
+ return ((Boolean) value).booleanValue();
+ }
+
+ private void requireType(final StructuredFieldType... expected) {
+ for (final StructuredFieldType candidate : expected) {
+ if (type == candidate) {
+ return;
+ }
+ }
+ throw new IllegalStateException("Bare Item is " + type);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldBareItem) {
+ final StructuredFieldBareItem that = (StructuredFieldBareItem) obj;
+ if (this.type != that.type) {
+ return false;
+ }
+ return this.type == StructuredFieldType.BYTE_SEQUENCE
+ ? Arrays.equals((byte[]) this.value, (byte[]) that.value)
+ : Objects.equals(this.value, that.value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.type);
+ hash = LangUtils.hashCode(hash, this.type == StructuredFieldType.BYTE_SEQUENCE
+ ? Arrays.hashCode((byte[]) this.value) : this.value.hashCode());
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return StructuredFieldSerializer.serializeBareItem(this);
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java
new file mode 100644
index 000000000..239c13a5c
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java
@@ -0,0 +1,193 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable, ordered Structured Field Dictionary.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldDictionary
+ implements StructuredFieldValue, Iterable<Map.Entry<String, StructuredFieldMember>> {
+
+ private final List<Map.Entry<String, StructuredFieldMember>> entries;
+ private final Map<String, StructuredFieldMember> values;
+
+ private StructuredFieldDictionary(final Map<String, StructuredFieldMember> source) {
+ final List<Map.Entry<String, StructuredFieldMember>> copy = new ArrayList<>(source.size());
+ final Map<String, StructuredFieldMember> lookup = new LinkedHashMap<>();
+ for (final Map.Entry<String, StructuredFieldMember> entry : source.entrySet()) {
+ StructuredFieldRules.validateKey(entry.getKey());
+ final StructuredFieldMember value = Args.notNull(entry.getValue(), "Dictionary member");
+ copy.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), value));
+ lookup.put(entry.getKey(), value);
+ }
+ this.entries = Collections.unmodifiableList(copy);
+ this.values = Collections.unmodifiableMap(lookup);
+ }
+
+ /**
+ * @return a new ordered Dictionary builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ static StructuredFieldDictionary copyOf(final Map<String, StructuredFieldMember> source) {
+ return new StructuredFieldDictionary(source);
+ }
+
+ /**
+ * @return the number of members.
+ */
+ public int size() {
+ return entries.size();
+ }
+
+ /**
+ * @return whether the Dictionary has no members.
+ */
+ public boolean isEmpty() {
+ return entries.isEmpty();
+ }
+
+ /**
+ * Looks up a member by key.
+ *
+ * @param key the member key.
+ * @return the member, or {@code null} when absent.
+ */
+ public StructuredFieldMember get(final String key) {
+ return values.get(key);
+ }
+
+ /**
+ * @param index the zero-based ordered index.
+ * @return the member key at the index.
+ */
+ public String getName(final int index) {
+ return entries.get(index).getKey();
+ }
+
+ /**
+ * @param index the zero-based ordered index.
+ * @return the member value at the index.
+ */
+ public StructuredFieldMember getValue(final int index) {
+ return entries.get(index).getValue();
+ }
+
+ /**
+ * @return an unmodifiable, insertion-ordered map view.
+ */
+ public Map<String, StructuredFieldMember> asMap() {
+ return values;
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, StructuredFieldMember>> iterator() {
+ return entries.iterator();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldDictionary) {
+ final StructuredFieldDictionary that = (StructuredFieldDictionary) obj;
+ return Objects.equals(this.entries, that.entries);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.entries);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ final String value = StructuredFieldSerializer.serializeDictionary(this);
+ return value != null ? value : "";
+ }
+
+ /** Builds immutable, insertion-ordered Dictionaries. */
+ public static final class Builder {
+
+ private final LinkedHashMap<String, StructuredFieldMember> values = new LinkedHashMap<>();
+
+ /**
+ * Adds or replaces a member. Replacing a key preserves its original position.
+ *
+ * @param key the member key.
+ * @param value the member value.
+ * @return this builder.
+ */
+ public Builder put(final String key, final StructuredFieldMember value) {
+ StructuredFieldRules.validateKey(key);
+ values.put(key, Args.notNull(value, "Dictionary member"));
+ return this;
+ }
+
+ /**
+ * Adds or replaces a bare Item member.
+ *
+ * @param key the member key.
+ * @param value the bare Item value.
+ * @return this builder.
+ */
+ public Builder put(final String key, final StructuredFieldBareItem value) {
+ return put(key, StructuredFieldItem.of(value));
+ }
+
+ /**
+ * @return an immutable Dictionary.
+ */
+ public StructuredFieldDictionary build() {
+ return StructuredFieldDictionary.copyOf(values);
+ }
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java
new file mode 100644
index 000000000..205a3eae9
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java
@@ -0,0 +1,170 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.MessageHeaders;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.message.BufferedHeader;
+import org.apache.hc.core5.http.message.MessageSupport;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
+
+/**
+ * Integration between Structured Field values and HttpComponents message headers.
+ *
+ * @since 5.5
+ */
+public final class StructuredFieldHeaders {
+
+ private StructuredFieldHeaders() {
+ }
+
+ /**
+ * Parses one header as an Structured Field Item.
+ *
+ * @param header the header.
+ * @return the parsed Item.
+ * @throws ParseException if the complete field value is invalid.
+ */
+ public static StructuredFieldItem parseItem(final Header header) throws ParseException {
+ return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseItem);
+ }
+
+ /**
+ * Parses the field named {@code name} as a single Structured Field Item. An Item is a single
+ * value and cannot span field lines, so more than one matching field line is rejected.
+ *
+ * @param headers the message headers.
+ * @param name the case-insensitive field name.
+ * @return the parsed Item.
+ * @throws ParseException if the field is absent, spans multiple field lines, or is invalid.
+ */
+ public static StructuredFieldItem parseItem(final MessageHeaders headers, final String name)
+ throws ParseException {
+ Args.notNull(headers, "Message headers");
+ Args.notBlank(name, "Header name");
+ final Iterator<Header> matching = headers.headerIterator(name);
+ if (!matching.hasNext()) {
+ throw new ParseException("Missing " + name + " field");
+ }
+ final Header header = matching.next();
+ if (matching.hasNext()) {
+ throw new ParseException(name + " Item must not span multiple field lines");
+ }
+ return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseItem);
+ }
+
+ /**
+ * Parses one header as an Structured Field List.
+ *
+ * @param header the header.
+ * @return the parsed List.
+ * @throws ParseException if the complete field value is invalid.
+ */
+ public static StructuredFieldList parseList(final Header header) throws ParseException {
+ return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseList);
+ }
+
+ /**
+ * Parses the matching field lines as a Structured Field List, reading each field line in place
+ * and appending its members, without combining the values into a new buffer.
+ *
+ * @param headers the message headers.
+ * @param name the case-insensitive field name.
+ * @return the parsed List, empty when the field is absent.
+ * @throws ParseException if any field line is invalid.
+ */
+ public static StructuredFieldList parseList(final MessageHeaders headers, final String name)
+ throws ParseException {
+ Args.notNull(headers, "Message headers");
+ Args.notBlank(name, "Header name");
+ final List<StructuredFieldMember> members = new ArrayList<>();
+ MessageSupport.parseElementListStrict(headers, name, (buffer, cursor) ->
+ members.add(StructuredFieldParser.parseListElement(buffer, cursor)));
+ return StructuredFieldList.of(members);
+ }
+
+ /**
+ * Parses one header as an Structured Field Dictionary.
+ *
+ * @param header the header.
+ * @return the parsed Dictionary.
+ * @throws ParseException if the complete field value is invalid.
+ */
+ public static StructuredFieldDictionary parseDictionary(final Header header) throws ParseException {
+ return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseDictionary);
+ }
+
+ /**
+ * Parses the matching field lines as a Structured Field Dictionary, reading each field line in
+ * place and merging its members, without combining the values into a new buffer. A repeated key
+ * keeps its last value.
+ *
+ * @param headers the message headers.
+ * @param name the case-insensitive field name.
+ * @return the parsed Dictionary, empty when the field is absent.
+ * @throws ParseException if any field line is invalid.
+ */
+ public static StructuredFieldDictionary parseDictionary(final MessageHeaders headers, final String name)
+ throws ParseException {
+ Args.notNull(headers, "Message headers");
+ Args.notBlank(name, "Header name");
+ final Map<String, StructuredFieldMember> members = new LinkedHashMap<>();
+ MessageSupport.parseElementListStrict(headers, name, (buffer, cursor) ->
+ StructuredFieldParser.parseDictionaryElement(buffer, cursor, members));
+ return StructuredFieldDictionary.copyOf(members);
+ }
+
+ /**
+ * Creates a header for a Structured Field value.
+ *
+ * @param name the field name.
+ * @param value the Structured Field value.
+ * @return a header, or {@code null} for an empty List or Dictionary.
+ */
+ public static Header format(final String name, final StructuredFieldValue value) {
+ Args.notBlank(name, "Header name");
+ Args.notNull(value, "Structured Field value");
+ if (value instanceof StructuredFieldList && ((StructuredFieldList) value).isEmpty()
+ || value instanceof StructuredFieldDictionary && ((StructuredFieldDictionary) value).isEmpty()) {
+ return null;
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(name.length() + 66);
+ buffer.append(name);
+ buffer.append(": ");
+ StructuredFieldSerializer.serialize(buffer, value);
+ return BufferedHeader.create(buffer);
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java
new file mode 100644
index 000000000..74a7794fc
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java
@@ -0,0 +1,159 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable Structured Field Inner List.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldInnerList implements StructuredFieldMember, Iterable<StructuredFieldItem> {
+
+ private final List<StructuredFieldItem> items;
+ private final StructuredFieldParameters parameters;
+
+ private StructuredFieldInnerList(
+ final List<StructuredFieldItem> items, final StructuredFieldParameters parameters) {
+ final ArrayList<StructuredFieldItem> copy = new ArrayList<>(items.size());
+ for (final StructuredFieldItem item : items) {
+ copy.add(Args.notNull(item, "Inner List Item"));
+ }
+ this.items = Collections.unmodifiableList(copy);
+ this.parameters = Args.notNull(parameters, "Parameters");
+ }
+
+ /**
+ * Creates an Inner List without parameters.
+ *
+ * @param items the ordered Items.
+ * @return the immutable Inner List.
+ */
+ public static StructuredFieldInnerList of(final List<StructuredFieldItem> items) {
+ return new StructuredFieldInnerList(items, StructuredFieldParameters.EMPTY);
+ }
+
+ /**
+ * Creates an Inner List without parameters.
+ *
+ * @param items the ordered Items.
+ * @return the immutable Inner List.
+ */
+ public static StructuredFieldInnerList of(final StructuredFieldItem... items) {
+ return of(Arrays.asList(items));
+ }
+
+ /**
+ * Creates an Inner List with parameters.
+ *
+ * @param items the ordered Items.
+ * @param parameters the Inner List parameters.
+ * @return the immutable Inner List.
+ */
+ public static StructuredFieldInnerList of(
+ final List<StructuredFieldItem> items, final StructuredFieldParameters parameters) {
+ return new StructuredFieldInnerList(items, parameters);
+ }
+
+ /**
+ * @return the number of Items.
+ */
+ public int size() {
+ return items.size();
+ }
+
+ /**
+ * @return whether the Inner List has no Items.
+ */
+ public boolean isEmpty() {
+ return items.isEmpty();
+ }
+
+ /**
+ * @param index the zero-based index.
+ * @return the Item at the index.
+ */
+ public StructuredFieldItem get(final int index) {
+ return items.get(index);
+ }
+
+ /**
+ * @return an unmodifiable ordered Item list.
+ */
+ public List<StructuredFieldItem> getItems() {
+ return items;
+ }
+
+ @Override
+ public StructuredFieldParameters getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public Iterator<StructuredFieldItem> iterator() {
+ return items.iterator();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldInnerList) {
+ final StructuredFieldInnerList that = (StructuredFieldInnerList) obj;
+ return Objects.equals(this.items, that.items)
+ && Objects.equals(this.parameters, that.parameters);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.items);
+ hash = LangUtils.hashCode(hash, this.parameters);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return StructuredFieldSerializer.serializeInnerList(this);
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java
new file mode 100644
index 000000000..862caa0ae
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java
@@ -0,0 +1,177 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable Structured Field Item.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldItem implements StructuredFieldValue, StructuredFieldMember {
+
+ private final StructuredFieldBareItem bareItem;
+ private final StructuredFieldParameters parameters;
+
+ private StructuredFieldItem(final StructuredFieldBareItem bareItem, final StructuredFieldParameters parameters) {
+ this.bareItem = Args.notNull(bareItem, "Bare Item");
+ this.parameters = Args.notNull(parameters, "Parameters");
+ }
+
+ /**
+ * Creates an Item without parameters.
+ *
+ * @param bareItem the bare Item value.
+ * @return the immutable Item.
+ */
+ public static StructuredFieldItem of(final StructuredFieldBareItem bareItem) {
+ return new StructuredFieldItem(bareItem, StructuredFieldParameters.EMPTY);
+ }
+
+ /**
+ * @param value the Integer value.
+ * @return an Integer Item without parameters.
+ */
+ public static StructuredFieldItem ofInteger(final long value) {
+ return of(StructuredFieldBareItem.ofInteger(value));
+ }
+
+ /**
+ * @param value the Decimal value.
+ * @return a Decimal Item without parameters.
+ */
+ public static StructuredFieldItem ofDecimal(final BigDecimal value) {
+ return of(StructuredFieldBareItem.ofDecimal(value));
+ }
+
+ /**
+ * @param value the printable ASCII String value.
+ * @return a String Item without parameters.
+ */
+ public static StructuredFieldItem ofString(final String value) {
+ return of(StructuredFieldBareItem.ofString(value));
+ }
+
+ /**
+ * @param value the Token value.
+ * @return a Token Item without parameters.
+ */
+ public static StructuredFieldItem ofToken(final String value) {
+ return of(StructuredFieldBareItem.ofToken(value));
+ }
+
+ /**
+ * @param value the Byte Sequence value.
+ * @return a Byte Sequence Item without parameters.
+ */
+ public static StructuredFieldItem ofByteSequence(final byte[] value) {
+ return of(StructuredFieldBareItem.ofByteSequence(value));
+ }
+
+ /**
+ * @param value the Boolean value.
+ * @return a Boolean Item without parameters.
+ */
+ public static StructuredFieldItem ofBoolean(final boolean value) {
+ return of(StructuredFieldBareItem.ofBoolean(value));
+ }
+
+ /**
+ * @param epochSeconds seconds since the Unix epoch.
+ * @return a Date Item without parameters.
+ */
+ public static StructuredFieldItem ofDate(final long epochSeconds) {
+ return of(StructuredFieldBareItem.ofDate(epochSeconds));
+ }
+
+ /**
+ * @param value the Unicode Display String value.
+ * @return a Display String Item without parameters.
+ */
+ public static StructuredFieldItem ofDisplayString(final String value) {
+ return of(StructuredFieldBareItem.ofDisplayString(value));
+ }
+
+ /**
+ * Creates an Item with parameters.
+ *
+ * @param bareItem the bare Item value.
+ * @param parameters the ordered parameters.
+ * @return the immutable Item.
+ */
+ public static StructuredFieldItem of(
+ final StructuredFieldBareItem bareItem, final StructuredFieldParameters parameters) {
+ return new StructuredFieldItem(bareItem, parameters);
+ }
+
+ /**
+ * @return the bare Item value.
+ */
+ public StructuredFieldBareItem getBareItem() {
+ return bareItem;
+ }
+
+ @Override
+ public StructuredFieldParameters getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldItem) {
+ final StructuredFieldItem that = (StructuredFieldItem) obj;
+ return Objects.equals(this.bareItem, that.bareItem)
+ && Objects.equals(this.parameters, that.parameters);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.bareItem);
+ hash = LangUtils.hashCode(hash, this.parameters);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return StructuredFieldSerializer.serializeItem(this);
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java
new file mode 100644
index 000000000..5ae96b25d
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java
@@ -0,0 +1,138 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable Structured Field List.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldList implements StructuredFieldValue, Iterable<StructuredFieldMember> {
+
+ private final List<StructuredFieldMember> members;
+
+ private StructuredFieldList(final List<? extends StructuredFieldMember> members) {
+ final ArrayList<StructuredFieldMember> copy = new ArrayList<>(members.size());
+ for (final StructuredFieldMember member : members) {
+ copy.add(Args.notNull(member, "List member"));
+ }
+ this.members = Collections.unmodifiableList(copy);
+ }
+
+ /**
+ * Creates a List from ordered members.
+ *
+ * @param members the ordered members.
+ * @return the immutable List.
+ */
+ public static StructuredFieldList of(final List<? extends StructuredFieldMember> members) {
+ return new StructuredFieldList(members);
+ }
+
+ /**
+ * Creates a List from ordered members.
+ *
+ * @param members the ordered members.
+ * @return the immutable List.
+ */
+ public static StructuredFieldList of(final StructuredFieldMember... members) {
+ return of(Arrays.asList(members));
+ }
+
+ /**
+ * @return the number of members.
+ */
+ public int size() {
+ return members.size();
+ }
+
+ /**
+ * @return whether the List has no members.
+ */
+ public boolean isEmpty() {
+ return members.isEmpty();
+ }
+
+ /**
+ * @param index the zero-based index.
+ * @return the member at the index.
+ */
+ public StructuredFieldMember get(final int index) {
+ return members.get(index);
+ }
+
+ /**
+ * @return an unmodifiable ordered member list.
+ */
+ public List<StructuredFieldMember> getMembers() {
+ return members;
+ }
+
+ @Override
+ public Iterator<StructuredFieldMember> iterator() {
+ return members.iterator();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldList) {
+ final StructuredFieldList that = (StructuredFieldList) obj;
+ return Objects.equals(this.members, that.members);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.members);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ final String value = StructuredFieldSerializer.serializeList(this);
+ return value != null ? value : "";
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java
new file mode 100644
index 000000000..308d9e02e
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+/**
+ * A member of an Structured Field List or Dictionary: an Item or Inner List.
+ *
+ * @since 5.5
+ */
+public interface StructuredFieldMember {
+
+ /**
+ * @return the ordered parameters associated with this member.
+ */
+ StructuredFieldParameters getParameters();
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java
new file mode 100644
index 000000000..1b9781e9c
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java
@@ -0,0 +1,194 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
+
+/**
+ * Immutable, ordered Structured Field Parameters map.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldParameters implements Iterable<Map.Entry<String, StructuredFieldBareItem>> {
+
+ /** Empty parameter map. */
+ public static final StructuredFieldParameters EMPTY = new StructuredFieldParameters(Collections.emptyMap());
+
+ private final List<Map.Entry<String, StructuredFieldBareItem>> entries;
+ private final Map<String, StructuredFieldBareItem> values;
+
+ private StructuredFieldParameters(final Map<String, StructuredFieldBareItem> source) {
+ final List<Map.Entry<String, StructuredFieldBareItem>> copy = new ArrayList<>(source.size());
+ final Map<String, StructuredFieldBareItem> lookup = new LinkedHashMap<>();
+ for (final Map.Entry<String, StructuredFieldBareItem> entry : source.entrySet()) {
+ StructuredFieldRules.validateKey(entry.getKey());
+ final StructuredFieldBareItem value = Args.notNull(entry.getValue(), "Parameter value");
+ copy.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), value));
+ lookup.put(entry.getKey(), value);
+ }
+ this.entries = Collections.unmodifiableList(copy);
+ this.values = Collections.unmodifiableMap(lookup);
+ }
+
+ /**
+ * @return a new ordered Parameters builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ static StructuredFieldParameters copyOf(final Map<String, StructuredFieldBareItem> source) {
+ return source.isEmpty() ? EMPTY : new StructuredFieldParameters(source);
+ }
+
+ /**
+ * @return the number of parameters.
+ */
+ public int size() {
+ return entries.size();
+ }
+
+ /**
+ * @return whether there are no parameters.
+ */
+ public boolean isEmpty() {
+ return entries.isEmpty();
+ }
+
+ /**
+ * Looks up a parameter by key.
+ *
+ * @param key the parameter key.
+ * @return the bare Item value, or {@code null} when absent.
+ */
+ public StructuredFieldBareItem get(final String key) {
+ return values.get(key);
+ }
+
+ /**
+ * @param index the zero-based ordered index.
+ * @return the parameter key at the index.
+ */
+ public String getName(final int index) {
+ return entries.get(index).getKey();
+ }
+
+ /**
+ * @param index the zero-based ordered index.
+ * @return the parameter value at the index.
+ */
+ public StructuredFieldBareItem getValue(final int index) {
+ return entries.get(index).getValue();
+ }
+
+ /**
+ * @return an unmodifiable, insertion-ordered map view.
+ */
+ public Map<String, StructuredFieldBareItem> asMap() {
+ return values;
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, StructuredFieldBareItem>> iterator() {
+ return entries.iterator();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StructuredFieldParameters) {
+ final StructuredFieldParameters that = (StructuredFieldParameters) obj;
+ return Objects.equals(this.entries, that.entries);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.entries);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return StructuredFieldSerializer.serializeParameters(this);
+ }
+
+ /** Builds immutable, insertion-ordered Parameters maps. */
+ public static final class Builder {
+
+ private final LinkedHashMap<String, StructuredFieldBareItem> values = new LinkedHashMap<>();
+
+ /**
+ * Adds or replaces a parameter. Replacing a key preserves its original position.
+ *
+ * @param key the parameter key.
+ * @param value the bare Item value.
+ * @return this builder.
+ */
+ public Builder put(final String key, final StructuredFieldBareItem value) {
+ StructuredFieldRules.validateKey(key);
+ values.put(key, Args.notNull(value, "Parameter value"));
+ return this;
+ }
+
+ /**
+ * Adds or replaces a Boolean parameter.
+ *
+ * @param key the parameter key.
+ * @param value the boolean value.
+ * @return this builder.
+ */
+ public Builder putBoolean(final String key, final boolean value) {
+ return put(key, StructuredFieldBareItem.ofBoolean(value));
+ }
+
+ /**
+ * @return an immutable Parameters map.
+ */
+ public StructuredFieldParameters build() {
+ return StructuredFieldParameters.copyOf(values);
+ }
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java
new file mode 100644
index 000000000..a35011cd1
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java
@@ -0,0 +1,639 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ByteArrayBuffer;
+import org.apache.hc.core5.util.CharArrayBuffer;
+import org.apache.hc.core5.util.Tokenizer;
+
+/**
+ * Strict parser for Structured Field Structured Field Values.
+ *
+ * @since 5.5
+ */
+final class StructuredFieldParser {
+
+ private static final Tokenizer TOKENIZER = Tokenizer.INSTANCE;
+ private static final Tokenizer.Delimiter KEY_TERMINATOR =
+ ch -> !StructuredFieldRules.isKeyChar(ch);
+ private static final Tokenizer.Delimiter TOKEN_TERMINATOR =
+ ch -> !StructuredFieldRules.isTokenChar(ch);
+
+ private final CharSequence input;
+ private final Tokenizer.Cursor cursor;
+
+ private StructuredFieldParser(final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+ this.input = Args.notNull(input, "Structured Field input");
+ this.cursor = Args.notNull(cursor, "Parser cursor");
+ if (cursor.getUpperBound() > input.length()) {
+ throw new IllegalArgumentException("Parser cursor exceeds Structured Field input length");
+ }
+ }
+
+ /**
+ * Parses a complete Structured Field Item field value.
+ *
+ * @param input the field value.
+ * @return the parsed Item.
+ * @throws ParseException if the complete value is invalid.
+ */
+ static StructuredFieldItem parseItem(final CharSequence input) throws ParseException {
+ Args.notNull(input, "Structured Field input");
+ return parseItem(input, new Tokenizer.Cursor(0, input.length()));
+ }
+
+ /**
+ * Parses an Item from the current cursor position through its upper bound.
+ *
+ * @param input the character sequence containing the field value.
+ * @param cursor the bounds and current parse position.
+ * @return the parsed Item.
+ * @throws ParseException if the complete bounded value is invalid.
+ */
+ static StructuredFieldItem parseItem(
+ final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+ final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+ parser.skipSpaces();
+ final StructuredFieldItem result = parser.parseItemValue();
+ parser.skipSpaces();
+ parser.requireEnd();
+ return result;
+ }
+
+ /**
+ * Parses a complete Structured Field List field value.
+ *
+ * @param input the field value.
+ * @return the parsed List.
+ * @throws ParseException if the complete value is invalid.
+ */
+ static StructuredFieldList parseList(final CharSequence input) throws ParseException {
+ Args.notNull(input, "Structured Field input");
+ return parseList(input, new Tokenizer.Cursor(0, input.length()));
+ }
+
+ /**
+ * Parses a List from the current cursor position through its upper bound.
+ *
+ * @param input the character sequence containing the field value.
+ * @param cursor the bounds and current parse position.
+ * @return the parsed List.
+ * @throws ParseException if the complete bounded value is invalid.
+ */
+ static StructuredFieldList parseList(
+ final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+ final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+ parser.skipSpaces();
+ final StructuredFieldList result = parser.parseListValue();
+ parser.skipSpaces();
+ parser.requireEnd();
+ return result;
+ }
+
+ /**
+ * Parses a complete Structured Field Dictionary field value.
+ *
+ * @param input the field value.
+ * @return the parsed Dictionary.
+ * @throws ParseException if the complete value is invalid.
+ */
+ static StructuredFieldDictionary parseDictionary(final CharSequence input) throws ParseException {
+ Args.notNull(input, "Structured Field input");
+ return parseDictionary(input, new Tokenizer.Cursor(0, input.length()));
+ }
+
+ /**
+ * Parses a Dictionary from the current cursor position through its upper bound.
+ *
+ * @param input the character sequence containing the field value.
+ * @param cursor the bounds and current parse position.
+ * @return the parsed Dictionary.
+ * @throws ParseException if the complete bounded value is invalid.
+ */
+ static StructuredFieldDictionary parseDictionary(
+ final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+ final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+ parser.skipSpaces();
+ final StructuredFieldDictionary result = parser.parseDictionaryValue();
+ parser.skipSpaces();
+ parser.requireEnd();
+ return result;
+ }
+
+ /**
+ * Parses a single List member at the current cursor position, consuming the surrounding optional
+ * whitespace so that the cursor stops at the following comma or the upper bound. One field line
+ * element is parsed in place, without copying the underlying data.
+ *
+ * @param input the character sequence containing the field line value.
+ * @param cursor the bounds and current parse position.
+ * @return the parsed member.
+ * @throws ParseException if the member is invalid or is not comma-separated.
+ */
+ static StructuredFieldMember parseListElement(final CharSequence input, final Tokenizer.Cursor cursor)
+ throws ParseException {
+ final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+ parser.skipOptionalWhitespace();
+ final StructuredFieldMember member = parser.parseMember();
+ parser.requireElementSeparator();
+ return member;
+ }
+
+ /**
+ * Parses a single Dictionary entry at the current cursor position into {@code accumulator},
+ * consuming the surrounding optional whitespace so that the cursor stops at the following comma or
+ * the upper bound; a repeated key keeps its last value. One field line element is parsed in place,
+ * without copying the underlying data.
+ *
+ * @param input the character sequence containing the field line value.
+ * @param cursor the bounds and current parse position.
+ * @param accumulator the map into which the parsed entry is merged.
+ * @throws ParseException if the entry is invalid or is not comma-separated.
+ */
+ static void parseDictionaryElement(final CharSequence input, final Tokenizer.Cursor cursor,
+ final Map<String, StructuredFieldMember> accumulator) throws ParseException {
+ final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+ parser.skipOptionalWhitespace();
+ final String key = parser.parseKey();
+ final StructuredFieldMember member;
+ if (parser.peek('=')) {
+ parser.advance();
+ member = parser.parseMember();
+ } else {
+ member = StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parser.parseParameters());
+ }
+ accumulator.put(key, member);
+ parser.requireElementSeparator();
+ }
+
+ /**
+ * Combines field lines with a comma and parses an Structured Field Item.
+ *
+ * @param fieldValues the field line values.
+ * @return the parsed Item.
+ * @throws ParseException if the combined field value is invalid.
+ */
+ static StructuredFieldItem parseItemLines(final Iterable<? extends CharSequence> fieldValues)
+ throws ParseException {
+ return parseItem(combine(fieldValues));
+ }
+
+ /**
+ * Combines field lines with a comma and parses an Structured Field List.
+ *
+ * @param fieldValues the field line values.
+ * @return the parsed List.
+ * @throws ParseException if the combined field value is invalid.
+ */
+ static StructuredFieldList parseListLines(final Iterable<? extends CharSequence> fieldValues)
+ throws ParseException {
+ return parseList(combine(fieldValues));
+ }
+
+ /**
+ * Combines field lines with a comma and parses an Structured Field Dictionary.
+ *
+ * @param fieldValues the field line values.
+ * @return the parsed Dictionary.
+ * @throws ParseException if the combined field value is invalid.
+ */
+ static StructuredFieldDictionary parseDictionaryLines(
+ final Iterable<? extends CharSequence> fieldValues) throws ParseException {
+ return parseDictionary(combine(fieldValues));
+ }
+
+ private static String combine(final Iterable<? extends CharSequence> fieldValues) {
+ Args.notNull(fieldValues, "Field values");
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ boolean first = true;
+ for (final CharSequence fieldValue : fieldValues) {
+ if (!first) {
+ buffer.append(", ");
+ }
+ buffer.append(Args.notNull(fieldValue, "Field value").toString());
+ first = false;
+ }
+ return buffer.toString();
+ }
+
+ private StructuredFieldList parseListValue() throws ParseException {
+ final List<StructuredFieldMember> members = new ArrayList<>();
+ while (!atEnd()) {
+ members.add(parseMember());
+ skipOptionalWhitespace();
+ if (atEnd()) {
+ break;
+ }
+ require(',');
+ skipOptionalWhitespace();
+ if (atEnd()) {
+ throw error("Trailing comma in Structured Field List");
+ }
+ }
+ return StructuredFieldList.of(members);
+ }
+
+ private StructuredFieldDictionary parseDictionaryValue() throws ParseException {
+ final LinkedHashMap<String, StructuredFieldMember> members = new LinkedHashMap<>();
+ while (!atEnd()) {
+ final String key = parseKey();
+ final StructuredFieldMember member;
+ if (peek('=')) {
+ advance();
+ member = parseMember();
+ } else {
+ member = StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parseParameters());
+ }
+ members.put(key, member);
+ skipOptionalWhitespace();
+ if (atEnd()) {
+ break;
+ }
+ require(',');
+ skipOptionalWhitespace();
+ if (atEnd()) {
+ throw error("Trailing comma in Structured Field Dictionary");
+ }
+ }
+ return StructuredFieldDictionary.copyOf(members);
+ }
+
+ private void requireElementSeparator() throws ParseException {
+ skipOptionalWhitespace();
+ if (!atEnd() && !peek(',')) {
+ throw error("Structured Field members must be separated by a comma");
+ }
+ }
+
+ private StructuredFieldMember parseMember() throws ParseException {
+ return peek('(') ? parseInnerList() : parseItemValue();
+ }
+
+ private StructuredFieldInnerList parseInnerList() throws ParseException {
+ require('(');
+ final List<StructuredFieldItem> items = new ArrayList<>();
+ while (!atEnd()) {
+ skipSpaces();
+ if (peek(')')) {
+ advance();
+ return StructuredFieldInnerList.of(items, parseParameters());
+ }
+ items.add(parseItemValue());
+ if (atEnd() || current() != Tokenizer.SP && current() != ')') {
+ throw error("Inner List Items must be separated by SP");
+ }
+ }
+ throw error("Unterminated Structured Field Inner List");
+ }
+
+ private StructuredFieldItem parseItemValue() throws ParseException {
+ return StructuredFieldItem.of(parseBareItem(), parseParameters());
+ }
+
+ private StructuredFieldParameters parseParameters() throws ParseException {
+ final Map<String, StructuredFieldBareItem> parameters = new LinkedHashMap<>();
+ while (peek(';')) {
+ advance();
+ skipSpaces();
+ final String key = parseKey();
+ final StructuredFieldBareItem value;
+ if (peek('=')) {
+ advance();
+ value = parseBareItem();
+ } else {
+ value = StructuredFieldBareItem.ofBoolean(true);
+ }
+ parameters.put(key, value);
+ }
+ return StructuredFieldParameters.copyOf(parameters);
+ }
+
+ private String parseKey() throws ParseException {
+ if (atEnd() || !StructuredFieldRules.isKeyStart(current())) {
+ throw error("Invalid Structured Field key");
+ }
+ return TOKENIZER.parseContent(input, cursor, KEY_TERMINATOR);
+ }
+
+ private StructuredFieldBareItem parseBareItem() throws ParseException {
+ if (atEnd()) {
+ throw error("Missing Structured Field bare Item");
+ }
+ final char ch = current();
+ if (ch == '-' || StructuredFieldRules.isDigit(ch)) {
+ return parseNumber();
+ }
+ if (ch == Tokenizer.DQUOTE) {
+ return StructuredFieldBareItem.ofString(parseString());
+ }
+ if (StructuredFieldRules.isAlpha(ch) || ch == '*') {
+ return StructuredFieldBareItem.ofToken(parseToken());
+ }
+ if (ch == ':') {
+ return StructuredFieldBareItem.ofByteSequence(parseByteSequence());
+ }
+ if (ch == '?') {
+ return StructuredFieldBareItem.ofBoolean(parseBoolean());
+ }
+ if (ch == '@') {
+ advance();
+ final StructuredFieldBareItem value = parseNumber();
+ if (value.getType() != StructuredFieldType.INTEGER) {
+ throw error("Structured Field Date must be an Integer");
+ }
+ return StructuredFieldBareItem.ofDate(value.getLongValue());
+ }
+ if (ch == '%') {
+ return StructuredFieldBareItem.ofDisplayString(parseDisplayString());
+ }
+ throw error("Unrecognized Structured Field bare Item");
+ }
+
+ private StructuredFieldBareItem parseNumber() throws ParseException {
+ boolean negative = false;
+ if (peek('-')) {
+ negative = true;
+ advance();
+ }
+ if (atEnd() || !StructuredFieldRules.isDigit(current())) {
+ throw error("Invalid Structured Field number");
+ }
+ final int digitsStart = position();
+ int integerDigits = 0;
+ while (!atEnd() && StructuredFieldRules.isDigit(current())) {
+ advance();
+ if (++integerDigits > 15) {
+ throw error("Structured Field Integer has more than 15 digits");
+ }
+ }
+ if (!peek('.')) {
+ final String number = subsequence(digitsStart, position());
+ final long value;
+ try {
+ value = Long.parseLong(number);
+ } catch (final NumberFormatException ex) {
+ throw error("Invalid Structured Field Integer");
+ }
+ return StructuredFieldBareItem.ofInteger(negative ? -value : value);
+ }
+ if (integerDigits > 12) {
+ throw error("Structured Field Decimal has more than 12 integer digits");
+ }
+ advance();
+ final int fractionStart = position();
+ while (!atEnd() && StructuredFieldRules.isDigit(current())) {
+ advance();
+ if (position() - fractionStart > 3) {
+ throw error("Structured Field Decimal has more than 3 fractional digits");
+ }
+ }
+ if (position() == fractionStart) {
+ throw error("Structured Field Decimal has no fractional digits");
+ }
+ final int numberStart = negative ? digitsStart - 1 : digitsStart;
+ return StructuredFieldBareItem.ofDecimal(new BigDecimal(subsequence(numberStart, position())));
+ }
+
+ private String parseString() throws ParseException {
+ require(Tokenizer.DQUOTE);
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ while (!atEnd()) {
+ final char ch = current();
+ advance();
+ if (ch == Tokenizer.ESCAPE) {
+ if (atEnd()) {
+ throw error("Incomplete escape in Structured Field String");
+ }
+ final char escaped = current();
+ advance();
+ if (escaped != Tokenizer.DQUOTE && escaped != Tokenizer.ESCAPE) {
+ throw error("Invalid escape in Structured Field String");
+ }
+ buffer.append(escaped);
+ } else if (ch == Tokenizer.DQUOTE) {
+ return buffer.toString();
+ } else if (ch < 0x20 || ch > 0x7e) {
+ throw error("Invalid character in Structured Field String");
+ } else {
+ buffer.append(ch);
+ }
+ }
+ throw error("Unterminated Structured Field String");
+ }
+
+ private String parseToken() {
+ return TOKENIZER.parseContent(input, cursor, TOKEN_TERMINATOR);
+ }
+
+ private byte[] parseByteSequence() throws ParseException {
+ require(':');
+ final int start = position();
+ while (!atEnd() && current() != ':') {
+ final char ch = current();
+ if (!StructuredFieldRules.isAlpha(ch) && !StructuredFieldRules.isDigit(ch)
+ && ch != '+' && ch != '/' && ch != '=') {
+ throw error("Invalid character in Structured Field Byte Sequence");
+ }
+ advance();
+ }
+ if (atEnd()) {
+ throw error("Unterminated Structured Field Byte Sequence");
+ }
+ final String encoded = subsequence(start, position());
+ advance();
+ final String padded = padBase64(encoded);
+ try {
+ return Base64.getDecoder().decode(padded);
+ } catch (final IllegalArgumentException ex) {
+ throw error("Invalid base64 in Structured Field Byte Sequence");
+ }
+ }
+
+ private String padBase64(final String encoded) throws ParseException {
+ final int paddingStart = encoded.indexOf('=');
+ final int contentLength = paddingStart >= 0 ? paddingStart : encoded.length();
+ for (int i = contentLength; i < encoded.length(); i++) {
+ if (encoded.charAt(i) != '=') {
+ throw error("Invalid padding in Structured Field Byte Sequence");
+ }
+ }
+ final int remainder = contentLength % 4;
+ if (remainder == 1) {
+ throw error("Invalid Structured Field Byte Sequence length");
+ }
+ final int requiredPadding = remainder == 0 ? 0 : 4 - remainder;
+ final int suppliedPadding = encoded.length() - contentLength;
+ if (suppliedPadding > requiredPadding) {
+ throw error("Invalid padding in Structured Field Byte Sequence");
+ }
+ if (suppliedPadding == requiredPadding) {
+ return encoded;
+ }
+ final StringBuilder buffer = new StringBuilder(encoded);
+ for (int i = suppliedPadding; i < requiredPadding; i++) {
+ buffer.append('=');
+ }
+ return buffer.toString();
+ }
+
+ private boolean parseBoolean() throws ParseException {
+ require('?');
+ if (atEnd()) {
+ throw error("Missing Structured Field Boolean value");
+ }
+ final char ch = current();
+ advance();
+ if (ch == '1') {
+ return true;
+ }
+ if (ch == '0') {
+ return false;
+ }
+ throw error("Invalid Structured Field Boolean");
+ }
+
+ private String parseDisplayString() throws ParseException {
+ require('%');
+ require(Tokenizer.DQUOTE);
+ final ByteArrayBuffer bytes = new ByteArrayBuffer(64);
+ while (!atEnd()) {
+ final char ch = current();
+ advance();
+ if (ch < 0x20 || ch > 0x7e) {
+ throw error("Invalid character in Structured Field Display String");
+ }
+ if (ch == Tokenizer.DQUOTE) {
+ try {
+ return StandardCharsets.UTF_8.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .decode(ByteBuffer.wrap(bytes.array(), 0, bytes.length())).toString();
+ } catch (final CharacterCodingException ex) {
+ throw error("Invalid UTF-8 in Structured Field Display String");
+ }
+ }
+ if (ch == '%') {
+ if (position() + 2 > cursor.getUpperBound()) {
+ throw error("Incomplete percent escape in Structured Field Display String");
+ }
+ final int high = lowerHex(current());
+ advance();
+ final int low = lowerHex(current());
+ advance();
+ if (high < 0 || low < 0) {
+ throw error("Display String percent escapes must use lowercase hexadecimal");
+ }
+ bytes.append(high << 4 | low);
+ } else {
+ bytes.append(ch);
+ }
+ }
+ throw error("Unterminated Structured Field Display String");
+ }
+
+ private static int lowerHex(final char ch) {
+ if (ch <= 0x7f && Character.isDigit(ch)) {
+ return ch - '0';
+ }
+ if (ch >= 'a' && ch <= 'f') {
+ return ch - 'a' + 10;
+ }
+ return -1;
+ }
+
+ private void skipSpaces() {
+ while (peek((char) Tokenizer.SP)) {
+ advance();
+ }
+ }
+
+ private void skipOptionalWhitespace() {
+ while (!atEnd() && (current() == Tokenizer.SP || current() == Tokenizer.HT)) {
+ advance();
+ }
+ }
+
+ private boolean peek(final char ch) {
+ return !atEnd() && current() == ch;
+ }
+
+ private void require(final char ch) throws ParseException {
+ if (!peek(ch)) {
+ throw error("Expected '" + ch + "'");
+ }
+ advance();
+ }
+
+ private void requireEnd() throws ParseException {
+ if (!atEnd()) {
+ throw error("Trailing data after Structured Field value");
+ }
+ }
+
+ private char current() {
+ return input.charAt(position());
+ }
+
+ private int position() {
+ return cursor.getPos();
+ }
+
+ private void advance() {
+ cursor.updatePos(position() + 1);
+ }
+
+ private String subsequence(final int start, final int end) {
+ return input.subSequence(start, end).toString();
+ }
+
+ private boolean atEnd() {
+ return cursor.atEnd();
+ }
+
+ private ParseException error(final String description) {
+ return new ParseException(
+ description,
+ input,
+ cursor.getLowerBound(),
+ cursor.getUpperBound() - cursor.getLowerBound(),
+ position());
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java
new file mode 100644
index 000000000..cd259dec7
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java
@@ -0,0 +1,110 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+final class StructuredFieldRules {
+
+ static final long MAX_INTEGER = 999999999999999L;
+
+ private StructuredFieldRules() {
+ }
+
+ static boolean isAlpha(final char ch) {
+ return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z';
+ }
+
+ static boolean isLowerAlpha(final char ch) {
+ return ch >= 'a' && ch <= 'z';
+ }
+
+ static boolean isDigit(final char ch) {
+ return ch <= 0x7f && Character.isDigit(ch);
+ }
+
+ static boolean isKeyStart(final char ch) {
+ return isLowerAlpha(ch) || ch == '*';
+ }
+
+ static boolean isKeyChar(final char ch) {
+ return isLowerAlpha(ch) || isDigit(ch) || ch == '_' || ch == '-' || ch == '.' || ch == '*';
+ }
+
+ static boolean isTokenChar(final char ch) {
+ return isAlpha(ch) || isDigit(ch) || "!#$%&'*+-.^_`|~:/".indexOf(ch) >= 0;
+ }
+
+ static void validateKey(final String key) {
+ if (key == null || key.isEmpty() || !isKeyStart(key.charAt(0))) {
+ throw new IllegalArgumentException("Invalid Structured Field key: " + key);
+ }
+ for (int i = 1; i < key.length(); i++) {
+ if (!isKeyChar(key.charAt(i))) {
+ throw new IllegalArgumentException("Invalid Structured Field key: " + key);
+ }
+ }
+ }
+
+ static void validateToken(final String token) {
+ if (token == null || token.isEmpty() || !(isAlpha(token.charAt(0)) || token.charAt(0) == '*')) {
+ throw new IllegalArgumentException("Invalid Structured Field token: " + token);
+ }
+ for (int i = 1; i < token.length(); i++) {
+ if (!isTokenChar(token.charAt(i))) {
+ throw new IllegalArgumentException("Invalid Structured Field token: " + token);
+ }
+ }
+ }
+
+ static void validateString(final String value) {
+ if (value == null) {
+ throw new NullPointerException("String value");
+ }
+ for (int i = 0; i < value.length(); i++) {
+ final char ch = value.charAt(i);
+ if (ch < 0x20 || ch > 0x7e) {
+ throw new IllegalArgumentException("Structured Field String must contain printable ASCII only");
+ }
+ }
+ }
+
+ static void validateDisplayString(final String value) {
+ if (value == null) {
+ throw new NullPointerException("Display String value");
+ }
+ for (int i = 0; i < value.length(); i++) {
+ final char ch = value.charAt(i);
+ if (Character.isHighSurrogate(ch)) {
+ if (++i >= value.length() || !Character.isLowSurrogate(value.charAt(i))) {
+ throw new IllegalArgumentException("Display String contains an unpaired surrogate");
+ }
+ } else if (Character.isLowSurrogate(ch)) {
+ throw new IllegalArgumentException("Display String contains an unpaired surrogate");
+ }
+ }
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java
new file mode 100644
index 000000000..b988fe465
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java
@@ -0,0 +1,348 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.util.Base64;
+import java.util.BitSet;
+import java.util.Map;
+
+import org.apache.hc.core5.net.PercentCodec;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
+
+/**
+ * Canonical serializer for Structured Field Structured Field Values.
+ *
+ * @since 5.5
+ */
+public final class StructuredFieldSerializer {
+
+ private static final PercentCodec DISPLAY_STRING_CODEC;
+
+ static {
+ final BitSet safeChars = new BitSet(256);
+ safeChars.set(0x20, 0x7f);
+ safeChars.clear('"');
+ safeChars.clear('%');
+ DISPLAY_STRING_CODEC = new PercentCodec(safeChars, true);
+ }
+
+ private StructuredFieldSerializer() {
+ }
+
+ /**
+ * Serializes a top-level field value.
+ *
+ * @param value the Structured Field value.
+ * @return the field value, or {@code null} for an empty List or Dictionary,
+ * which Structured Field represents by omitting the field.
+ */
+ public static String serialize(final StructuredFieldValue value) {
+ Args.notNull(value, "Structured Field value");
+ if (isEmpty(value)) {
+ return null;
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ serialize(buffer, value);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends the canonical serialization of a top-level Structured Field value.
+ * Empty Lists and Dictionaries append no characters because Structured Field
+ * represents them by omitting the field.
+ *
+ * @param buffer the destination buffer.
+ * @param value the Structured Field value.
+ */
+ public static void serialize(final CharArrayBuffer buffer, final StructuredFieldValue value) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(value, "Structured Field value");
+ if (value instanceof StructuredFieldItem) {
+ serializeItem(buffer, (StructuredFieldItem) value);
+ } else if (value instanceof StructuredFieldList) {
+ serializeList(buffer, (StructuredFieldList) value);
+ } else if (value instanceof StructuredFieldDictionary) {
+ serializeDictionary(buffer, (StructuredFieldDictionary) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported Structured Field value: " + value.getClass());
+ }
+ }
+
+ /**
+ * @param item the Item.
+ * @return the canonical serialized Item.
+ */
+ public static String serializeItem(final StructuredFieldItem item) {
+ Args.notNull(item, "Item");
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ serializeItem(buffer, item);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends a canonical serialized Item.
+ *
+ * @param buffer the destination buffer.
+ * @param item the Item.
+ */
+ public static void serializeItem(final CharArrayBuffer buffer, final StructuredFieldItem item) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(item, "Item");
+ serializeBareItem(buffer, item.getBareItem());
+ serializeParameters(buffer, item.getParameters());
+ }
+
+ /**
+ * @param list the List.
+ * @return the canonical serialized List, or {@code null} when empty.
+ */
+ public static String serializeList(final StructuredFieldList list) {
+ Args.notNull(list, "List");
+ if (list.isEmpty()) {
+ return null;
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ serializeList(buffer, list);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends a canonical serialized List. An empty List appends no characters.
+ *
+ * @param buffer the destination buffer.
+ * @param list the List.
+ */
+ public static void serializeList(final CharArrayBuffer buffer, final StructuredFieldList list) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(list, "List");
+ for (int i = 0; i < list.size(); i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ serializeMember(buffer, list.get(i));
+ }
+ }
+
+ /**
+ * @param dictionary the Dictionary.
+ * @return the canonical serialized Dictionary, or {@code null} when empty.
+ */
+ public static String serializeDictionary(final StructuredFieldDictionary dictionary) {
+ Args.notNull(dictionary, "Dictionary");
+ if (dictionary.isEmpty()) {
+ return null;
+ }
+ final CharArrayBuffer buffer = new CharArrayBuffer(64);
+ serializeDictionary(buffer, dictionary);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends a canonical serialized Dictionary. An empty Dictionary appends no characters.
+ *
+ * @param buffer the destination buffer.
+ * @param dictionary the Dictionary.
+ */
+ public static void serializeDictionary(
+ final CharArrayBuffer buffer, final StructuredFieldDictionary dictionary) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(dictionary, "Dictionary");
+ int index = 0;
+ for (final Map.Entry<String, StructuredFieldMember> entry : dictionary) {
+ if (index++ > 0) {
+ buffer.append(", ");
+ }
+ buffer.append(entry.getKey());
+ final StructuredFieldMember member = entry.getValue();
+ if (member instanceof StructuredFieldItem
+ && isTrue(((StructuredFieldItem) member).getBareItem())) {
+ serializeParameters(buffer, member.getParameters());
+ } else {
+ buffer.append('=');
+ serializeMember(buffer, member);
+ }
+ }
+ }
+
+ /**
+ * @param innerList the Inner List.
+ * @return the canonical serialized Inner List.
+ */
+ public static String serializeInnerList(final StructuredFieldInnerList innerList) {
+ Args.notNull(innerList, "Inner List");
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ serializeInnerList(buffer, innerList);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends a canonical serialized Inner List.
+ *
+ * @param buffer the destination buffer.
+ * @param innerList the Inner List.
+ */
+ public static void serializeInnerList(
+ final CharArrayBuffer buffer, final StructuredFieldInnerList innerList) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(innerList, "Inner List");
+ buffer.append('(');
+ for (int i = 0; i < innerList.size(); i++) {
+ if (i > 0) {
+ buffer.append(' ');
+ }
+ serializeItem(buffer, innerList.get(i));
+ }
+ buffer.append(')');
+ serializeParameters(buffer, innerList.getParameters());
+ }
+
+ /**
+ * @param parameters the Parameters map.
+ * @return the canonical serialized parameters.
+ */
+ public static String serializeParameters(final StructuredFieldParameters parameters) {
+ Args.notNull(parameters, "Parameters");
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ serializeParameters(buffer, parameters);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends canonical serialized parameters.
+ *
+ * @param buffer the destination buffer.
+ * @param parameters the Parameters map.
+ */
+ public static void serializeParameters(
+ final CharArrayBuffer buffer, final StructuredFieldParameters parameters) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(parameters, "Parameters");
+ for (final Map.Entry<String, StructuredFieldBareItem> entry : parameters) {
+ buffer.append(';');
+ buffer.append(entry.getKey());
+ if (!isTrue(entry.getValue())) {
+ buffer.append('=');
+ serializeBareItem(buffer, entry.getValue());
+ }
+ }
+ }
+
+ /**
+ * @param item the bare Item.
+ * @return the canonical serialized bare Item.
+ */
+ public static String serializeBareItem(final StructuredFieldBareItem item) {
+ Args.notNull(item, "Bare Item");
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ serializeBareItem(buffer, item);
+ return buffer.toString();
+ }
+
+ /**
+ * Appends a canonical serialized bare Item.
+ *
+ * @param buffer the destination buffer.
+ * @param item the bare Item.
+ */
+ public static void serializeBareItem(final CharArrayBuffer buffer, final StructuredFieldBareItem item) {
+ Args.notNull(buffer, "Destination");
+ Args.notNull(item, "Bare Item");
+ switch (item.getType()) {
+ case INTEGER:
+ buffer.append(Long.toString(item.getLongValue()));
+ break;
+ case DECIMAL:
+ buffer.append(item.getDecimalValue().toPlainString());
+ break;
+ case STRING:
+ serializeString(buffer, item.getTextValue());
+ break;
+ case TOKEN:
+ buffer.append(item.getTextValue());
+ break;
+ case BYTE_SEQUENCE:
+ buffer.append(':');
+ buffer.append(Base64.getEncoder().encodeToString(item.getByteSequenceValue()));
+ buffer.append(':');
+ break;
+ case BOOLEAN:
+ buffer.append(item.getBooleanValue() ? "?1" : "?0");
+ break;
+ case DATE:
+ buffer.append('@');
+ buffer.append(Long.toString(item.getLongValue()));
+ break;
+ case DISPLAY_STRING:
+ serializeDisplayString(buffer, item.getTextValue());
+ break;
+ default:
+ throw new IllegalStateException("Unsupported bare Item type: " + item.getType());
+ }
+ }
+
+ private static void serializeMember(final CharArrayBuffer buffer, final StructuredFieldMember member) {
+ if (member instanceof StructuredFieldItem) {
+ serializeItem(buffer, (StructuredFieldItem) member);
+ } else if (member instanceof StructuredFieldInnerList) {
+ serializeInnerList(buffer, (StructuredFieldInnerList) member);
+ } else {
+ throw new IllegalArgumentException("Unsupported Structured Field member: " + member.getClass());
+ }
+ }
+
+ private static boolean isTrue(final StructuredFieldBareItem value) {
+ return value.getType() == StructuredFieldType.BOOLEAN && value.getBooleanValue();
+ }
+
+ private static void serializeString(final CharArrayBuffer buffer, final String value) {
+ buffer.append('"');
+ for (int i = 0; i < value.length(); i++) {
+ final char ch = value.charAt(i);
+ if (ch == '"' || ch == '\\') {
+ buffer.append('\\');
+ }
+ buffer.append(ch);
+ }
+ buffer.append('"');
+ }
+
+ private static void serializeDisplayString(final CharArrayBuffer buffer, final String value) {
+ buffer.append('%');
+ buffer.append('"');
+ final StringBuilder encoded = new StringBuilder(value.length());
+ DISPLAY_STRING_CODEC.encode(encoded, value);
+ buffer.append(encoded);
+ buffer.append('"');
+ }
+
+ private static boolean isEmpty(final StructuredFieldValue value) {
+ return value instanceof StructuredFieldList && ((StructuredFieldList) value).isEmpty()
+ || value instanceof StructuredFieldDictionary && ((StructuredFieldDictionary) value).isEmpty();
+ }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java
new file mode 100644
index 000000000..deab912f5
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+/**
+ * Structured Field bare Item types.
+ *
+ * @since 5.5
+ */
+public enum StructuredFieldType {
+ /** Integer. */
+ INTEGER,
+ /** Decimal. */
+ DECIMAL,
+ /** Printable ASCII String. */
+ STRING,
+ /** Token. */
+ TOKEN,
+ /** Byte Sequence. */
+ BYTE_SEQUENCE,
+ /** Boolean. */
+ BOOLEAN,
+ /** Date expressed as seconds since the Unix epoch. */
+ DATE,
+ /** Unicode Display String. */
+ DISPLAY_STRING
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java
new file mode 100644
index 000000000..406f5128d
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java
@@ -0,0 +1,36 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+/**
+ * Marker interface for the three Structured Field top-level field value types.
+ *
+ * @since 5.5
+ */
+public interface StructuredFieldValue {
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java
new file mode 100644
index 000000000..b537bef46
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+/**
+ * Immutable data model, strict parser, canonical serializer, and message-header
+ * integration for Structured Field Structured Field Values for HTTP.
+ */
+package org.apache.hc.core5.http.structured;
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
index 2a7f30b38..e81c667f4 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
@@ -175,6 +175,11 @@ public class PercentCodec {
static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
final BitSet safechars, final boolean blankAsPlus) {
+ encode(buf, content, charset, safechars, blankAsPlus, false);
+ }
+
+ private static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
+ final BitSet safechars, final boolean blankAsPlus, final boolean lowerCaseHex) {
if (content == null) {
return;
}
@@ -188,10 +193,10 @@ static void encode(final StringBuilder buf, final CharSequence content, final Ch
buf.append("+");
} else {
buf.append("%");
- final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
- final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
- buf.append(hex1);
- buf.append(hex2);
+ final char hex1 = Character.forDigit((b >> 4) & 0xF, RADIX);
+ final char hex2 = Character.forDigit(b & 0xF, RADIX);
+ buf.append(lowerCaseHex ? hex1 : Character.toUpperCase(hex1));
+ buf.append(lowerCaseHex ? hex2 : Character.toUpperCase(hex2));
}
}
}
@@ -256,21 +261,31 @@ public static String decode(final CharSequence content, final Charset charset) {
public static final PercentCodec HTTP_TOKEN = new PercentCodec(HTTP_TOKEN_UNRESERVED);
private final BitSet unreserved;
+ private final boolean lowerCaseHex;
+ /**
+ * @since 5.5
+ */
@Internal
- public PercentCodec(final BitSet unreserved) {
+ public PercentCodec(final BitSet unreserved, final boolean lowerCaseHex) {
this.unreserved = unreserved;
+ this.lowerCaseHex = lowerCaseHex;
+ }
+
+ @Internal
+ public PercentCodec(final BitSet unreserved) {
+ this(unreserved, false);
}
public PercentCodec() {
- this.unreserved = UNRESERVED;
+ this(UNRESERVED, false);
}
/**
* @since 5.3
*/
public void encode(final StringBuilder buf, final CharSequence content) {
- encode(buf, content, StandardCharsets.UTF_8, unreserved, false);
+ encode(buf, content, StandardCharsets.UTF_8, unreserved, false, lowerCaseHex);
}
/**
@@ -281,7 +296,7 @@ public String encode(final CharSequence content) {
return null;
}
final StringBuilder buf = new StringBuilder();
- encode(buf, content, StandardCharsets.UTF_8, unreserved, false);
+ encode(buf, content, StandardCharsets.UTF_8, unreserved, false, lowerCaseHex);
return buf.toString();
}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java
new file mode 100644
index 000000000..6f86cba1f
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.examples;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.HeaderGroup;
+import org.apache.hc.core5.http.structured.StructuredFieldDictionary;
+import org.apache.hc.core5.http.structured.StructuredFieldHeaders;
+import org.apache.hc.core5.http.structured.StructuredFieldInnerList;
+import org.apache.hc.core5.http.structured.StructuredFieldItem;
+import org.apache.hc.core5.http.structured.StructuredFieldList;
+
+/**
+ * Creates and consumes real HTTP fields that use the RFC 9651 data model.
+ */
+public final class StructuredFieldsExample {
+
+ public static void main(final String... args) throws Exception {
+ contentDigestRoundTrip();
+ parsePriority();
+ parseCacheStatus();
+ parseSignatureInput();
+ }
+
+ /** RFC 9530 Content-Digest sender and receiver round trip. */
+ private static void contentDigestRoundTrip() throws Exception {
+ final byte[] body = "{\"message\":\"hello\"}".getBytes(StandardCharsets.UTF_8);
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(body);
+
+ final StructuredFieldDictionary contentDigest = StructuredFieldDictionary.builder()
+ .put("sha-256", StructuredFieldItem.ofByteSequence(digest))
+ .build();
+ final Header header = StructuredFieldHeaders.format("Content-Digest", contentDigest);
+ System.out.println(header);
+
+ final HeaderGroup receivedHeaders = new HeaderGroup();
+ receivedHeaders.addHeader(header);
+ final StructuredFieldItem receivedDigest = (StructuredFieldItem)
+ StructuredFieldHeaders.parseDictionary(receivedHeaders, "Content-Digest").get("sha-256");
+ final boolean valid = MessageDigest.isEqual(digest, receivedDigest.getBareItem().getByteSequenceValue());
+ System.out.println("Digest valid: " + valid);
+ }
+
+ /** RFC 9218 Priority is a Dictionary Structured Field. */
+ private static void parsePriority() throws Exception {
+ final StructuredFieldDictionary priority = StructuredFieldHeaders.parseDictionary(
+ new BasicHeader("Priority", "u=0, i"));
+ final long urgency = ((StructuredFieldItem) priority.get("u")).getBareItem().getLongValue();
+ final boolean incremental = ((StructuredFieldItem) priority.get("i"))
+ .getBareItem().getBooleanValue();
+ System.out.println("Priority urgency=" + urgency + ", incremental=" + incremental);
+ }
+
+ /** RFC 9211 Cache-Status is a List Structured Field. */
+ private static void parseCacheStatus() throws Exception {
+ final StructuredFieldList cacheStatus = StructuredFieldHeaders.parseList(
+ new BasicHeader("Cache-Status", "ExampleCache;hit;ttl=376"));
+ final StructuredFieldItem cache = (StructuredFieldItem) cacheStatus.get(0);
+ System.out.println("Cache " + cache.getBareItem().getTextValue()
+ + " hit=" + cache.getParameters().get("hit").getBooleanValue()
+ + " ttl=" + cache.getParameters().get("ttl").getLongValue());
+ }
+
+ /** RFC 9421 Signature-Input is a Dictionary Structured Field. */
+ private static void parseSignatureInput() throws Exception {
+ final Header header = new BasicHeader("Signature-Input",
+ "sig1=(\"@method\" \"@target-uri\" \"content-digest\")"
+ + ";created=1618884475;keyid=\"test-key-rsa-pss\"");
+ final StructuredFieldInnerList signature = (StructuredFieldInnerList)
+ StructuredFieldHeaders.parseDictionary(header).get("sig1");
+ System.out.println("Signature covers " + signature.size()
+ + " components using key " + signature.getParameters().get("keyid").getTextValue());
+ }
+
+ private StructuredFieldsExample() {
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java
new file mode 100644
index 000000000..9f13c2272
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java
@@ -0,0 +1,558 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.ParseException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Runs the complete HTTP Working Group Structured Field test corpus.
+ *
+ * <p>The corpus is revision 1e280c3ed9ffe0ca5fdb1d97219dddc389007677
+ * from <a href="https://github.com/httpwg/structured-field-tests">...</a>.</p>
+ */
+class TestStructuredFieldConformance {
+
+ private static final String ROOT = "/org/apache/hc/core5/http/structured/rfc9651/";
+
+ private static final String[] PARSING_FILES = {
+ "binary.json",
+ "boolean.json",
+ "date.json",
+ "dictionary.json",
+ "display-string.json",
+ "examples.json",
+ "item.json",
+ "key-generated.json",
+ "large-generated.json",
+ "list.json",
+ "listlist.json",
+ "number-generated.json",
+ "number.json",
+ "param-dict.json",
+ "param-list.json",
+ "param-listlist.json",
+ "string-generated.json",
+ "string.json",
+ "token-generated.json",
+ "token.json"
+ };
+
+ private static final String[] SERIALIZATION_FILES = {
+ "serialisation-tests/key-generated.json",
+ "serialisation-tests/number.json",
+ "serialisation-tests/string-generated.json",
+ "serialisation-tests/token-generated.json"
+ };
+
+ @Test
+ void testOfficialCorpus() throws Exception {
+ final List<String> failures = new ArrayList<>();
+ int count = 0;
+ for (final String file : PARSING_FILES) {
+ count += runFile(file, true, failures);
+ }
+ for (final String file : SERIALIZATION_FILES) {
+ count += runFile(file, false, failures);
+ }
+ Assertions.assertEquals(2135, count, "Unexpected number of HTTPWG test cases");
+ if (!failures.isEmpty()) {
+ Assertions.fail(failures.size() + " HTTPWG conformance failure(s):\n" + String.join("\n", failures));
+ }
+ }
+
+ private static int runFile(
+ final String file, final boolean parsing, final List<String> failures) throws IOException {
+ final Object document;
+ try (InputStream inputStream = TestStructuredFieldConformance.class.getResourceAsStream(ROOT + file)) {
+ if (inputStream == null) {
+ throw new IOException("Missing test corpus resource " + file);
+ }
+ document = new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).parse();
+ }
+ final List<?> tests = list(document);
+ for (final Object value : tests) {
+ final Map<?, ?> test = map(value);
+ final String name = string(test.get("name"));
+ try {
+ if (parsing) {
+ runParsingCase(test);
+ } else {
+ runSerializationCase(test);
+ }
+ } catch (final Exception ex) {
+ if (!booleanValue(test.get("must_fail")) && !booleanValue(test.get("can_fail"))) {
+ failures.add(file + " :: " + name + " :: unexpected "
+ + ex.getClass().getSimpleName() + ": " + ex.getMessage());
+ }
+ } catch (final AssertionError ex) {
+ failures.add(file + " :: " + name + " :: " + ex.getMessage());
+ }
+ }
+ return tests.size();
+ }
+
+ private static void runParsingCase(final Map<?, ?> test) throws Exception {
+ final boolean mustFail = booleanValue(test.get("must_fail"));
+ try {
+ final StructuredFieldValue actual = parse(
+ string(test.get("header_type")), strings(test.get("raw")));
+ if (mustFail) {
+ throw new AssertionError("parsing succeeded but must_fail is true");
+ }
+ final StructuredFieldValue expected = value(
+ string(test.get("header_type")), test.get("expected"));
+ if (!expected.equals(actual)) {
+ throw new AssertionError("expected " + expected + " but parsed " + actual);
+ }
+ verifyCanonical(test, actual);
+ } catch (final ParseException | IllegalArgumentException ex) {
+ if (!mustFail) {
+ throw ex;
+ }
+ }
+ }
+
+ private static void runSerializationCase(final Map<?, ?> test) {
+ final boolean mustFail = booleanValue(test.get("must_fail"));
+ try {
+ final StructuredFieldValue value = value(
+ string(test.get("header_type")), test.get("expected"));
+ final String actual = StructuredFieldSerializer.serialize(value);
+ if (mustFail) {
+ throw new AssertionError("serialization succeeded but must_fail is true: " + actual);
+ }
+ verifyCanonical(test, value);
+ } catch (final IllegalArgumentException ex) {
+ if (!mustFail) {
+ throw ex;
+ }
+ }
+ }
+
+ private static void verifyCanonical(final Map<?, ?> test, final StructuredFieldValue value) {
+ final Object canonical = test.containsKey("canonical") ? test.get("canonical") : test.get("raw");
+ final List<String> fieldLines = strings(canonical);
+ final String expected = fieldLines.isEmpty() ? null : String.join(", ", fieldLines);
+ final String actual = StructuredFieldSerializer.serialize(value);
+ if (expected == null ? actual != null : !expected.equals(actual)) {
+ throw new AssertionError("expected canonical " + expected + " but serialized " + actual);
+ }
+ }
+
+ private static StructuredFieldValue parse(final String type, final List<String> lines) throws ParseException {
+ switch (type) {
+ case "item":
+ return StructuredFieldParser.parseItemLines(lines);
+ case "list":
+ return StructuredFieldParser.parseListLines(lines);
+ case "dictionary":
+ return StructuredFieldParser.parseDictionaryLines(lines);
+ default:
+ throw new IllegalArgumentException("Unknown header type " + type);
+ }
+ }
+
+ private static StructuredFieldValue value(final String type, final Object expected) {
+ switch (type) {
+ case "item":
+ return item(expected);
+ case "list":
+ return structuredList(expected);
+ case "dictionary":
+ return dictionary(expected);
+ default:
+ throw new IllegalArgumentException("Unknown header type " + type);
+ }
+ }
+
+ private static StructuredFieldList structuredList(final Object value) {
+ final List<StructuredFieldMember> members = new ArrayList<>();
+ for (final Object member : list(value)) {
+ members.add(member(member));
+ }
+ return StructuredFieldList.of(members);
+ }
+
+ private static StructuredFieldDictionary dictionary(final Object value) {
+ final StructuredFieldDictionary.Builder builder = StructuredFieldDictionary.builder();
+ for (final Object entry : list(value)) {
+ final List<?> pair = list(entry);
+ builder.put(string(pair.get(0)), member(pair.get(1)));
+ }
+ return builder.build();
+ }
+
+ private static StructuredFieldMember member(final Object value) {
+ final List<?> pair = list(value);
+ if (pair.get(0) instanceof List<?>) {
+ final List<StructuredFieldItem> items = new ArrayList<>();
+ for (final Object item : list(pair.get(0))) {
+ items.add(item(item));
+ }
+ return StructuredFieldInnerList.of(items, parameters(pair.get(1)));
+ }
+ return item(value);
+ }
+
+ private static StructuredFieldItem item(final Object value) {
+ final List<?> pair = list(value);
+ return StructuredFieldItem.of(bareItem(pair.get(0)), parameters(pair.get(1)));
+ }
+
+ private static StructuredFieldParameters parameters(final Object value) {
+ final StructuredFieldParameters.Builder builder = StructuredFieldParameters.builder();
+ for (final Object parameter : list(value)) {
+ final List<?> pair = list(parameter);
+ builder.put(string(pair.get(0)), bareItem(pair.get(1)));
+ }
+ return builder.build();
+ }
+
+ private static StructuredFieldBareItem bareItem(final Object value) {
+ if (value instanceof Boolean) {
+ return StructuredFieldBareItem.ofBoolean(((Boolean) value).booleanValue());
+ }
+ if (value instanceof Long) {
+ return StructuredFieldBareItem.ofInteger(((Long) value).longValue());
+ }
+ if (value instanceof BigDecimal) {
+ return StructuredFieldBareItem.ofDecimal((BigDecimal) value);
+ }
+ if (value instanceof String) {
+ return StructuredFieldBareItem.ofString((String) value);
+ }
+ final Map<?, ?> typed = map(value);
+ final String type = string(typed.get("__type"));
+ switch (type) {
+ case "token":
+ return StructuredFieldBareItem.ofToken(string(typed.get("value")));
+ case "binary":
+ return StructuredFieldBareItem.ofByteSequence(decodeBase32(string(typed.get("value"))));
+ case "date":
+ return StructuredFieldBareItem.ofDate(((Long) typed.get("value")).longValue());
+ case "displaystring":
+ return StructuredFieldBareItem.ofDisplayString(string(typed.get("value")));
+ default:
+ throw new IllegalArgumentException("Unknown bare Item type " + type);
+ }
+ }
+
+ private static byte[] decodeBase32(final String encoded) {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ int bits = 0;
+ int buffer = 0;
+ for (int i = 0; i < encoded.length(); i++) {
+ final char ch = encoded.charAt(i);
+ if (ch == '=') {
+ break;
+ }
+ final int value = ch >= 'A' && ch <= 'Z' ? ch - 'A'
+ : ch >= '2' && ch <= '7' ? ch - '2' + 26 : -1;
+ if (value < 0) {
+ throw new IllegalArgumentException("Invalid base32 character " + ch);
+ }
+ buffer = buffer << 5 | value;
+ bits += 5;
+ if (bits >= 8) {
+ output.write(buffer >> (bits - 8) & 0xff);
+ bits -= 8;
+ }
+ }
+ return output.toByteArray();
+ }
+
+ private static boolean booleanValue(final Object value) {
+ return value instanceof Boolean && ((Boolean) value).booleanValue();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<Object> list(final Object value) {
+ if (!(value instanceof List<?>)) {
+ throw new IllegalArgumentException("Expected JSON array but got " + value);
+ }
+ return (List<Object>) value;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<Object, Object> map(final Object value) {
+ if (!(value instanceof Map<?, ?>)) {
+ throw new IllegalArgumentException("Expected JSON object but got " + value);
+ }
+ return (Map<Object, Object>) value;
+ }
+
+ private static String string(final Object value) {
+ if (!(value instanceof String)) {
+ throw new IllegalArgumentException("Expected JSON string but got " + value);
+ }
+ return (String) value;
+ }
+
+ private static List<String> strings(final Object value) {
+ final List<String> strings = new ArrayList<>();
+ for (final Object element : list(value)) {
+ strings.add(string(element));
+ }
+ return strings;
+ }
+
+ private static final class JsonReader {
+
+ private final Reader reader;
+ private int current = -2;
+
+ JsonReader(final Reader reader) {
+ this.reader = reader;
+ }
+
+ Object parse() throws IOException {
+ final Object value = readValue();
+ skipWhitespace();
+ if (peek() != -1) {
+ throw error("Trailing JSON data");
+ }
+ return value;
+ }
+
+ private Object readValue() throws IOException {
+ skipWhitespace();
+ switch (peek()) {
+ case '[':
+ return readArray();
+ case '{':
+ return readObject();
+ case '"':
+ return readString();
+ case 't':
+ readLiteral("true");
+ return Boolean.TRUE;
+ case 'f':
+ readLiteral("false");
+ return Boolean.FALSE;
+ case 'n':
+ readLiteral("null");
+ return null;
+ default:
+ return readNumber();
+ }
+ }
+
+ private List<Object> readArray() throws IOException {
+ expect('[');
+ final List<Object> values = new ArrayList<>();
+ skipWhitespace();
+ if (peek() == ']') {
+ read();
+ return values;
+ }
+ while (true) {
+ values.add(readValue());
+ skipWhitespace();
+ final int ch = read();
+ if (ch == ']') {
+ return values;
+ }
+ if (ch != ',') {
+ throw error("Expected ',' or ']'");
+ }
+ }
+ }
+
+ private Map<Object, Object> readObject() throws IOException {
+ expect('{');
+ final Map<Object, Object> values = new LinkedHashMap<>();
+ skipWhitespace();
+ if (peek() == '}') {
+ read();
+ return values;
+ }
+ while (true) {
+ skipWhitespace();
+ final String key = readString();
+ skipWhitespace();
+ expect(':');
+ values.put(key, readValue());
+ skipWhitespace();
+ final int ch = read();
+ if (ch == '}') {
+ return values;
+ }
+ if (ch != ',') {
+ throw error("Expected ',' or '}'");
+ }
+ }
+ }
+
+ private String readString() throws IOException {
+ expect('"');
+ final StringBuilder buffer = new StringBuilder();
+ while (true) {
+ final int ch = read();
+ if (ch == -1) {
+ throw error("Unterminated JSON string");
+ }
+ if (ch == '"') {
+ return buffer.toString();
+ }
+ if (ch == '\\') {
+ final int escaped = read();
+ switch (escaped) {
+ case '"':
+ case '\\':
+ case '/':
+ buffer.append((char) escaped);
+ break;
+ case 'b':
+ buffer.append('\b');
+ break;
+ case 'f':
+ buffer.append('\f');
+ break;
+ case 'n':
+ buffer.append('\n');
+ break;
+ case 'r':
+ buffer.append('\r');
+ break;
+ case 't':
+ buffer.append('\t');
+ break;
+ case 'u':
+ buffer.append((char) readHex4());
+ break;
+ default:
+ throw error("Invalid JSON escape");
+ }
+ } else {
+ buffer.append((char) ch);
+ }
+ }
+ }
+
+ private Number readNumber() throws IOException {
+ final StringBuilder buffer = new StringBuilder();
+ int ch = peek();
+ if (ch == '-') {
+ buffer.append((char) read());
+ ch = peek();
+ }
+ if (ch < '0' || ch > '9') {
+ throw error("Invalid JSON number");
+ }
+ while ((ch = peek()) >= '0' && ch <= '9') {
+ buffer.append((char) read());
+ }
+ boolean decimal = false;
+ if (peek() == '.') {
+ decimal = true;
+ buffer.append((char) read());
+ while ((ch = peek()) >= '0' && ch <= '9') {
+ buffer.append((char) read());
+ }
+ }
+ ch = peek();
+ if (ch == 'e' || ch == 'E') {
+ decimal = true;
+ buffer.append((char) read());
+ ch = peek();
+ if (ch == '+' || ch == '-') {
+ buffer.append((char) read());
+ }
+ while ((ch = peek()) >= '0' && ch <= '9') {
+ buffer.append((char) read());
+ }
+ }
+ return decimal ? new BigDecimal(buffer.toString()) : Long.valueOf(buffer.toString());
+ }
+
+ private void readLiteral(final String literal) throws IOException {
+ for (int i = 0; i < literal.length(); i++) {
+ expect(literal.charAt(i));
+ }
+ }
+
+ private int readHex4() throws IOException {
+ int value = 0;
+ for (int i = 0; i < 4; i++) {
+ final int ch = read();
+ final int digit = ch >= '0' && ch <= '9' ? ch - '0'
+ : ch >= 'a' && ch <= 'f' ? ch - 'a' + 10
+ : ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1;
+ if (digit < 0) {
+ throw error("Invalid JSON Unicode escape");
+ }
+ value = value << 4 | digit;
+ }
+ return value;
+ }
+
+ private void skipWhitespace() throws IOException {
+ int ch;
+ while ((ch = peek()) == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
+ read();
+ }
+ }
+
+ private void expect(final int expected) throws IOException {
+ if (read() != expected) {
+ throw error("Expected '" + (char) expected + "'");
+ }
+ }
+
+ private int peek() throws IOException {
+ if (current == -2) {
+ current = reader.read();
+ }
+ return current;
+ }
+
+ private int read() throws IOException {
+ final int value = peek();
+ current = -2;
+ return value;
+ }
+
+ private IOException error(final String message) {
+ return new IOException(message);
+ }
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java
new file mode 100644
index 000000000..4c1e3fb4a
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java
@@ -0,0 +1,135 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.util.Tokenizer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldParser {
+
+ @Test
+ void testAllBareItemTypes() throws Exception {
+ assertItem("42", StructuredFieldType.INTEGER, Long.valueOf(42));
+ assertItem("-999999999999999", StructuredFieldType.INTEGER, Long.valueOf(-999999999999999L));
+ assertItem("4.5", StructuredFieldType.DECIMAL, new BigDecimal("4.5"));
+ assertItem("\"hello \\\"world\\\"\"", StructuredFieldType.STRING, "hello \"world\"");
+ assertItem("foo123/456", StructuredFieldType.TOKEN, "foo123/456");
+ assertItem("?1", StructuredFieldType.BOOLEAN, Boolean.TRUE);
+ assertItem("@1659578233", StructuredFieldType.DATE, Long.valueOf(1659578233));
+ assertItem("%\"f%c3%bc%c3%bc\"", StructuredFieldType.DISPLAY_STRING, "füü");
+
+ final StructuredFieldBareItem bytes = StructuredFieldParser.parseItem(":aGVsbG8=:").getBareItem();
+ Assertions.assertEquals(StructuredFieldType.BYTE_SEQUENCE, bytes.getType());
+ Assertions.assertArrayEquals("hello".getBytes(StandardCharsets.US_ASCII), bytes.getByteSequenceValue());
+ }
+
+ @Test
+ void testListAndInnerList() throws Exception {
+ final StructuredFieldList list = StructuredFieldParser.parseList(
+ "sugar, (tea \"with milk\";q=0.7);fresh, rum;proof=40");
+ Assertions.assertEquals(3, list.size());
+ Assertions.assertEquals("sugar", ((StructuredFieldItem) list.get(0)).getBareItem().getTextValue());
+ final StructuredFieldInnerList inner = (StructuredFieldInnerList) list.get(1);
+ Assertions.assertEquals(2, inner.size());
+ Assertions.assertEquals(new BigDecimal("0.7"), inner.get(1).getParameters().get("q").getDecimalValue());
+ Assertions.assertTrue(inner.getParameters().get("fresh").getBooleanValue());
+ Assertions.assertEquals(40L, ((StructuredFieldItem) list.get(2)).getParameters().get("proof").getLongValue());
+ }
+
+ @Test
+ void testDictionaryAndDuplicateReplacement() throws Exception {
+ final StructuredFieldDictionary dictionary = StructuredFieldParser.parseDictionary(
+ "a=?0, b, c;foo=bar, a=(1 2);valid");
+ Assertions.assertEquals(3, dictionary.size());
+ Assertions.assertEquals("a", dictionary.getName(0));
+ Assertions.assertTrue(dictionary.get("a") instanceof StructuredFieldInnerList);
+ Assertions.assertTrue(((StructuredFieldItem) dictionary.get("b")).getBareItem().getBooleanValue());
+ Assertions.assertEquals("bar", ((StructuredFieldItem) dictionary.get("c"))
+ .getParameters().get("foo").getTextValue());
+ }
+
+ @Test
+ void testDuplicateParameterReplacementPreservesPosition() throws Exception {
+ final StructuredFieldParameters parameters = StructuredFieldParser.parseItem("token;a=1;b=2;a=3")
+ .getParameters();
+ Assertions.assertEquals(2, parameters.size());
+ Assertions.assertEquals("a", parameters.getName(0));
+ Assertions.assertEquals(3L, parameters.get("a").getLongValue());
+ }
+
+ @Test
+ void testMultipleFieldLines() throws Exception {
+ final StructuredFieldList list = StructuredFieldParser.parseListLines(Arrays.asList("one, two", "three"));
+ Assertions.assertEquals(3, list.size());
+ Assertions.assertEquals("one, two, three", StructuredFieldSerializer.serializeList(list));
+
+ final StructuredFieldDictionary dictionary = StructuredFieldParser.parseDictionaryLines(
+ Arrays.asList("a=1", "b=2"));
+ Assertions.assertEquals("a=1, b=2", StructuredFieldSerializer.serializeDictionary(dictionary));
+ }
+
+ @Test
+ void testTokenizerCursorBoundsAndPosition() throws Exception {
+ final String input = "xx 42 yy";
+ final Tokenizer.Cursor cursor = new Tokenizer.Cursor(2, 6);
+ final StructuredFieldItem item = StructuredFieldParser.parseItem(input, cursor);
+ Assertions.assertEquals(42L, item.getBareItem().getLongValue());
+ Assertions.assertEquals(6, cursor.getPos());
+ }
+
+ @Test
+ void testRfcMinimumContainerSizes() throws Exception {
+ final StringBuilder listValue = new StringBuilder();
+ for (int i = 0; i < 1024; i++) {
+ if (i > 0) {
+ listValue.append(',');
+ }
+ listValue.append(i);
+ }
+ Assertions.assertEquals(1024, StructuredFieldParser.parseList(listValue).size());
+
+ final StringBuilder parameters = new StringBuilder("token");
+ for (int i = 0; i < 256; i++) {
+ parameters.append(";p").append(i);
+ }
+ Assertions.assertEquals(256, StructuredFieldParser.parseItem(parameters).getParameters().size());
+ }
+
+ private static void assertItem(
+ final String encoded, final StructuredFieldType type, final Object expected) throws ParseException {
+ final StructuredFieldBareItem item = StructuredFieldParser.parseItem(encoded).getBareItem();
+ Assertions.assertEquals(type, item.getType());
+ Assertions.assertEquals(expected, item.getValue());
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java
new file mode 100644
index 000000000..a0a7772d9
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java
@@ -0,0 +1,99 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldRealWorldHeaders {
+
+ @Test
+ void testRfc9218Priority() throws Exception {
+ final StructuredFieldDictionary priority = StructuredFieldHeaders.parseDictionary(
+ new BasicHeader("Priority", "u=0, i"));
+ Assertions.assertEquals(0L, item(priority, "u").getBareItem().getLongValue());
+ Assertions.assertTrue(item(priority, "i").getBareItem().getBooleanValue());
+ }
+
+ @Test
+ void testRfc9530ContentDigest() throws Exception {
+ final byte[] content = "{\"hello\":\"world\"}\n".getBytes(StandardCharsets.UTF_8);
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(content);
+ final StructuredFieldDictionary value = StructuredFieldDictionary.builder()
+ .put("sha-256", StructuredFieldItem.ofByteSequence(digest))
+ .build();
+ final Header header = StructuredFieldHeaders.format("Content-Digest", value);
+ final byte[] parsed = item(StructuredFieldHeaders.parseDictionary(header), "sha-256")
+ .getBareItem().getByteSequenceValue();
+ Assertions.assertTrue(MessageDigest.isEqual(digest, parsed));
+ }
+
+ @Test
+ void testRfc9211CacheStatus() throws Exception {
+ final StructuredFieldList status = StructuredFieldHeaders.parseList(
+ new BasicHeader("Cache-Status", "ExampleCache;hit;ttl=376"));
+ final StructuredFieldItem cache = (StructuredFieldItem) status.get(0);
+ Assertions.assertEquals("ExampleCache", cache.getBareItem().getTextValue());
+ Assertions.assertTrue(cache.getParameters().get("hit").getBooleanValue());
+ Assertions.assertEquals(376L, cache.getParameters().get("ttl").getLongValue());
+ }
+
+ @Test
+ void testRfc9209ProxyStatus() throws Exception {
+ final StructuredFieldList status = StructuredFieldHeaders.parseList(new BasicHeader(
+ "Proxy-Status", "ExampleCDN;error=connection_timeout;received-status=504"));
+ final StructuredFieldItem proxy = (StructuredFieldItem) status.get(0);
+ Assertions.assertEquals("connection_timeout",
+ proxy.getParameters().get("error").getTextValue());
+ Assertions.assertEquals(504L,
+ proxy.getParameters().get("received-status").getLongValue());
+ }
+
+ @Test
+ void testRfc9421SignatureInput() throws Exception {
+ final StructuredFieldDictionary signatureInput = StructuredFieldHeaders.parseDictionary(new BasicHeader(
+ "Signature-Input", "sig1=(\"@method\" \"@target-uri\" \"content-digest\")"
+ + ";created=1618884475;keyid=\"test-key-rsa-pss\""));
+ final StructuredFieldInnerList signature = (StructuredFieldInnerList) signatureInput.get("sig1");
+ Assertions.assertEquals(3, signature.size());
+ Assertions.assertEquals("@method", signature.get(0).getBareItem().getTextValue());
+ Assertions.assertEquals(1618884475L,
+ signature.getParameters().get("created").getLongValue());
+ Assertions.assertEquals("test-key-rsa-pss",
+ signature.getParameters().get("keyid").getTextValue());
+ }
+
+ private static StructuredFieldItem item(final StructuredFieldDictionary dictionary, final String key) {
+ return (StructuredFieldItem) dictionary.get(key);
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java
new file mode 100644
index 000000000..be02880a2
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java
@@ -0,0 +1,191 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.BufferedHeader;
+import org.apache.hc.core5.http.message.HeaderGroup;
+import org.apache.hc.core5.util.CharArrayBuffer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldSerializer {
+
+ @Test
+ void testCanonicalNumbersAndDisplayString() {
+ Assertions.assertEquals("0.0", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0001"))));
+ Assertions.assertEquals("1.234", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2344"))));
+ Assertions.assertEquals("1.234", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2345"))));
+ Assertions.assertEquals("1.236", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2355"))));
+ Assertions.assertEquals("0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("0.0015"))));
+ Assertions.assertEquals("0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("0.0025"))));
+ Assertions.assertEquals("-0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0015"))));
+ Assertions.assertEquals("-0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0025"))));
+ Assertions.assertEquals("10.0", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("9.9995"))));
+ Assertions.assertEquals("%\"F%c3%bc%c3%9fe %22%25%22\"",
+ bare(StructuredFieldBareItem.ofDisplayString("Füße \"%\"")));
+ }
+
+ @Test
+ void testCanonicalDictionaryAndParameters() {
+ final StructuredFieldParameters parameters = StructuredFieldParameters.builder()
+ .putBoolean("valid", true)
+ .putBoolean("cached", false)
+ .put("ttl", StructuredFieldBareItem.ofInteger(60))
+ .build();
+ final StructuredFieldDictionary dictionary = StructuredFieldDictionary.builder()
+ .put("available", StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parameters))
+ .put("rating", StructuredFieldItem.of(
+ StructuredFieldBareItem.ofDecimal(new BigDecimal("1.500"))))
+ .put("feelings", StructuredFieldInnerList.of(
+ StructuredFieldItem.of(StructuredFieldBareItem.ofToken("joy")),
+ StructuredFieldItem.of(StructuredFieldBareItem.ofToken("sadness"))))
+ .build();
+ Assertions.assertEquals(
+ "available;valid;cached=?0;ttl=60, rating=1.5, feelings=(joy sadness)",
+ StructuredFieldSerializer.serializeDictionary(dictionary));
+ }
+
+ @Test
+ void testEmptyContainersOmitField() throws Exception {
+ final StructuredFieldList list = StructuredFieldList.of();
+ final StructuredFieldDictionary dictionary = StructuredFieldDictionary.builder().build();
+ Assertions.assertNull(StructuredFieldSerializer.serializeList(list));
+ Assertions.assertNull(StructuredFieldSerializer.serializeDictionary(dictionary));
+ Assertions.assertNull(StructuredFieldHeaders.format("Example", list));
+ Assertions.assertTrue(StructuredFieldParser.parseList("").isEmpty());
+ Assertions.assertTrue(StructuredFieldParser.parseDictionary("").isEmpty());
+ }
+
+ @Test
+ void testMessageHeadersCombineLinesCaseInsensitively() throws Exception {
+ final HeaderGroup headers = new HeaderGroup();
+ headers.addHeader(new BasicHeader("Example", "one, two"));
+ headers.addHeader(new BasicHeader("EXAMPLE", "three"));
+ final StructuredFieldList value = StructuredFieldHeaders.parseList(headers, "example");
+ Assertions.assertEquals(3, value.size());
+ final Header formatted = StructuredFieldHeaders.format("Example", value);
+ Assertions.assertNotNull(formatted);
+ Assertions.assertTrue(formatted instanceof BufferedHeader);
+ Assertions.assertEquals("one, two, three", formatted.getValue());
+ Assertions.assertEquals(value, StructuredFieldHeaders.parseList(formatted));
+ }
+
+ @Test
+ void testMessageHeadersCombineFormattedHeaderLines() throws Exception {
+ final HeaderGroup headers = new HeaderGroup();
+ headers.addHeader(bufferedHeader("Example: one, two"));
+ headers.addHeader(bufferedHeader("Example: three"));
+ final StructuredFieldList value = StructuredFieldHeaders.parseList(headers, "example");
+ Assertions.assertEquals(3, value.size());
+ Assertions.assertEquals("one, two, three",
+ StructuredFieldHeaders.format("Example", value).getValue());
+ }
+
+ @Test
+ void testMessageHeadersItemMustNotSpanFieldLines() throws Exception {
+ final HeaderGroup single = new HeaderGroup();
+ single.addHeader(new BasicHeader("Example", "10"));
+ Assertions.assertEquals(
+ StructuredFieldHeaders.parseItem(new BasicHeader("Example", "10")),
+ StructuredFieldHeaders.parseItem(single, "example"));
+
+ final HeaderGroup multiple = new HeaderGroup();
+ multiple.addHeader(new BasicHeader("Example", "10"));
+ multiple.addHeader(new BasicHeader("Example", "20"));
+ Assertions.assertThrows(ParseException.class,
+ () -> StructuredFieldHeaders.parseItem(multiple, "example"));
+ }
+
+ @Test
+ void testMessageHeadersListMembersMustBeCommaSeparated() {
+ final HeaderGroup headers = new HeaderGroup();
+ headers.addHeader(new BasicHeader("Example", "a b"));
+ Assertions.assertThrows(ParseException.class,
+ () -> StructuredFieldHeaders.parseList(headers, "example"));
+ }
+
+ private static BufferedHeader bufferedHeader(final String line) {
+ final CharArrayBuffer buffer = new CharArrayBuffer(line.length());
+ buffer.append(line);
+ return BufferedHeader.create(buffer);
+ }
+
+ @Test
+ void testSerializeIntoExistingBuffer() {
+ final CharArrayBuffer buffer = new CharArrayBuffer(32);
+ buffer.append("Priority: ");
+ StructuredFieldSerializer.serialize(buffer, StructuredFieldDictionary.builder()
+ .put("u", StructuredFieldItem.ofInteger(0))
+ .put("i", StructuredFieldItem.ofBoolean(true))
+ .build());
+ Assertions.assertEquals("Priority: u=0, i", buffer.toString());
+ }
+
+ @Test
+ void testImmutabilityAndValueSemantics() {
+ final byte[] bytes = {1, 2, 3};
+ final StructuredFieldBareItem item = StructuredFieldBareItem.ofByteSequence(bytes);
+ bytes[0] = 9;
+ Assertions.assertArrayEquals(new byte[] {1, 2, 3}, item.getByteSequenceValue());
+ final byte[] returned = item.getByteSequenceValue();
+ returned[0] = 8;
+ Assertions.assertArrayEquals(new byte[] {1, 2, 3}, item.getByteSequenceValue());
+
+ final StructuredFieldList list = StructuredFieldList.of(
+ StructuredFieldItem.of(StructuredFieldBareItem.ofInteger(1)));
+ Assertions.assertThrows(UnsupportedOperationException.class,
+ () -> list.getMembers().add(StructuredFieldItem.of(StructuredFieldBareItem.ofInteger(2))));
+ Assertions.assertEquals(list, StructuredFieldList.of(Arrays.asList(list.get(0))));
+ }
+
+ @Test
+ void testValidationAtConstructionBoundary() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldBareItem.ofInteger(1000000000000000L));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldBareItem.ofDecimal(new BigDecimal("999999999999.9995")));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldBareItem.ofString("é"));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldBareItem.ofToken("1token"));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldParameters.builder().putBoolean("Upper", true));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldBareItem.ofDisplayString("\ud800"));
+ }
+
+ private static String bare(final StructuredFieldBareItem item) {
+ return StructuredFieldSerializer.serializeBareItem(item);
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java
new file mode 100644
index 000000000..c8484e416
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.structured;
+
+import org.apache.hc.core5.http.ParseException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Strictness cases aligned with the HTTPWG structured-field-tests corpus.
+ */
+class TestStructuredFieldStrictness {
+
+ @Test
+ void testInvalidItems() {
+ assertItemFails("", "-", "9999999999999999", "1.", "1.2345", "1000000000000.0",
+ "\"unterminated", "\"bad\\q\"", "?2", "@1.2", "'single'", "\té", "١");
+ }
+
+ @Test
+ void testInvalidContainers() {
+ assertListFails("one,", "one,,two", "(one\ttwo)", "(one,two)", "(one", "one ;x");
+ assertDictionaryFails("A=1", "a =1", "a=1,", "a==1", "1a=1", "a=(1\t2)");
+ }
+
+ @Test
+ void testStrictByteSequences() throws Exception {
+ Assertions.assertEquals(5,
+ StructuredFieldParser.parseItem(":aGVsbG8:").getBareItem().getByteSequenceValue().length);
+ Assertions.assertEquals(":YQ==:", StructuredFieldSerializer.serializeItem(
+ StructuredFieldParser.parseItem(":YQ=:")));
+ assertItemFails(":=aGVsbG8=:", ":a=GVsbG8=:", ":aGVsb G8=:", ":aGVsbG8=", ":_-Ah:", ":",
+ ":YQ===:");
+
+ final StructuredFieldItem nonZeroPadBits = StructuredFieldParser.parseItem(":iZ==:");
+ Assertions.assertEquals(":iQ==:", StructuredFieldSerializer.serializeItem(nonZeroPadBits));
+ }
+
+ @Test
+ void testStrictDisplayStrings() throws Exception {
+ final StructuredFieldItem item = StructuredFieldParser.parseItem("%\"foo %22bar%22 \\ baz\"");
+ Assertions.assertEquals("foo \"bar\" \\ baz", item.getBareItem().getTextValue());
+ Assertions.assertEquals("%\"foo %22bar%22 \\ baz\"", StructuredFieldSerializer.serializeItem(item));
+
+ final StructuredFieldItem overEncoded = StructuredFieldParser.parseItem("%\"%61\"");
+ Assertions.assertEquals("%\"a\"", StructuredFieldSerializer.serializeItem(overEncoded));
+
+ assertItemFails("%\"f%C3%BC\"", "%\"füü\"", "%\"%c3%28\"", "%\"%a0%a1\"",
+ "%\"%e2%28%a1\"", "%\"%f0%28%8c%28\"", "%\"%g0\"", "%\"%\"", "%foo");
+ }
+
+ @Test
+ void testOnlySpacesAreAllowedAtTopLevel() throws Exception {
+ Assertions.assertEquals(1L, StructuredFieldParser.parseItem(" 1 ").getBareItem().getLongValue());
+ assertItemFails(" \t 1", "1 \t ");
+ }
+
+ @Test
+ void testUnicodeDigitsAreNotRfcDigits() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> StructuredFieldParameters.builder().putBoolean("aÙ¡", true));
+ }
+
+ private static void assertItemFails(final String... values) {
+ for (final String value : values) {
+ Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseItem(value), value);
+ }
+ }
+
+ private static void assertListFails(final String... values) {
+ for (final String value : values) {
+ Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseList(value), value);
+ }
+ }
+
+ private static void assertDictionaryFails(final String... values) {
+ for (final String value : values) {
+ Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseDictionary(value), value);
+ }
+ }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
index c6a2b0459..42c220c8f 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
@@ -30,6 +30,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -50,6 +51,15 @@ void testCoding() {
assertEquals("blah%21%20~%20huh%3F", buf.toString());
}
+ @Test
+ void testLowerCaseHexEncoding() {
+ final BitSet safeChars = new BitSet(256);
+ safeChars.set(' ');
+ final PercentCodec codec = new PercentCodec(safeChars, true);
+ assertEquals(" %c3%bc%22%25", codec.encode(" ü\"%"));
+ assertEquals("%C3%BC", PercentCodec.RFC3986.encode("ü"));
+ }
+
@Test
void testDecoding() {
assertEquals("blah! ~ huh?", PercentCodec.decode("blah%21%20~%20huh%3F", StandardCharsets.UTF_8));
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json
new file mode 100644
index 000000000..bead6e1a3
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json
@@ -0,0 +1,106 @@
+[
+ {
+ "name": "basic binary",
+ "raw": [":aGVsbG8=:"],
+ "header_type": "item",
+ "expected": [
+ {"__type": "binary", "value": "NBSWY3DP"},
+ []]
+ },
+ {
+ "name": "empty binary",
+ "raw": ["::"],
+ "header_type": "item",
+ "expected": [
+ {"__type": "binary", "value": ""},
+ []]
+ },
+ {
+ "name": "padding at beginning",
+ "raw": [":=aGVsbG8=:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "padding in middle",
+ "raw": [":a=GVsbG8=:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad padding",
+ "raw": [":aGVsbG8:"],
+ "header_type": "item",
+ "expected": [
+ {"__type": "binary", "value": "NBSWY3DP"},
+ []],
+ "can_fail": true,
+ "canonical": [":aGVsbG8=:"]
+ },
+ {
+ "name": "bad padding dot",
+ "raw": [":aGVsbG8.:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad end delimiter",
+ "raw": [":aGVsbG8="],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "extra whitespace",
+ "raw": [":aGVsb G8=:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "all whitespace",
+ "raw": [": :"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "extra chars",
+ "raw": [":aGVsbG!8=:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "suffix chars",
+ "raw": [":aGVsbG8=!:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "non-zero pad bits",
+ "raw": [":iZ==:"],
+ "header_type": "item",
+ "expected": [
+ {"__type": "binary", "value": "RE======"},
+ []],
+ "can_fail": true,
+ "canonical": [":iQ==:"]
+ },
+ {
+ "name": "non-ASCII binary",
+ "raw": [":/+Ah:"],
+ "header_type": "item",
+ "expected": [
+ {"__type": "binary", "value": "77QCC==="},
+ []]
+ },
+ {
+ "name": "base64url binary",
+ "raw": [":_-Ah:"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "binary prefix only",
+ "raw": [":"],
+ "header_type": "item",
+ "must_fail": true
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json
new file mode 100644
index 000000000..e3fc09226
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json
@@ -0,0 +1,75 @@
+[
+ {
+ "name": "basic true boolean",
+ "raw": ["?1"],
+ "header_type": "item",
+ "expected": [true, []]
+ },
+ {
+ "name": "basic false boolean",
+ "raw": ["?0"],
+ "header_type": "item",
+ "expected": [false, []]
+ },
+ {
+ "name": "unknown boolean",
+ "raw": ["?Q"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "whitespace boolean",
+ "raw": ["? 1"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "negative zero boolean",
+ "raw": ["?-0"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "T boolean",
+ "raw": ["?T"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "F boolean",
+ "raw": ["?F"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "t boolean",
+ "raw": ["?t"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "f boolean",
+ "raw": ["?f"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "spelled-out True boolean",
+ "raw": ["?True"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "spelled-out False boolean",
+ "raw": ["?False"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "boolean prefix only",
+ "raw": ["?"],
+ "header_type": "item",
+ "must_fail": true
+ }
+]
+
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json
new file mode 100644
index 000000000..8c41ff784
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json
@@ -0,0 +1,107 @@
+[
+ {
+ "name": "date - 1970-01-01 00:00:00",
+ "raw": ["@0"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 0}, []]
+ },
+ {
+ "name": "date - 2022-08-04 01:57:13",
+ "raw": ["@1659578233"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 1659578233}, []]
+ },
+ {
+ "name": "date - 1917-05-30 22:02:47",
+ "raw": ["@-1659578233"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": -1659578233}, []]
+ },
+ {
+ "name": "date - 2^31",
+ "raw": ["@2147483648"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 2147483648}, []]
+ },
+ {
+ "name": "date - 2^32",
+ "raw": ["@4294967296"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 4294967296}, []]
+ },
+ {
+ "name": "interoperability max date - 9999-12-31 00:00:00",
+ "raw": ["@253402214400"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 253402214400}, []]
+ },
+ {
+ "name": "interoperability min date - 0001-01-01 00:00:00",
+ "raw": ["@-62135596800"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": -62135596800}, []]
+ },
+ {
+ "name": "date - decimal",
+ "raw": ["@1659578233.12"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "syntactic max date - 999,999,999,999,999",
+ "raw": ["@999999999999999"],
+ "header_type": "item",
+ "can_fail": true,
+ "expected": [{"__type": "date", "value": 999999999999999}, []]
+ },
+ {
+ "name": "syntactic min date - -999,999,999,999,999",
+ "raw": ["@-999999999999999"],
+ "header_type": "item",
+ "can_fail": true,
+ "expected": [{"__type": "date", "value": -999999999999999}, []]
+ },
+ {
+ "name": "too large syntactic date - 1,000,000,000,000,000",
+ "raw": ["@1000000000000000"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "too small syntactic date - -1,000,000,000,000,000",
+ "raw": ["@-1000000000000000"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "date with negative zero",
+ "raw": ["@-0"],
+ "header_type": "item",
+ "expected": [{"__type": "date", "value": 0}, []],
+ "canonical": ["@0"]
+ },
+ {
+ "name": "date - empty",
+ "raw": ["@"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "date - sign only",
+ "raw": ["@-"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "date - space",
+ "raw": ["@ 12345678"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "date - non-digit",
+ "raw": ["@abc"],
+ "header_type": "item",
+ "must_fail": true
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json
new file mode 100644
index 000000000..0a7a7dda6
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json
@@ -0,0 +1,169 @@
+[
+ {
+ "name": "basic dictionary",
+ "raw": ["en=\"Applepie\", da=:w4ZibGV0w6ZydGUK:"],
+ "header_type": "dictionary",
+ "expected": [["en", ["Applepie", []]], ["da", [
+ {"__type": "binary", "value": "YODGE3DFOTB2M4TUMUFA===="},
+ []]
+ ]]
+ },
+ {
+ "name": "empty dictionary",
+ "raw": [""],
+ "header_type": "dictionary",
+ "expected": [],
+ "canonical": []
+ },
+ {
+ "name": "single item dictionary",
+ "raw": ["a=1"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]]]
+ },
+ {
+ "name": "list item dictionary",
+ "raw": ["a=(1 2)"],
+ "header_type": "dictionary",
+ "expected": [["a", [[[1, []], [2, []]], []]]]
+ },
+ {
+ "name": "single list item dictionary",
+ "raw": ["a=(1)"],
+ "header_type": "dictionary",
+ "expected": [["a", [[[1, []]], []]]]
+ },
+ {
+ "name": "empty list item dictionary",
+ "raw": ["a=()"],
+ "header_type": "dictionary",
+ "expected": [["a", [[], []]]]
+ },
+ {
+ "name": "no whitespace dictionary",
+ "raw": ["a=1,b=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [2, []]]],
+ "canonical": ["a=1, b=2"]
+ },
+ {
+ "name": "extra whitespace dictionary",
+ "raw": ["a=1 , b=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [2, []]]],
+ "canonical": ["a=1, b=2"]
+ },
+ {
+ "name": "tab separated dictionary",
+ "raw": ["a=1\t,\tb=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [2, []]]],
+ "canonical": ["a=1, b=2"]
+ },
+ {
+ "name": "leading whitespace dictionary",
+ "raw": [" a=1 , b=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [2, []]]],
+ "canonical": ["a=1, b=2"]
+ },
+ {
+ "name": "whitespace before = dictionary",
+ "raw": ["a =1, b=2"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "whitespace after = dictionary",
+ "raw": ["a=1, b= 2"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "two lines dictionary",
+ "raw": ["a=1", "b=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [2, []]]],
+ "canonical": ["a=1, b=2"]
+ },
+ {
+ "name": "missing value dictionary",
+ "raw": ["a=1, b, c=3"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [true, []]], ["c", [3, []]]]
+ },
+ {
+ "name": "all missing value dictionary",
+ "raw": ["a, b, c"],
+ "header_type": "dictionary",
+ "expected": [["a", [true, []]], ["b", [true, []]], ["c", [true, []]]]
+ },
+ {
+ "name": "start missing value dictionary",
+ "raw": ["a, b=2"],
+ "header_type": "dictionary",
+ "expected": [["a", [true, []]], ["b", [2, []]]]
+ },
+ {
+ "name": "end missing value dictionary",
+ "raw": ["a=1, b"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [true, []]]]
+ },
+ {
+ "name": "missing value with params dictionary",
+ "raw": ["a=1, b;foo=9, c=3"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [true, [["foo", 9]]]], ["c", [3, []]]]
+ },
+ {
+ "name": "explicit true value with params dictionary",
+ "raw": ["a=1, b=?1;foo=9, c=3"],
+ "header_type": "dictionary",
+ "expected": [["a", [1, []]], ["b", [true, [["foo", 9]]]], ["c", [3, []]]],
+ "canonical": ["a=1, b;foo=9, c=3"]
+ },
+ {
+ "name": "trailing comma dictionary",
+ "raw": ["a=1, b=2,"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "empty item dictionary",
+ "raw": ["a=1,,b=2,"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "duplicate key dictionary",
+ "raw": ["a=1,b=2,a=3"],
+ "header_type": "dictionary",
+ "expected": [["a", [3, []]], ["b", [2, []]]],
+ "canonical": ["a=3, b=2"]
+ },
+ {
+ "name": "dictionary key ordering",
+ "raw": ["m, z, t"],
+ "header_type": "dictionary",
+ "expected": [["m", [true, []]], ["z", [true, []]], ["t", [true, []]]]
+ },
+ {
+ "name": "numeric key dictionary",
+ "raw": ["a=1,1b=2,a=1"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "uppercase key dictionary",
+ "raw": ["a=1,B=2,a=1"],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "bad key dictionary",
+ "raw": ["a=1,b!=2,a=1"],
+ "header_type": "dictionary",
+ "must_fail": true
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json
new file mode 100644
index 000000000..3fca7c692
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json
@@ -0,0 +1,137 @@
+[
+ {
+ "name": "basic display string (ascii content)",
+ "raw": ["%\"foo bar\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": "foo bar"}, []]
+ },
+ {
+ "name": "all printable ascii",
+ "raw": ["%\" !%22#$%25&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"}, []]
+ },
+ {
+ "name": "non-ascii display string (uppercase escaping)",
+ "raw": ["%\"f%C3%BC%C3%BC\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "non-ascii display string (lowercase escaping)",
+ "raw": ["%\"f%c3%bc%c3%bc\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": "füü"}, []]
+ },
+ {
+ "name": "non-ascii display string (unescaped)",
+ "raw": ["%\"füü\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "tab in display string",
+ "raw": ["%\"\t\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "newline in display string",
+ "raw": ["%\"\n\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "single quoted display string",
+ "raw": ["%'foo'"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "unquoted display string",
+ "raw": ["%foo"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "display string missing initial quote",
+ "raw": ["%foo\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "unbalanced display string",
+ "raw": ["%\"foo"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "display string quoting",
+ "raw": ["%\"foo %22bar%22 \\ baz\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": "foo \"bar\" \\ baz"}, []]
+ },
+ {
+ "name": "bad display string escaping",
+ "raw": ["%\"foo %a"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad display string utf-8 (invalid 2-byte seq)",
+ "raw": ["%\"%c3%28\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad display string utf-8 (invalid sequence id)",
+ "raw": ["%\"%a0%a1\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad display string utf-8 (invalid hex)",
+ "raw": ["%\"%g0%1w\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad display string utf-8 (invalid 3-byte seq)",
+ "raw": ["%\"%e2%28%a1\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "bad display string utf-8 (invalid 4-byte seq)",
+ "raw": ["%\"%f0%28%8c%28\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "BOM in display string",
+ "raw": ["%\"BOM: %ef%bb%bf\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": "BOM: \uFEFF"}, []]
+ },
+ {
+ "name": "two lines display string",
+ "raw": ["%\"foo", "bar\""],
+ "header_type": "item",
+ "can_fail": true,
+ "expected": [{"__type": "displaystring", "value": "foo, bar"}, []],
+ "canonical": ["%\"foo, bar\""]
+ },
+ {
+ "name": "truncated display string escape (zero digits)",
+ "raw": ["%\"%\""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "over-encoded display string",
+ "raw": ["%\"%61\""],
+ "header_type": "item",
+ "expected": [{"__type": "displaystring", "value": "a"}, []],
+ "canonical": ["%\"a\""]
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json
new file mode 100644
index 000000000..eb7dc349f
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json
@@ -0,0 +1,208 @@
+[
+ {
+ "name": "Foo-Example",
+ "raw": ["2; foourl=\"https://foo.example.com/\""],
+ "header_type": "item",
+ "expected": [2, [["foourl", "https://foo.example.com/"]]],
+ "canonical": ["2;foourl=\"https://foo.example.com/\""]
+ },
+ {
+ "name": "Example-StrListHeader",
+ "raw": ["\"foo\", \"bar\", \"It was the best of times.\""],
+ "header_type": "list",
+ "expected": [
+ ["foo", []],
+ ["bar", []],
+ ["It was the best of times.", []]
+ ]
+ },
+ {
+ "name": "Example-Hdr (list on one line)",
+ "raw": ["foo, bar"],
+ "header_type": "list",
+ "expected": [
+ [{"__type":"token", "value":"foo"}, []],
+ [{"__type":"token", "value":"bar"}, []]
+ ]
+ },
+ {
+ "name": "Example-Hdr (list on two lines)",
+ "raw": ["foo", "bar"],
+ "header_type": "list",
+ "expected": [
+ [{"__type":"token", "value":"foo"}, []],
+ [{"__type":"token", "value":"bar"}, []]
+ ],
+ "canonical": ["foo, bar"]
+ },
+ {
+ "name": "Example-StrListListHeader",
+ "raw": ["(\"foo\" \"bar\"), (\"baz\"), (\"bat\" \"one\"), ()"],
+ "header_type": "list",
+ "expected": [
+ [[
+ ["foo", []],
+ ["bar", []]
+ ], []],
+ [[
+ ["baz", []]
+ ], []],
+ [[
+ ["bat", []],
+ ["one", []]
+ ], []],
+ [[], []]
+ ]
+ },
+ {
+ "name": "Example-ListListParam",
+ "raw": ["(\"foo\"; a=1;b=2);lvl=5, (\"bar\" \"baz\");lvl=1"],
+ "header_type": "list",
+ "expected": [
+ [[
+ ["foo", [["a", 1], ["b", 2]]]
+ ], [["lvl", 5]]],
+ [[
+ ["bar", []], ["baz", []]
+ ], [["lvl", 1]]]
+ ],
+ "canonical": ["(\"foo\";a=1;b=2);lvl=5, (\"bar\" \"baz\");lvl=1"]
+ },
+
+ {
+ "name": "Example-ParamListHeader",
+ "raw": ["abc;a=1;b=2; cde_456, (ghi;jk=4 l);q=\"9\";r=w"],
+ "header_type": "list",
+ "expected": [
+ [{"__type": "token", "value": "abc"}, [["a", 1], ["b", 2], ["cde_456", true]]],
+ [
+ [
+ [{"__type": "token", "value": "ghi"}, [["jk", 4]]],
+ [{"__type": "token", "value": "l"}, []]
+ ],
+ [["q", "9"], ["r", {"__type": "token", "value": "w"}]]
+ ]
+ ],
+ "canonical": ["abc;a=1;b=2;cde_456, (ghi;jk=4 l);q=\"9\";r=w"]
+ },
+ {
+ "name": "Example-IntHeader",
+ "raw": ["1; a; b=?0"],
+ "header_type": "item",
+ "expected": [1, [["a", true], ["b", false]]],
+ "canonical": ["1;a;b=?0"]
+ },
+ {
+ "name": "Example-DictHeader",
+ "raw": ["en=\"Applepie\", da=:w4ZibGV0w6ZydGU=:"],
+ "header_type": "dictionary",
+ "expected": [
+ ["en", ["Applepie", []]],
+ ["da", [{"__type": "binary", "value": "YODGE3DFOTB2M4TUMU======"}, []]]
+ ]
+ },
+ {
+ "name": "Example-DictHeader (boolean values)",
+ "raw": ["a=?0, b, c; foo=bar"],
+ "header_type": "dictionary",
+ "expected": [
+ ["a", [false, []]],
+ ["b", [true, []]],
+ ["c", [true, [["foo", {"__type": "token", "value": "bar"}]]]]
+ ],
+ "canonical": ["a=?0, b, c;foo=bar"]
+ },
+ {
+ "name": "Example-DictListHeader",
+ "raw": ["rating=1.5, feelings=(joy sadness)"],
+ "header_type": "dictionary",
+ "expected": [
+ ["rating", [1.5, []]],
+ ["feelings", [[
+ [{"__type": "token", "value": "joy"}, []],
+ [{"__type": "token", "value": "sadness"}, []]
+ ], []]]
+ ]
+ },
+ {
+ "name": "Example-MixDict",
+ "raw": ["a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid"],
+ "header_type": "dictionary",
+ "expected": [
+ ["a", [[
+ [1, []],
+ [2, []]
+ ], []]],
+ ["b", [3, []]],
+ ["c", [4, [["aa", {"__type": "token", "value": "bb"}]]]],
+ ["d", [[
+ [5, []],
+ [6, []]
+ ], [["valid", true]]]]
+ ],
+ "canonical": ["a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid"]
+ },
+ {
+ "name": "Example-Hdr (dictionary on one line)",
+ "raw": ["foo=1, bar=2"],
+ "header_type": "dictionary",
+ "expected": [
+ ["foo", [1, []]],
+ ["bar", [2, []]]
+ ]
+ },
+ {
+ "name": "Example-Hdr (dictionary on two lines)",
+ "raw": ["foo=1", "bar=2"],
+ "header_type": "dictionary",
+ "expected": [
+ ["foo", [1, []]],
+ ["bar", [2, []]]
+ ],
+ "canonical": ["foo=1, bar=2"]
+ },
+
+ {
+ "name": "Example-IntItemHeader",
+ "raw": ["5"],
+ "header_type": "item",
+ "expected": [5, []]
+ },
+ {
+ "name": "Example-IntItemHeader (params)",
+ "raw": ["5; foo=bar"],
+ "header_type": "item",
+ "expected": [5, [["foo", {"__type": "token", "value": "bar"}]]],
+ "canonical": ["5;foo=bar"]
+ },
+ {
+ "name": "Example-IntegerHeader",
+ "raw": ["42"],
+ "header_type": "item",
+ "expected": [42, []]
+ },
+ {
+ "name": "Example-FloatHeader",
+ "raw": ["4.5"],
+ "header_type": "item",
+ "expected": [4.5, []]
+ },
+ {
+ "name": "Example-StringHeader",
+ "raw": ["\"hello world\""],
+ "header_type": "item",
+ "expected": ["hello world", []]
+ },
+ {
+ "name": "Example-BinaryHdr",
+ "raw": [":cHJldGVuZCB0aGlzIGlzIGJpbmFyeSBjb250ZW50Lg==:"],
+ "header_type": "item",
+ "expected": [{"__type": "binary", "value": "OBZGK5DFNZSCA5DINFZSA2LTEBRGS3TBOJ4SAY3PNZ2GK3TUFY======"}, []]
+ },
+ {
+ "name": "Example-BoolHdr",
+ "raw": ["?1"],
+ "header_type": "item",
+ "expected": [true, []]
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json
new file mode 100644
index 000000000..e86cea0e9
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json
@@ -0,0 +1,34 @@
+[
+ {
+ "name": "empty item",
+ "raw": [""],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "leading space",
+ "raw": [" \t 1"],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "trailing space",
+ "raw": ["1 \t "],
+ "header_type": "item",
+ "must_fail": true
+ },
+ {
+ "name": "leading and trailing space",
+ "raw": [" 1 "],
+ "header_type": "item",
+ "expected": [1, []],
+ "canonical": ["1"]
+ },
+ {
+ "name": "leading and trailing whitespace",
+ "raw": [" 1 "],
+ "header_type": "item",
+ "expected": [1, []],
+ "canonical": ["1"]
+ }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json
new file mode 100644
index 000000000..efa67d963
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json
@@ -0,0 +1,7013 @@
+[
+ {
+ "name": "0x00 as a single-character dictionary key",
+ "raw": [
+ "\u0000=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x01 as a single-character dictionary key",
+ "raw": [
+ "\u0001=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x02 as a single-character dictionary key",
+ "raw": [
+ "\u0002=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x03 as a single-character dictionary key",
+ "raw": [
+ "\u0003=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x04 as a single-character dictionary key",
+ "raw": [
+ "\u0004=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x05 as a single-character dictionary key",
+ "raw": [
+ "\u0005=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x06 as a single-character dictionary key",
+ "raw": [
+ "\u0006=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x07 as a single-character dictionary key",
+ "raw": [
+ "\u0007=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x08 as a single-character dictionary key",
+ "raw": [
+ "\b=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x09 as a single-character dictionary key",
+ "raw": [
+ "\t=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0a as a single-character dictionary key",
+ "raw": [
+ "\n=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0b as a single-character dictionary key",
+ "raw": [
+ "\u000b=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0c as a single-character dictionary key",
+ "raw": [
+ "\f=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0d as a single-character dictionary key",
+ "raw": [
+ "\r=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0e as a single-character dictionary key",
+ "raw": [
+ "\u000e=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0f as a single-character dictionary key",
+ "raw": [
+ "\u000f=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x10 as a single-character dictionary key",
+ "raw": [
+ "\u0010=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x11 as a single-character dictionary key",
+ "raw": [
+ "\u0011=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x12 as a single-character dictionary key",
+ "raw": [
+ "\u0012=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x13 as a single-character dictionary key",
+ "raw": [
+ "\u0013=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x14 as a single-character dictionary key",
+ "raw": [
+ "\u0014=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x15 as a single-character dictionary key",
+ "raw": [
+ "\u0015=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x16 as a single-character dictionary key",
+ "raw": [
+ "\u0016=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x17 as a single-character dictionary key",
+ "raw": [
+ "\u0017=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x18 as a single-character dictionary key",
+ "raw": [
+ "\u0018=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x19 as a single-character dictionary key",
+ "raw": [
+ "\u0019=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1a as a single-character dictionary key",
+ "raw": [
+ "\u001a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1b as a single-character dictionary key",
+ "raw": [
+ "\u001b=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1c as a single-character dictionary key",
+ "raw": [
+ "\u001c=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1d as a single-character dictionary key",
+ "raw": [
+ "\u001d=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1e as a single-character dictionary key",
+ "raw": [
+ "\u001e=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1f as a single-character dictionary key",
+ "raw": [
+ "\u001f=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x20 as a single-character dictionary key",
+ "raw": [
+ "=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x21 as a single-character dictionary key",
+ "raw": [
+ "!=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x22 as a single-character dictionary key",
+ "raw": [
+ "\"=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x23 as a single-character dictionary key",
+ "raw": [
+ "#=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x24 as a single-character dictionary key",
+ "raw": [
+ "$=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x25 as a single-character dictionary key",
+ "raw": [
+ "%=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x26 as a single-character dictionary key",
+ "raw": [
+ "&=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x27 as a single-character dictionary key",
+ "raw": [
+ "'=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x28 as a single-character dictionary key",
+ "raw": [
+ "(=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x29 as a single-character dictionary key",
+ "raw": [
+ ")=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2a as a single-character dictionary key",
+ "raw": [
+ "*=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "*",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x2b as a single-character dictionary key",
+ "raw": [
+ "+=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2c as a single-character dictionary key",
+ "raw": [
+ ",=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2d as a single-character dictionary key",
+ "raw": [
+ "-=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2e as a single-character dictionary key",
+ "raw": [
+ ".=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2f as a single-character dictionary key",
+ "raw": [
+ "/=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x30 as a single-character dictionary key",
+ "raw": [
+ "0=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x31 as a single-character dictionary key",
+ "raw": [
+ "1=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x32 as a single-character dictionary key",
+ "raw": [
+ "2=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x33 as a single-character dictionary key",
+ "raw": [
+ "3=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x34 as a single-character dictionary key",
+ "raw": [
+ "4=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x35 as a single-character dictionary key",
+ "raw": [
+ "5=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x36 as a single-character dictionary key",
+ "raw": [
+ "6=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x37 as a single-character dictionary key",
+ "raw": [
+ "7=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x38 as a single-character dictionary key",
+ "raw": [
+ "8=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x39 as a single-character dictionary key",
+ "raw": [
+ "9=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3a as a single-character dictionary key",
+ "raw": [
+ ":=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3b as a single-character dictionary key",
+ "raw": [
+ ";=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3c as a single-character dictionary key",
+ "raw": [
+ "<=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3d as a single-character dictionary key",
+ "raw": [
+ "==1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3e as a single-character dictionary key",
+ "raw": [
+ ">=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3f as a single-character dictionary key",
+ "raw": [
+ "?=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x40 as a single-character dictionary key",
+ "raw": [
+ "@=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x41 as a single-character dictionary key",
+ "raw": [
+ "A=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x42 as a single-character dictionary key",
+ "raw": [
+ "B=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x43 as a single-character dictionary key",
+ "raw": [
+ "C=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x44 as a single-character dictionary key",
+ "raw": [
+ "D=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x45 as a single-character dictionary key",
+ "raw": [
+ "E=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x46 as a single-character dictionary key",
+ "raw": [
+ "F=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x47 as a single-character dictionary key",
+ "raw": [
+ "G=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x48 as a single-character dictionary key",
+ "raw": [
+ "H=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x49 as a single-character dictionary key",
+ "raw": [
+ "I=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4a as a single-character dictionary key",
+ "raw": [
+ "J=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4b as a single-character dictionary key",
+ "raw": [
+ "K=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4c as a single-character dictionary key",
+ "raw": [
+ "L=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4d as a single-character dictionary key",
+ "raw": [
+ "M=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4e as a single-character dictionary key",
+ "raw": [
+ "N=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4f as a single-character dictionary key",
+ "raw": [
+ "O=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x50 as a single-character dictionary key",
+ "raw": [
+ "P=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x51 as a single-character dictionary key",
+ "raw": [
+ "Q=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x52 as a single-character dictionary key",
+ "raw": [
+ "R=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x53 as a single-character dictionary key",
+ "raw": [
+ "S=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x54 as a single-character dictionary key",
+ "raw": [
+ "T=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x55 as a single-character dictionary key",
+ "raw": [
+ "U=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x56 as a single-character dictionary key",
+ "raw": [
+ "V=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x57 as a single-character dictionary key",
+ "raw": [
+ "W=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x58 as a single-character dictionary key",
+ "raw": [
+ "X=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x59 as a single-character dictionary key",
+ "raw": [
+ "Y=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5a as a single-character dictionary key",
+ "raw": [
+ "Z=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5b as a single-character dictionary key",
+ "raw": [
+ "[=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5c as a single-character dictionary key",
+ "raw": [
+ "\\=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5d as a single-character dictionary key",
+ "raw": [
+ "]=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5e as a single-character dictionary key",
+ "raw": [
+ "^=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5f as a single-character dictionary key",
+ "raw": [
+ "_=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x60 as a single-character dictionary key",
+ "raw": [
+ "`=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x61 as a single-character dictionary key",
+ "raw": [
+ "a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x62 as a single-character dictionary key",
+ "raw": [
+ "b=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "b",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x63 as a single-character dictionary key",
+ "raw": [
+ "c=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "c",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x64 as a single-character dictionary key",
+ "raw": [
+ "d=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "d",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x65 as a single-character dictionary key",
+ "raw": [
+ "e=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "e",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x66 as a single-character dictionary key",
+ "raw": [
+ "f=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "f",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x67 as a single-character dictionary key",
+ "raw": [
+ "g=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "g",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x68 as a single-character dictionary key",
+ "raw": [
+ "h=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "h",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x69 as a single-character dictionary key",
+ "raw": [
+ "i=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "i",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6a as a single-character dictionary key",
+ "raw": [
+ "j=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "j",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6b as a single-character dictionary key",
+ "raw": [
+ "k=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "k",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6c as a single-character dictionary key",
+ "raw": [
+ "l=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "l",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6d as a single-character dictionary key",
+ "raw": [
+ "m=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "m",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6e as a single-character dictionary key",
+ "raw": [
+ "n=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "n",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6f as a single-character dictionary key",
+ "raw": [
+ "o=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "o",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x70 as a single-character dictionary key",
+ "raw": [
+ "p=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "p",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x71 as a single-character dictionary key",
+ "raw": [
+ "q=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "q",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x72 as a single-character dictionary key",
+ "raw": [
+ "r=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "r",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x73 as a single-character dictionary key",
+ "raw": [
+ "s=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "s",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x74 as a single-character dictionary key",
+ "raw": [
+ "t=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "t",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x75 as a single-character dictionary key",
+ "raw": [
+ "u=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "u",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x76 as a single-character dictionary key",
+ "raw": [
+ "v=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "v",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x77 as a single-character dictionary key",
+ "raw": [
+ "w=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "w",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x78 as a single-character dictionary key",
+ "raw": [
+ "x=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "x",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x79 as a single-character dictionary key",
+ "raw": [
+ "y=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "y",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7a as a single-character dictionary key",
+ "raw": [
+ "z=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "z",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7b as a single-character dictionary key",
+ "raw": [
+ "{=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7c as a single-character dictionary key",
+ "raw": [
+ "|=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7d as a single-character dictionary key",
+ "raw": [
+ "}=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7e as a single-character dictionary key",
+ "raw": [
+ "~=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7f as a single-character dictionary key",
+ "raw": [
+ "\u007f=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x00 in dictionary key",
+ "raw": [
+ "a\u0000a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x01 in dictionary key",
+ "raw": [
+ "a\u0001a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x02 in dictionary key",
+ "raw": [
+ "a\u0002a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x03 in dictionary key",
+ "raw": [
+ "a\u0003a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x04 in dictionary key",
+ "raw": [
+ "a\u0004a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x05 in dictionary key",
+ "raw": [
+ "a\u0005a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x06 in dictionary key",
+ "raw": [
+ "a\u0006a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x07 in dictionary key",
+ "raw": [
+ "a\u0007a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x08 in dictionary key",
+ "raw": [
+ "a\ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x09 in dictionary key",
+ "raw": [
+ "a\ta=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0a in dictionary key",
+ "raw": [
+ "a\na=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0b in dictionary key",
+ "raw": [
+ "a\u000ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0c in dictionary key",
+ "raw": [
+ "a\fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0d in dictionary key",
+ "raw": [
+ "a\ra=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0e in dictionary key",
+ "raw": [
+ "a\u000ea=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0f in dictionary key",
+ "raw": [
+ "a\u000fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x10 in dictionary key",
+ "raw": [
+ "a\u0010a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x11 in dictionary key",
+ "raw": [
+ "a\u0011a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x12 in dictionary key",
+ "raw": [
+ "a\u0012a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x13 in dictionary key",
+ "raw": [
+ "a\u0013a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x14 in dictionary key",
+ "raw": [
+ "a\u0014a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x15 in dictionary key",
+ "raw": [
+ "a\u0015a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x16 in dictionary key",
+ "raw": [
+ "a\u0016a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x17 in dictionary key",
+ "raw": [
+ "a\u0017a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x18 in dictionary key",
+ "raw": [
+ "a\u0018a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x19 in dictionary key",
+ "raw": [
+ "a\u0019a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1a in dictionary key",
+ "raw": [
+ "a\u001aa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1b in dictionary key",
+ "raw": [
+ "a\u001ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1c in dictionary key",
+ "raw": [
+ "a\u001ca=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1d in dictionary key",
+ "raw": [
+ "a\u001da=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1e in dictionary key",
+ "raw": [
+ "a\u001ea=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1f in dictionary key",
+ "raw": [
+ "a\u001fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x20 in dictionary key",
+ "raw": [
+ "a a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x21 in dictionary key",
+ "raw": [
+ "a!a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x22 in dictionary key",
+ "raw": [
+ "a\"a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x23 in dictionary key",
+ "raw": [
+ "a#a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x24 in dictionary key",
+ "raw": [
+ "a$a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x25 in dictionary key",
+ "raw": [
+ "a%a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x26 in dictionary key",
+ "raw": [
+ "a&a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x27 in dictionary key",
+ "raw": [
+ "a'a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x28 in dictionary key",
+ "raw": [
+ "a(a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x29 in dictionary key",
+ "raw": [
+ "a)a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2a in dictionary key",
+ "raw": [
+ "a*a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a*a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x2b in dictionary key",
+ "raw": [
+ "a+a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2c in dictionary key",
+ "raw": [
+ "a,a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ],
+ "canonical": [
+ "a=1"
+ ]
+ },
+ {
+ "name": "0x2d in dictionary key",
+ "raw": [
+ "a-a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a-a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x2e in dictionary key",
+ "raw": [
+ "a.a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a.a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x2f in dictionary key",
+ "raw": [
+ "a/a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x30 in dictionary key",
+ "raw": [
+ "a0a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a0a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x31 in dictionary key",
+ "raw": [
+ "a1a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a1a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x32 in dictionary key",
+ "raw": [
+ "a2a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a2a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x33 in dictionary key",
+ "raw": [
+ "a3a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a3a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x34 in dictionary key",
+ "raw": [
+ "a4a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a4a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x35 in dictionary key",
+ "raw": [
+ "a5a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a5a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x36 in dictionary key",
+ "raw": [
+ "a6a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a6a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x37 in dictionary key",
+ "raw": [
+ "a7a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a7a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x38 in dictionary key",
+ "raw": [
+ "a8a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a8a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x39 in dictionary key",
+ "raw": [
+ "a9a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a9a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x3a in dictionary key",
+ "raw": [
+ "a:a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3b in dictionary key",
+ "raw": [
+ "a;a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a",
+ [
+ true,
+ [
+ [
+ "a",
+ 1
+ ]
+ ]
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x3c in dictionary key",
+ "raw": [
+ "a<a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3d in dictionary key",
+ "raw": [
+ "a=a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3e in dictionary key",
+ "raw": [
+ "a>a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3f in dictionary key",
+ "raw": [
+ "a?a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x40 in dictionary key",
+ "raw": [
+ "a@a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x41 in dictionary key",
+ "raw": [
+ "aAa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x42 in dictionary key",
+ "raw": [
+ "aBa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x43 in dictionary key",
+ "raw": [
+ "aCa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x44 in dictionary key",
+ "raw": [
+ "aDa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x45 in dictionary key",
+ "raw": [
+ "aEa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x46 in dictionary key",
+ "raw": [
+ "aFa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x47 in dictionary key",
+ "raw": [
+ "aGa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x48 in dictionary key",
+ "raw": [
+ "aHa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x49 in dictionary key",
+ "raw": [
+ "aIa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4a in dictionary key",
+ "raw": [
+ "aJa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4b in dictionary key",
+ "raw": [
+ "aKa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4c in dictionary key",
+ "raw": [
+ "aLa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4d in dictionary key",
+ "raw": [
+ "aMa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4e in dictionary key",
+ "raw": [
+ "aNa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4f in dictionary key",
+ "raw": [
+ "aOa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x50 in dictionary key",
+ "raw": [
+ "aPa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x51 in dictionary key",
+ "raw": [
+ "aQa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x52 in dictionary key",
+ "raw": [
+ "aRa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x53 in dictionary key",
+ "raw": [
+ "aSa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x54 in dictionary key",
+ "raw": [
+ "aTa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x55 in dictionary key",
+ "raw": [
+ "aUa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x56 in dictionary key",
+ "raw": [
+ "aVa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x57 in dictionary key",
+ "raw": [
+ "aWa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x58 in dictionary key",
+ "raw": [
+ "aXa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x59 in dictionary key",
+ "raw": [
+ "aYa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5a in dictionary key",
+ "raw": [
+ "aZa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5b in dictionary key",
+ "raw": [
+ "a[a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5c in dictionary key",
+ "raw": [
+ "a\\a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5d in dictionary key",
+ "raw": [
+ "a]a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5e in dictionary key",
+ "raw": [
+ "a^a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5f in dictionary key",
+ "raw": [
+ "a_a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a_a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x60 in dictionary key",
+ "raw": [
+ "a`a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x61 in dictionary key",
+ "raw": [
+ "aaa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aaa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x62 in dictionary key",
+ "raw": [
+ "aba=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aba",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x63 in dictionary key",
+ "raw": [
+ "aca=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aca",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x64 in dictionary key",
+ "raw": [
+ "ada=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ada",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x65 in dictionary key",
+ "raw": [
+ "aea=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aea",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x66 in dictionary key",
+ "raw": [
+ "afa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "afa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x67 in dictionary key",
+ "raw": [
+ "aga=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aga",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x68 in dictionary key",
+ "raw": [
+ "aha=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aha",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x69 in dictionary key",
+ "raw": [
+ "aia=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aia",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6a in dictionary key",
+ "raw": [
+ "aja=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aja",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6b in dictionary key",
+ "raw": [
+ "aka=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aka",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6c in dictionary key",
+ "raw": [
+ "ala=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ala",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6d in dictionary key",
+ "raw": [
+ "ama=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ama",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6e in dictionary key",
+ "raw": [
+ "ana=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ana",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6f in dictionary key",
+ "raw": [
+ "aoa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aoa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x70 in dictionary key",
+ "raw": [
+ "apa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "apa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x71 in dictionary key",
+ "raw": [
+ "aqa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aqa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x72 in dictionary key",
+ "raw": [
+ "ara=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ara",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x73 in dictionary key",
+ "raw": [
+ "asa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "asa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x74 in dictionary key",
+ "raw": [
+ "ata=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ata",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x75 in dictionary key",
+ "raw": [
+ "aua=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aua",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x76 in dictionary key",
+ "raw": [
+ "ava=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ava",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x77 in dictionary key",
+ "raw": [
+ "awa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "awa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x78 in dictionary key",
+ "raw": [
+ "axa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "axa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x79 in dictionary key",
+ "raw": [
+ "aya=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aya",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7a in dictionary key",
+ "raw": [
+ "aza=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aza",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7b in dictionary key",
+ "raw": [
+ "a{a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7c in dictionary key",
+ "raw": [
+ "a|a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7d in dictionary key",
+ "raw": [
+ "a}a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7e in dictionary key",
+ "raw": [
+ "a~a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7f in dictionary key",
+ "raw": [
+ "a\u007fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x00 starting a dictionary key",
+ "raw": [
+ "\u0000a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x01 starting a dictionary key",
+ "raw": [
+ "\u0001a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x02 starting a dictionary key",
+ "raw": [
+ "\u0002a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x03 starting a dictionary key",
+ "raw": [
+ "\u0003a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x04 starting a dictionary key",
+ "raw": [
+ "\u0004a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x05 starting a dictionary key",
+ "raw": [
+ "\u0005a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x06 starting a dictionary key",
+ "raw": [
+ "\u0006a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x07 starting a dictionary key",
+ "raw": [
+ "\u0007a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x08 starting a dictionary key",
+ "raw": [
+ "\ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x09 starting a dictionary key",
+ "raw": [
+ "\ta=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0a starting a dictionary key",
+ "raw": [
+ "\na=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0b starting a dictionary key",
+ "raw": [
+ "\u000ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0c starting a dictionary key",
+ "raw": [
+ "\fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0d starting a dictionary key",
+ "raw": [
+ "\ra=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0e starting a dictionary key",
+ "raw": [
+ "\u000ea=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x0f starting a dictionary key",
+ "raw": [
+ "\u000fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x10 starting a dictionary key",
+ "raw": [
+ "\u0010a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x11 starting a dictionary key",
+ "raw": [
+ "\u0011a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x12 starting a dictionary key",
+ "raw": [
+ "\u0012a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x13 starting a dictionary key",
+ "raw": [
+ "\u0013a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x14 starting a dictionary key",
+ "raw": [
+ "\u0014a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x15 starting a dictionary key",
+ "raw": [
+ "\u0015a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x16 starting a dictionary key",
+ "raw": [
+ "\u0016a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x17 starting a dictionary key",
+ "raw": [
+ "\u0017a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x18 starting a dictionary key",
+ "raw": [
+ "\u0018a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x19 starting a dictionary key",
+ "raw": [
+ "\u0019a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1a starting a dictionary key",
+ "raw": [
+ "\u001aa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1b starting a dictionary key",
+ "raw": [
+ "\u001ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1c starting a dictionary key",
+ "raw": [
+ "\u001ca=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1d starting a dictionary key",
+ "raw": [
+ "\u001da=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1e starting a dictionary key",
+ "raw": [
+ "\u001ea=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x1f starting a dictionary key",
+ "raw": [
+ "\u001fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x20 starting a dictionary key",
+ "raw": [
+ " a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ],
+ "canonical": [
+ "a=1"
+ ]
+ },
+ {
+ "name": "0x21 starting a dictionary key",
+ "raw": [
+ "!a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x22 starting a dictionary key",
+ "raw": [
+ "\"a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x23 starting a dictionary key",
+ "raw": [
+ "#a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x24 starting a dictionary key",
+ "raw": [
+ "$a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x25 starting a dictionary key",
+ "raw": [
+ "%a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x26 starting a dictionary key",
+ "raw": [
+ "&a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x27 starting a dictionary key",
+ "raw": [
+ "'a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x28 starting a dictionary key",
+ "raw": [
+ "(a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x29 starting a dictionary key",
+ "raw": [
+ ")a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2a starting a dictionary key",
+ "raw": [
+ "*a=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "*a",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x2b starting a dictionary key",
+ "raw": [
+ "+a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2c starting a dictionary key",
+ "raw": [
+ ",a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2d starting a dictionary key",
+ "raw": [
+ "-a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2e starting a dictionary key",
+ "raw": [
+ ".a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x2f starting a dictionary key",
+ "raw": [
+ "/a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x30 starting a dictionary key",
+ "raw": [
+ "0a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x31 starting a dictionary key",
+ "raw": [
+ "1a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x32 starting a dictionary key",
+ "raw": [
+ "2a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x33 starting a dictionary key",
+ "raw": [
+ "3a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x34 starting a dictionary key",
+ "raw": [
+ "4a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x35 starting a dictionary key",
+ "raw": [
+ "5a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x36 starting a dictionary key",
+ "raw": [
+ "6a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x37 starting a dictionary key",
+ "raw": [
+ "7a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x38 starting a dictionary key",
+ "raw": [
+ "8a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x39 starting a dictionary key",
+ "raw": [
+ "9a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3a starting a dictionary key",
+ "raw": [
+ ":a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3b starting a dictionary key",
+ "raw": [
+ ";a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3c starting a dictionary key",
+ "raw": [
+ "<a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3d starting a dictionary key",
+ "raw": [
+ "=a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3e starting a dictionary key",
+ "raw": [
+ ">a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x3f starting a dictionary key",
+ "raw": [
+ "?a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x40 starting a dictionary key",
+ "raw": [
+ "@a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x41 starting a dictionary key",
+ "raw": [
+ "Aa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x42 starting a dictionary key",
+ "raw": [
+ "Ba=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x43 starting a dictionary key",
+ "raw": [
+ "Ca=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x44 starting a dictionary key",
+ "raw": [
+ "Da=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x45 starting a dictionary key",
+ "raw": [
+ "Ea=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x46 starting a dictionary key",
+ "raw": [
+ "Fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x47 starting a dictionary key",
+ "raw": [
+ "Ga=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x48 starting a dictionary key",
+ "raw": [
+ "Ha=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x49 starting a dictionary key",
+ "raw": [
+ "Ia=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4a starting a dictionary key",
+ "raw": [
+ "Ja=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4b starting a dictionary key",
+ "raw": [
+ "Ka=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4c starting a dictionary key",
+ "raw": [
+ "La=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4d starting a dictionary key",
+ "raw": [
+ "Ma=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4e starting a dictionary key",
+ "raw": [
+ "Na=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x4f starting a dictionary key",
+ "raw": [
+ "Oa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x50 starting a dictionary key",
+ "raw": [
+ "Pa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x51 starting a dictionary key",
+ "raw": [
+ "Qa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x52 starting a dictionary key",
+ "raw": [
+ "Ra=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x53 starting a dictionary key",
+ "raw": [
+ "Sa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x54 starting a dictionary key",
+ "raw": [
+ "Ta=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x55 starting a dictionary key",
+ "raw": [
+ "Ua=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x56 starting a dictionary key",
+ "raw": [
+ "Va=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x57 starting a dictionary key",
+ "raw": [
+ "Wa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x58 starting a dictionary key",
+ "raw": [
+ "Xa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x59 starting a dictionary key",
+ "raw": [
+ "Ya=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5a starting a dictionary key",
+ "raw": [
+ "Za=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5b starting a dictionary key",
+ "raw": [
+ "[a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5c starting a dictionary key",
+ "raw": [
+ "\\a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5d starting a dictionary key",
+ "raw": [
+ "]a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5e starting a dictionary key",
+ "raw": [
+ "^a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x5f starting a dictionary key",
+ "raw": [
+ "_a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x60 starting a dictionary key",
+ "raw": [
+ "`a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x61 starting a dictionary key",
+ "raw": [
+ "aa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "aa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x62 starting a dictionary key",
+ "raw": [
+ "ba=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ba",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x63 starting a dictionary key",
+ "raw": [
+ "ca=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ca",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x64 starting a dictionary key",
+ "raw": [
+ "da=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "da",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x65 starting a dictionary key",
+ "raw": [
+ "ea=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ea",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x66 starting a dictionary key",
+ "raw": [
+ "fa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "fa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x67 starting a dictionary key",
+ "raw": [
+ "ga=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ga",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x68 starting a dictionary key",
+ "raw": [
+ "ha=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ha",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x69 starting a dictionary key",
+ "raw": [
+ "ia=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ia",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6a starting a dictionary key",
+ "raw": [
+ "ja=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ja",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6b starting a dictionary key",
+ "raw": [
+ "ka=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ka",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6c starting a dictionary key",
+ "raw": [
+ "la=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "la",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6d starting a dictionary key",
+ "raw": [
+ "ma=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ma",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6e starting a dictionary key",
+ "raw": [
+ "na=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "na",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x6f starting a dictionary key",
+ "raw": [
+ "oa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "oa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x70 starting a dictionary key",
+ "raw": [
+ "pa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "pa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x71 starting a dictionary key",
+ "raw": [
+ "qa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "qa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x72 starting a dictionary key",
+ "raw": [
+ "ra=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ra",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x73 starting a dictionary key",
+ "raw": [
+ "sa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "sa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x74 starting a dictionary key",
+ "raw": [
+ "ta=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ta",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x75 starting a dictionary key",
+ "raw": [
+ "ua=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ua",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x76 starting a dictionary key",
+ "raw": [
+ "va=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "va",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x77 starting a dictionary key",
+ "raw": [
+ "wa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "wa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x78 starting a dictionary key",
+ "raw": [
+ "xa=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "xa",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x79 starting a dictionary key",
+ "raw": [
+ "ya=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "ya",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7a starting a dictionary key",
+ "raw": [
+ "za=1"
+ ],
+ "header_type": "dictionary",
+ "expected": [
+ [
+ "za",
+ [
+ 1,
+ []
+ ]
+ ]
+ ]
+ },
+ {
+ "name": "0x7b starting a dictionary key",
+ "raw": [
+ "{a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7c starting a dictionary key",
+ "raw": [
+ "|a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7d starting a dictionary key",
+ "raw": [
+ "}a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7e starting a dictionary key",
+ "raw": [
+ "~a=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x7f starting a dictionary key",
+ "raw": [
+ "\u007fa=1"
+ ],
+ "header_type": "dictionary",
+ "must_fail": true
+ },
+ {
+ "name": "0x00 in parameterised list key",
+ "raw": [
+ "foo; a\u0000a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x01 in parameterised list key",
+ "raw": [
+ "foo; a\u0001a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x02 in parameterised list key",
+ "raw": [
+ "foo; a\u0002a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x03 in parameterised list key",
+ "raw": [
+ "foo; a\u0003a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x04 in parameterised list key",
+ "raw": [
+ "foo; a\u0004a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x05 in parameterised list key",
+ "raw": [
+ "foo; a\u0005a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x06 in parameterised list key",
+ "raw": [
+ "foo; a\u0006a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x07 in parameterised list key",
+ "raw": [
+ "foo; a\u0007a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x08 in parameterised list key",
+ "raw": [
+ "foo; a\ba=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x09 in parameterised list key",
+ "raw": [
+ "foo; a\ta=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0a in parameterised list key",
+ "raw": [
+ "foo; a\na=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0b in parameterised list key",
+ "raw": [
+ "foo; a\u000ba=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0c in parameterised list key",
+ "raw": [
+ "foo; a\fa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0d in parameterised list key",
+ "raw": [
+ "foo; a\ra=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0e in parameterised list key",
+ "raw": [
+ "foo; a\u000ea=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x0f in parameterised list key",
+ "raw": [
+ "foo; a\u000fa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x10 in parameterised list key",
+ "raw": [
+ "foo; a\u0010a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x11 in parameterised list key",
+ "raw": [
+ "foo; a\u0011a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x12 in parameterised list key",
+ "raw": [
+ "foo; a\u0012a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x13 in parameterised list key",
+ "raw": [
+ "foo; a\u0013a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x14 in parameterised list key",
+ "raw": [
+ "foo; a\u0014a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x15 in parameterised list key",
+ "raw": [
+ "foo; a\u0015a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x16 in parameterised list key",
+ "raw": [
+ "foo; a\u0016a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x17 in parameterised list key",
+ "raw": [
+ "foo; a\u0017a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x18 in parameterised list key",
+ "raw": [
+ "foo; a\u0018a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x19 in parameterised list key",
+ "raw": [
+ "foo; a\u0019a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1a in parameterised list key",
+ "raw": [
+ "foo; a\u001aa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1b in parameterised list key",
+ "raw": [
+ "foo; a\u001ba=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1c in parameterised list key",
+ "raw": [
+ "foo; a\u001ca=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1d in parameterised list key",
+ "raw": [
+ "foo; a\u001da=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1e in parameterised list key",
+ "raw": [
+ "foo; a\u001ea=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x1f in parameterised list key",
+ "raw": [
+ "foo; a\u001fa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x20 in parameterised list key",
+ "raw": [
+ "foo; a a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x21 in parameterised list key",
+ "raw": [
+ "foo; a!a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x22 in parameterised list key",
+ "raw": [
+ "foo; a\"a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x23 in parameterised list key",
+ "raw": [
+ "foo; a#a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x24 in parameterised list key",
+ "raw": [
+ "foo; a$a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x25 in parameterised list key",
+ "raw": [
+ "foo; a%a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x26 in parameterised list key",
+ "raw": [
+ "foo; a&a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x27 in parameterised list key",
+ "raw": [
+ "foo; a'a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x28 in parameterised list key",
+ "raw": [
+ "foo; a(a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x29 in parameterised list key",
+ "raw": [
+ "foo; a)a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x2a in parameterised list key",
+ "raw": [
+ "foo; a*a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a*a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a*a=1"
+ ]
+ },
+ {
+ "name": "0x2b in parameterised list key",
+ "raw": [
+ "foo; a+a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x2c in parameterised list key",
+ "raw": [
+ "foo; a,a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x2d in parameterised list key",
+ "raw": [
+ "foo; a-a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a-a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a-a=1"
+ ]
+ },
+ {
+ "name": "0x2e in parameterised list key",
+ "raw": [
+ "foo; a.a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a.a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a.a=1"
+ ]
+ },
+ {
+ "name": "0x2f in parameterised list key",
+ "raw": [
+ "foo; a/a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x30 in parameterised list key",
+ "raw": [
+ "foo; a0a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a0a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a0a=1"
+ ]
+ },
+ {
+ "name": "0x31 in parameterised list key",
+ "raw": [
+ "foo; a1a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a1a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a1a=1"
+ ]
+ },
+ {
+ "name": "0x32 in parameterised list key",
+ "raw": [
+ "foo; a2a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a2a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a2a=1"
+ ]
+ },
+ {
+ "name": "0x33 in parameterised list key",
+ "raw": [
+ "foo; a3a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a3a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a3a=1"
+ ]
+ },
+ {
+ "name": "0x34 in parameterised list key",
+ "raw": [
+ "foo; a4a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a4a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a4a=1"
+ ]
+ },
+ {
+ "name": "0x35 in parameterised list key",
+ "raw": [
+ "foo; a5a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a5a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a5a=1"
+ ]
+ },
+ {
+ "name": "0x36 in parameterised list key",
+ "raw": [
+ "foo; a6a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a6a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a6a=1"
+ ]
+ },
+ {
+ "name": "0x37 in parameterised list key",
+ "raw": [
+ "foo; a7a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a7a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a7a=1"
+ ]
+ },
+ {
+ "name": "0x38 in parameterised list key",
+ "raw": [
+ "foo; a8a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a8a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a8a=1"
+ ]
+ },
+ {
+ "name": "0x39 in parameterised list key",
+ "raw": [
+ "foo; a9a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a9a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a9a=1"
+ ]
+ },
+ {
+ "name": "0x3a in parameterised list key",
+ "raw": [
+ "foo; a:a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x3b in parameterised list key",
+ "raw": [
+ "foo; a;a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a=1"
+ ]
+ },
+ {
+ "name": "0x3c in parameterised list key",
+ "raw": [
+ "foo; a<a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x3d in parameterised list key",
+ "raw": [
+ "foo; a=a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x3e in parameterised list key",
+ "raw": [
+ "foo; a>a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x3f in parameterised list key",
+ "raw": [
+ "foo; a?a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x40 in parameterised list key",
+ "raw": [
+ "foo; a@a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x41 in parameterised list key",
+ "raw": [
+ "foo; aAa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x42 in parameterised list key",
+ "raw": [
+ "foo; aBa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x43 in parameterised list key",
+ "raw": [
+ "foo; aCa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x44 in parameterised list key",
+ "raw": [
+ "foo; aDa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x45 in parameterised list key",
+ "raw": [
+ "foo; aEa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x46 in parameterised list key",
+ "raw": [
+ "foo; aFa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x47 in parameterised list key",
+ "raw": [
+ "foo; aGa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x48 in parameterised list key",
+ "raw": [
+ "foo; aHa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x49 in parameterised list key",
+ "raw": [
+ "foo; aIa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4a in parameterised list key",
+ "raw": [
+ "foo; aJa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4b in parameterised list key",
+ "raw": [
+ "foo; aKa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4c in parameterised list key",
+ "raw": [
+ "foo; aLa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4d in parameterised list key",
+ "raw": [
+ "foo; aMa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4e in parameterised list key",
+ "raw": [
+ "foo; aNa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x4f in parameterised list key",
+ "raw": [
+ "foo; aOa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x50 in parameterised list key",
+ "raw": [
+ "foo; aPa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x51 in parameterised list key",
+ "raw": [
+ "foo; aQa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x52 in parameterised list key",
+ "raw": [
+ "foo; aRa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x53 in parameterised list key",
+ "raw": [
+ "foo; aSa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x54 in parameterised list key",
+ "raw": [
+ "foo; aTa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x55 in parameterised list key",
+ "raw": [
+ "foo; aUa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x56 in parameterised list key",
+ "raw": [
+ "foo; aVa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x57 in parameterised list key",
+ "raw": [
+ "foo; aWa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x58 in parameterised list key",
+ "raw": [
+ "foo; aXa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x59 in parameterised list key",
+ "raw": [
+ "foo; aYa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5a in parameterised list key",
+ "raw": [
+ "foo; aZa=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5b in parameterised list key",
+ "raw": [
+ "foo; a[a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5c in parameterised list key",
+ "raw": [
+ "foo; a\\a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5d in parameterised list key",
+ "raw": [
+ "foo; a]a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5e in parameterised list key",
+ "raw": [
+ "foo; a^a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x5f in parameterised list key",
+ "raw": [
+ "foo; a_a=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "a_a",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;a_a=1"
+ ]
+ },
+ {
+ "name": "0x60 in parameterised list key",
+ "raw": [
+ "foo; a`a=1"
+ ],
+ "header_type": "list",
+ "must_fail": true
+ },
+ {
+ "name": "0x61 in parameterised list key",
+ "raw": [
+ "foo; aaa=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aaa",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aaa=1"
+ ]
+ },
+ {
+ "name": "0x62 in parameterised list key",
+ "raw": [
+ "foo; aba=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aba",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aba=1"
+ ]
+ },
+ {
+ "name": "0x63 in parameterised list key",
+ "raw": [
+ "foo; aca=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aca",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aca=1"
+ ]
+ },
+ {
+ "name": "0x64 in parameterised list key",
+ "raw": [
+ "foo; ada=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "ada",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;ada=1"
+ ]
+ },
+ {
+ "name": "0x65 in parameterised list key",
+ "raw": [
+ "foo; aea=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aea",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aea=1"
+ ]
+ },
+ {
+ "name": "0x66 in parameterised list key",
+ "raw": [
+ "foo; afa=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "afa",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;afa=1"
+ ]
+ },
+ {
+ "name": "0x67 in parameterised list key",
+ "raw": [
+ "foo; aga=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aga",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aga=1"
+ ]
+ },
+ {
+ "name": "0x68 in parameterised list key",
+ "raw": [
+ "foo; aha=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aha",
+ 1
+ ]
+ ]
+ ]
+ ],
+ "canonical": [
+ "foo;aha=1"
+ ]
+ },
+ {
+ "name": "0x69 in parameterised list key",
+ "raw": [
+ "foo; aia=1"
+ ],
+ "header_type": "list",
+ "expected": [
+ [
+ {
+ "__type": "token",
+ "value": "foo"
+ },
+ [
+ [
+ "aia",
+ 1
+ ]
... 47338 lines suppressed ...