Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibWeb: Ensure Headers API can handle non-ascii characters #2814

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions Libraries/LibWeb/Fetch/Headers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <LibWeb/Bindings/HeadersPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Fetch/Headers.h>
#include <LibWeb/Infra/Strings.h>

namespace Web::Fetch {

Expand Down Expand Up @@ -56,10 +57,7 @@ void Headers::visit_edges(JS::Cell::Visitor& visitor)
WebIDL::ExceptionOr<void> Headers::append(String const& name_string, String const& value_string)
{
// The append(name, value) method steps are to append (name, value) to this.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name_string.bytes())),
.value = MUST(ByteBuffer::copy(value_string.bytes())),
};
auto header = Infrastructure::Header::from_string_pair(name_string, value_string);
TRY(append(move(header)));
return {};
}
Expand Down Expand Up @@ -106,7 +104,7 @@ WebIDL::ExceptionOr<Optional<String>> Headers::get(String const& name_string)

// 2. Return the result of getting name from this’s header list.
auto byte_buffer = m_header_list->get(name);
return byte_buffer.has_value() ? MUST(String::from_utf8(*byte_buffer)) : Optional<String> {};
return byte_buffer.has_value() ? Infra::isomorphic_decode(*byte_buffer) : Optional<String> {};
}

// https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
Expand All @@ -123,7 +121,7 @@ Vector<String> Headers::get_set_cookie()
// `Set-Cookie`, in order.
for (auto const& header : *m_header_list) {
if (StringView { header.name }.equals_ignoring_ascii_case("Set-Cookie"sv))
values.append(MUST(String::from_utf8(header.value)));
values.append(Infra::isomorphic_decode(header.value));
}
return values;
}
Expand Down Expand Up @@ -152,10 +150,7 @@ WebIDL::ExceptionOr<void> Headers::set(String const& name_string, String const&
// 1. Normalize value.
auto normalized_value = Infrastructure::normalize_header_value(value);

auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name)),
.value = move(normalized_value),
};
auto header = Infrastructure::Header::from_string_pair(name, normalized_value);

// 2. If validating (name, value) for headers returns false, then return.
if (!TRY(validate(header)))
Expand Down Expand Up @@ -197,7 +192,7 @@ JS::ThrowCompletionOr<void> Headers::for_each(ForEachCallback callback)
auto const& pair = pairs[i];

// 2. Invoke idlCallback with « pair’s value, pair’s key, idlObject » and with thisArg as the callback this value.
TRY(callback(MUST(String::from_utf8(pair.name)), MUST(String::from_utf8(pair.value))));
TRY(callback(Infra::isomorphic_decode(pair.name), Infra::isomorphic_decode(pair.value)));

// 3. Set pairs to idlObject’s current list of value pairs to iterate over. (It might have changed.)
pairs = value_pairs_to_iterate_over();
Expand Down
5 changes: 3 additions & 2 deletions Libraries/LibWeb/Fetch/HeadersIterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <LibWeb/Bindings/HeadersIteratorPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Fetch/HeadersIterator.h>
#include <LibWeb/Infra/Strings.h>

