blob: a336c59dabc6ed05b6fc5cb1122fb0055cee1d7d [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/structured_headers.h"
#include <optional>
#include <string_view>
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/fuzztest/src/fuzztest/fuzztest.h"
namespace net::structured_headers {
class StructuredHeadersLenientTest : public testing::TestWithParam<bool> {
public:
StructuredHeadersLenientTest() {
if (GetParam()) {
feature_list_.InitAndEnableFeature(kStructuredHeadersInRust);
} else {
feature_list_.InitAndDisableFeature(kStructuredHeadersInRust);
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(,
StructuredHeadersLenientTest,
testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "Rust" : "Cpp";
});
TEST_P(StructuredHeadersLenientTest, TrailingDecimal) {
// RFC 8941 requires at least one digit after the decimal point.
// Legacy Quiche allows trailing dots (e.g. "1.") if followed by EOF.
// Rust backend replicates this in lenient mode.
std::optional<ParameterizedItem> result = ParseItem("1.");
ASSERT_TRUE(result.has_value());
EXPECT_TRUE(result->item.is_decimal());
EXPECT_DOUBLE_EQ(result->item.GetDecimal(), 1.0);
}
TEST_P(StructuredHeadersLenientTest, ByteSequenceWhitespace) {
// Legacy Quiche/Abseil skips ASCII whitespace in byte sequences,
// provided it doesn't interfere with the 4-byte block boundary checks
// in its synthesized padding logic.
std::optional<ParameterizedItem> result = ParseItem(": Zm9v :");
ASSERT_TRUE(result.has_value());
const std::string* value = result->item.GetIfByteSequence();
ASSERT_TRUE(value);
EXPECT_EQ(*value, "foo");
// Replicate Abseil's ascii_isspace behavior, which includes vertical tab
// (0x0b). The raw length between colons must be a multiple of 4 so that
// Quiche doesn't synthesize too much padding (e.g. 8 chars results in no
// added padding).
std::optional<ParameterizedItem> result2 = ParseItem(":mmmM\ncm\x0b:");
ASSERT_TRUE(result2.has_value());
EXPECT_TRUE(result2->item.is_byte_sequence());
}
TEST_P(StructuredHeadersLenientTest, ByteSequenceDotAsPadding) {
// Abseil allows '.' as an alias for '=' in base64.
// ":Zm9." is 5 chars (including colons), so internal string is "Zm9.".
// Legacy Quiche pads to "Zm9.". Standard SH would require ":Zm9=:".
std::optional<ParameterizedItem> result = ParseItem(":Zm9.:");
ASSERT_TRUE(result.has_value());
const std::string* value = result->item.GetIfByteSequence();
ASSERT_TRUE(value);
EXPECT_EQ(*value, "fo");
}
void ParseItemParity(const std::string_view input) {
std::optional<ParameterizedItem> cpp_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(kStructuredHeadersInRust);
cpp_result = ParseItem(input);
}
std::optional<ParameterizedItem> rust_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kStructuredHeadersInRust);
rust_result = ParseItem(input);
}
ASSERT_EQ(cpp_result.has_value(), rust_result.has_value())
<< "ParseItem discrepancy for input: " << input;
if (cpp_result.has_value()) {
EXPECT_EQ(*cpp_result, *rust_result)
<< "ParseItem value mismatch for input: " << input
<< "\nCPP: " << *net::structured_headers::SerializeItem(*cpp_result)
<< "\nRust: " << *net::structured_headers::SerializeItem(*rust_result);
}
}
FUZZ_TEST(StructuredHeadersTest, ParseItemParity)
.WithSeeds({
R"(1)", // Integer
R"(1.0)", // Decimal
R"(token;abc=123;def)", // Parameterized Token (implicit boolean param)
R"("string")", // String
R"(:aGVsbG8=:)", // Byte Sequence
R"(?1)", // Boolean
});
void ParseListParity(const std::string_view input) {
std::optional<List> cpp_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(kStructuredHeadersInRust);
cpp_result = ParseList(input);
}
std::optional<List> rust_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kStructuredHeadersInRust);
rust_result = ParseList(input);
}
ASSERT_EQ(cpp_result.has_value(), rust_result.has_value())
<< "ParseList discrepancy for input: " << input;
if (cpp_result.has_value()) {
EXPECT_EQ(*cpp_result, *rust_result)
<< "ParseList value mismatch for input: " << input
<< "\nCPP: " << *net::structured_headers::SerializeList(*cpp_result)
<< "\nRust: " << *net::structured_headers::SerializeList(*rust_result);
}
}
FUZZ_TEST(StructuredHeadersTest, ParseListParity)
.WithSeeds({
R"(foo)", // Single item
R"(())", // Empty inner list
R"((foo))", // Inner list with one item
R"((foo bar))", // Inner list with multiple items
R"(foo;a=1)", // Single item with parameter
R"((foo bar);a=1)", // Inner list with parameter
});
void ParseDictionaryParity(const std::string_view input) {
std::optional<Dictionary> cpp_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(kStructuredHeadersInRust);
cpp_result = ParseDictionary(input);
}
std::optional<Dictionary> rust_result;
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kStructuredHeadersInRust);
rust_result = ParseDictionary(input);
}
ASSERT_EQ(cpp_result.has_value(), rust_result.has_value())
<< "ParseDictionary discrepancy for input: " << input;
if (cpp_result.has_value()) {
EXPECT_EQ(*cpp_result, *rust_result)
<< "ParseDictionary value mismatch for input: " << input << "\nCPP: "
<< *net::structured_headers::SerializeDictionary(*cpp_result)
<< "\nRust: "
<< *net::structured_headers::SerializeDictionary(*rust_result);
}
}
FUZZ_TEST(StructuredHeadersTest, ParseDictionaryParity)
.WithSeeds({
R"(en="Applepie", da=:w4ZibGV0w6ZydGUK:)", //
R"(a=1)", //
R"(a=(1 2))", //
R"(a=1, b=2)", //
R"(a=1;b=2, c=3;d=4)", //
R"(a)", // Implicit boolean value
});
} // namespace net::structured_headers