namespace Web::Bindings {

Expand Down Expand Up @@ -65,8 +66,8 @@ GC::Ref<JS::Object> HeadersIterator::next()
return create_iterator_result_object(vm(), JS::js_undefined(), true);

auto const& pair = pairs[m_index++];
StringView pair_name { pair.name };
StringView pair_value { pair.value };
auto pair_name = Infra::isomorphic_decode(pair.name);
auto pair_value = Infra::isomorphic_decode(pair.value);

switch (m_iteration_kind) {
case JS::Object::PropertyKind::Key:
Expand Down
34 changes: 34 additions & 0 deletions Tests/LibWeb/Text/expected/Fetch/fetch-headers-non-ascii.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--------------------------------
Headers constructor
--------------------------------
Accept: before-æøå-after
X-Test: before-ß-after

--------------------------------
Headers.append()
--------------------------------
Accept: before-æøå-after
X-Test: before-ß-after

--------------------------------
Headers.set()
--------------------------------
Accept: before-æøå-after
X-Test: before-ß-after

--------------------------------
Headers.getSetCookie()
--------------------------------
Set-Cookie: before-æøå-after

--------------------------------
Headers iterator
--------------------------------
accept: before-æøå-after
x-test: before-ß-after

--------------------------------
Headers.forEach()
--------------------------------
accept: before-æøå-after
x-test: before-ß-after
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Harness status: OK

Found 3 tests

3 Pass
Pass Create headers with not normalized values
Pass Check append method with not normalized values
Pass Check set method with not normalized values
66 changes: 66 additions & 0 deletions Tests/LibWeb/Text/input/Fetch/fetch-headers-non-ascii.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
// This test ensures that the Headers get() methods can handle non-ASCII latin1 characters: code points U+0080-U+00FF.
test(() => {
println("--------------------------------");
println("Headers constructor")
println("--------------------------------");
const headers = new Headers({
"Accept": "before-æøå-after",
"X-Test": "before-ß-after"
});
println("Accept: " + headers.get("Accept"));
println("X-Test: " + headers.get("X-Test"));

println("\n--------------------------------");
println("Headers.append()")
println("--------------------------------");
const headers2 = new Headers();
headers2.append("Accept", "before-æøå-after");
headers2.append("X-Test", "before-ß-after");
println("Accept: " + headers2.get("Accept"));
println("X-Test: " + headers2.get("X-Test"));

println("\n--------------------------------");
println("Headers.set()")
println("--------------------------------");
const headers3 = new Headers({
"X-Test": "should be overwritten"
});
headers3.set("Accept", "before-æøå-after");
headers3.set("X-Test", "before-ß-after");
println("Accept: " + headers3.get("Accept"));
println("X-Test: " + headers3.get("X-Test"));

println("\n--------------------------------");
println("Headers.getSetCookie()")
println("--------------------------------");
const headers4 = new Headers({
"Set-Cookie": "before-æøå-after",
});
println("Set-Cookie: " + headers4.getSetCookie());

println("\n--------------------------------");
println("Headers iterator")
println("--------------------------------");
const headers5 = new Headers({
"Accept": "before-æøå-after",
"X-Test": "before-ß-after"
});
for (const [key, value] of headers5) {
println(`${key}: ${value}`);
}

println("\n--------------------------------");
println("Headers.forEach()")
println("--------------------------------");
const headers6 = new Headers({
"Accept": "before-æøå-after",
"X-Test": "before-ß-after"
});
headers6.forEach((value, key) => {
println(`${key}: ${value}`);
});
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Headers normalize values</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>

<div id=log></div>
<script src="../../../fetch/api/headers/headers-normalize.any.js"></script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// META: title=Headers normalize values
// META: global=window,worker

"use strict";

const expectations = {
"name1": [" space ", "space"],
"name2": ["\ttab\t", "tab"],
"name3": [" spaceAndTab\t", "spaceAndTab"],
"name4": ["\r\n newLine", "newLine"], //obs-fold cases
"name5": ["newLine\r\n ", "newLine"],
"name6": ["\r\n\tnewLine", "newLine"],
"name7": ["\t\f\tnewLine\n", "\f\tnewLine"],
"name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space
};

test(function () {
const headerDict = Object.fromEntries(
Object.entries(expectations).map(([name, [actual]]) => [name, actual]),
);
var headers = new Headers(headerDict);
for (const name in expectations) {
const expected = expectations[name][1];
assert_equals(
headers.get(name),
expected,
"name: " + name + " has normalized value: " + expected,
);
}
}, "Create headers with not normalized values");

test(function () {
var headers = new Headers();
for (const name in expectations) {
headers.append(name, expectations[name][0]);
const expected = expectations[name][1];
assert_equals(
headers.get(name),
expected,
"name: " + name + " has value: " + expected,
);
}
}, "Check append method with not normalized values");

test(function () {
var headers = new Headers();
for (const name in expectations) {
headers.set(name, expectations[name][0]);
const expected = expectations[name][1];
assert_equals(
headers.get(name),
expected,
"name: " + name + " has value: " + expected,
);
}
}, "Check set method with not normalized values");
Loading