mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-24 16:40:21 +00:00
LibWeb/Fetch: Implement blob range section of scheme fetch specification
This commit is contained in:
parent
84f673515b
commit
13f349aea2
Notes:
github-actions[bot]
2024-11-21 00:58:58 +00:00
Author: https://github.com/rmg-x Commit: https://github.com/LadybirdBrowser/ladybird/commit/13f349aea24 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2434 Reviewed-by: https://github.com/tcl3 ✅
4 changed files with 359 additions and 15 deletions
|
@ -860,33 +860,79 @@ WebIDL::ExceptionOr<GC::Ref<PendingResponse>> scheme_fetch(JS::Realm& realm, Inf
|
|||
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
|
||||
response->header_list()->append(move(content_type_header));
|
||||
}
|
||||
// FIXME: 9. Otherwise:
|
||||
// 9. Otherwise:
|
||||
else {
|
||||
// 1. Set response’s range-requested flag.
|
||||
response->set_range_requested(true);
|
||||
|
||||
// 2. Let rangeHeader be the result of getting `Range` from request’s header list.
|
||||
auto const range_header = request->header_list()->get("Range"sv.bytes()).value_or(ByteBuffer {});
|
||||
|
||||
// 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
|
||||
auto maybe_range_value = Infrastructure::parse_single_range_header_value(range_header, true);
|
||||
|
||||
// 4. If rangeValue is failure, then return a network error.
|
||||
if (!maybe_range_value.has_value())
|
||||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Failed to parse single range header value"sv));
|
||||
|
||||
// 5. Let (rangeStart, rangeEnd) be rangeValue.
|
||||
auto& [range_start, range_end] = maybe_range_value.value();
|
||||
|
||||
// 6. If rangeStart is null:
|
||||
// 1. Set rangeStart to fullLength − rangeEnd.
|
||||
// 2. Set rangeEnd to rangeStart + rangeEnd − 1.
|
||||
if (!range_start.has_value()) {
|
||||
VERIFY(range_end.has_value());
|
||||
|
||||
// 1. Set rangeStart to fullLength − rangeEnd.
|
||||
range_start = full_length - *range_end;
|
||||
|
||||
// 2. Set rangeEnd to rangeStart + rangeEnd − 1.
|
||||
range_end = *range_start + *range_end - 1;
|
||||
}
|
||||
// 7. Otherwise:
|
||||
// 1. If rangeStart is greater than or equal to fullLength, then return a network error.
|
||||
// 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set rangeEnd to fullLength − 1.
|
||||
else {
|
||||
// 1. If rangeStart is greater than or equal to fullLength, then return a network error.
|
||||
if (*range_start >= full_length)
|
||||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "rangeStart is greater than or equal to fullLength"sv));
|
||||
|
||||
// 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set rangeEnd to fullLength − 1.
|
||||
if (!range_end.has_value() || *range_end >= full_length)
|
||||
range_end = full_length - 1;
|
||||
}
|
||||
|
||||
// 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart, rangeEnd + 1, and type.
|
||||
auto sliced_blob = TRY(blob->slice(*range_start, *range_end + 1, type));
|
||||
|
||||
// 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
|
||||
auto sliced_body_with_type = TRY(safely_extract_body(realm, sliced_blob->raw_bytes()));
|
||||
|
||||
// 10. Set response’s body to slicedBodyWithType’s body.
|
||||
response->set_body(sliced_body_with_type.body);
|
||||
|
||||
// 11. Let serializedSlicedLength be slicedBlob’s size, serialized and isomorphic encoded.
|
||||
// 12. Let contentRange be `bytes `.
|
||||
// 13. Append rangeStart, serialized and isomorphic encoded, to contentRange.
|
||||
// 14. Append 0x2D (-) to contentRange.
|
||||
// 15. Append rangeEnd, serialized and isomorphic encoded to contentRange.
|
||||
// 16. Append 0x2F (/) to contentRange.
|
||||
// 17. Append serializedFullLength to contentRange.
|
||||
// 18. Set response’s status to 206.
|
||||
// 19. Set response’s status message to `Partial Content`.
|
||||
// 20. Set response’s header list to « (`Content-Length`, serializedSlicedLength), (`Content-Type`, type), (`Content-Range`, contentRange) ».
|
||||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Request has a 'blob:' URL with a Content-Range header, which is currently unsupported"sv));
|
||||
auto serialized_sliced_length = String::number(sliced_blob->size());
|
||||
|
||||
// 12. Let contentRange be the result of invoking build a content range given rangeStart, rangeEnd, and fullLength.
|
||||
auto content_range = Infrastructure::build_content_range(*range_start, *range_end, full_length);
|
||||
|
||||
// 13. Set response’s status to 206.
|
||||
response->set_status(206);
|
||||
|
||||
// 14. Set response’s status message to `Partial Content`.
|
||||
response->set_status_message(MUST(ByteBuffer::copy("Partial Content"sv.bytes())));
|
||||
|
||||
// 15. Set response’s header list to «
|
||||
|
||||
// (`Content-Length`, serializedSlicedLength),
|
||||
auto content_length_header = Infrastructure::Header::from_string_pair("Content-Length"sv, serialized_sliced_length);
|
||||
response->header_list()->append(move(content_length_header));
|
||||
|
||||
// (`Content-Type`, type),
|
||||
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
|
||||
response->header_list()->append(move(content_type_header));
|
||||
|
||||
// (`Content-Range`, contentRange) ».
|
||||
auto content_range_header = Infrastructure::Header::from_string_pair("Content-Range"sv, content_range);
|
||||
response->header_list()->append(move(content_range_header));
|
||||
}
|
||||
|
||||
// 10. Return response.
|
||||
|
|
37
Tests/LibWeb/Text/expected/wpt-import/xhr/blob-range.any.txt
Normal file
37
Tests/LibWeb/Text/expected/wpt-import/xhr/blob-range.any.txt
Normal file
|
@ -0,0 +1,37 @@
|
|||
Summary
|
||||
|
||||
Harness status: OK
|
||||
|
||||
Rerun
|
||||
|
||||
Found 27 tests
|
||||
|
||||
27 Pass
|
||||
Details
|
||||
Result Test Name MessagePass A simple blob range request.
|
||||
Pass A blob range request with no type.
|
||||
Pass A blob range request with no end.
|
||||
Pass A blob range request with no start.
|
||||
Pass A simple blob range request with whitespace.
|
||||
Pass Blob content with short content and a large range end
|
||||
Pass Blob content with short content and a range end matching content length
|
||||
Pass Blob range with whitespace before and after hyphen
|
||||
Pass Blob range with whitespace after hyphen
|
||||
Pass Blob range with whitespace around equals sign
|
||||
Pass Blob range with no value
|
||||
Pass Blob range with incorrect range header
|
||||
Pass Blob range with incorrect range header #2
|
||||
Pass Blob range with incorrect range header #3
|
||||
Pass Blob range request with multiple range values
|
||||
Pass Blob range request with multiple range values and whitespace
|
||||
Pass Blob range request with trailing comma
|
||||
Pass Blob range with no start or end
|
||||
Pass Blob range request with short range end
|
||||
Pass Blob range start should be an ASCII digit
|
||||
Pass Blob range should have a dash
|
||||
Pass Blob range end should be an ASCII digit
|
||||
Pass Blob range should include '-'
|
||||
Pass Blob range should include '='
|
||||
Pass Blob range should include 'bytes='
|
||||
Pass Blob content with short content and a large range start
|
||||
Pass Blob content with short content and a range start matching the content length
|
15
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.html
Normal file
15
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
|
||||
<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="../xhr/blob-range.any.js"></script>
|
246
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.js
Normal file
246
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.js
Normal file
|
@ -0,0 +1,246 @@
|
|||
// See also /fetch/range/blob.any.js
|
||||
|
||||
const supportedBlobRange = [
|
||||
{
|
||||
name: "A simple blob range request.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: "text/plain",
|
||||
range: "bytes=9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no type.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: undefined,
|
||||
range: "bytes=9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no end.",
|
||||
data: ["Range with no end"],
|
||||
type: "text/plain",
|
||||
range: "bytes=11-",
|
||||
content_length: 6,
|
||||
content_range: "bytes 11-16/17",
|
||||
result: "no end",
|
||||
},
|
||||
{
|
||||
name: "A blob range request with no start.",
|
||||
data: ["Range with no start"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-8",
|
||||
content_length: 8,
|
||||
content_range: "bytes 11-18/19",
|
||||
result: "no start",
|
||||
},
|
||||
{
|
||||
name: "A simple blob range request with whitespace.",
|
||||
data: ["A simple Hello, World! example"],
|
||||
type: "text/plain",
|
||||
range: "bytes= \t9-21",
|
||||
content_length: 13,
|
||||
content_range: "bytes 9-21/30",
|
||||
result: "Hello, World!",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a large range end",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=4-100000000000",
|
||||
content_length: 9,
|
||||
content_range: "bytes 4-12/13",
|
||||
result: "much here",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a range end matching content length",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=4-13",
|
||||
content_length: 9,
|
||||
content_range: "bytes 4-12/13",
|
||||
result: "much here",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace before and after hyphen",
|
||||
data: ["Valid whitespace #1"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5 - 10",
|
||||
content_length: 6,
|
||||
content_range: "bytes 5-10/19",
|
||||
result: " white",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace after hyphen",
|
||||
data: ["Valid whitespace #2"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-\t 5",
|
||||
content_length: 5,
|
||||
content_range: "bytes 14-18/19",
|
||||
result: "ce #2",
|
||||
},
|
||||
{
|
||||
name: "Blob range with whitespace around equals sign",
|
||||
data: ["Valid whitespace #3"],
|
||||
type: "text/plain",
|
||||
range: "bytes \t =\t 6-",
|
||||
content_length: 13,
|
||||
content_range: "bytes 6-18/19",
|
||||
result: "whitespace #3",
|
||||
},
|
||||
];
|
||||
|
||||
const unsupportedBlobRange = [
|
||||
{
|
||||
name: "Blob range with no value",
|
||||
data: ["Blob range should have a value"],
|
||||
type: "text/plain",
|
||||
range: "",
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "byte=0-"
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header #2",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "bytes"
|
||||
},
|
||||
{
|
||||
name: "Blob range with incorrect range header #3",
|
||||
data: ["A"],
|
||||
type: "text/plain",
|
||||
range: "bytes\t \t"
|
||||
},
|
||||
{
|
||||
name: "Blob range request with multiple range values",
|
||||
data: ["Multiple ranges are not currently supported"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5,15-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with multiple range values and whitespace",
|
||||
data: ["Multiple ranges are not currently supported"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5, 15-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with trailing comma",
|
||||
data: ["Range with invalid trailing comma"],
|
||||
type: "text/plain",
|
||||
range: "bytes=0-5,",
|
||||
},
|
||||
{
|
||||
name: "Blob range with no start or end",
|
||||
data: ["Range with no start or end"],
|
||||
type: "text/plain",
|
||||
range: "bytes=-",
|
||||
},
|
||||
{
|
||||
name: "Blob range request with short range end",
|
||||
data: ["Range end should be greater than range start"],
|
||||
type: "text/plain",
|
||||
range: "bytes=10-5",
|
||||
},
|
||||
{
|
||||
name: "Blob range start should be an ASCII digit",
|
||||
data: ["Range start must be an ASCII digit"],
|
||||
type: "text/plain",
|
||||
range: "bytes=x-5",
|
||||
},
|
||||
{
|
||||
name: "Blob range should have a dash",
|
||||
data: ["Blob range should have a dash"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5",
|
||||
},
|
||||
{
|
||||
name: "Blob range end should be an ASCII digit",
|
||||
data: ["Range end must be an ASCII digit"],
|
||||
type: "text/plain",
|
||||
range: "bytes=5-x",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include '-'",
|
||||
data: ["Range end must include '-'"],
|
||||
type: "text/plain",
|
||||
range: "bytes=x",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include '='",
|
||||
data: ["Range end must include '='"],
|
||||
type: "text/plain",
|
||||
range: "bytes 5-",
|
||||
},
|
||||
{
|
||||
name: "Blob range should include 'bytes='",
|
||||
data: ["Range end must include 'bytes='"],
|
||||
type: "text/plain",
|
||||
range: "5-",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a large range start",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=100000-",
|
||||
},
|
||||
{
|
||||
name: "Blob content with short content and a range start matching the content length",
|
||||
data: ["Not much here"],
|
||||
type: "text/plain",
|
||||
range: "bytes=13-",
|
||||
},
|
||||
];
|
||||
|
||||
supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => {
|
||||
promise_test(async t => {
|
||||
const blob = new Blob(data, { "type" : type });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
t.add_cleanup(() => URL.revokeObjectURL(blobURL));
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", blobURL);
|
||||
xhr.responseType = "text";
|
||||
xhr.setRequestHeader("Range", range);
|
||||
await new Promise(resolve => {
|
||||
xhr.onloadend = resolve;
|
||||
xhr.send();
|
||||
});
|
||||
assert_equals(xhr.status, 206, "HTTP status is 206");
|
||||
assert_equals(xhr.getResponseHeader("Content-Type"), type || "", "Content-Type is " + xhr.getResponseHeader("Content-Type"));
|
||||
assert_equals(xhr.getResponseHeader("Content-Length"), content_length.toString(), "Content-Length is " + xhr.getResponseHeader("Content-Length"));
|
||||
assert_equals(xhr.getResponseHeader("Content-Range"), content_range, "Content-Range is " + xhr.getResponseHeader("Content-Range"));
|
||||
assert_equals(xhr.responseText, result, "Response's body is correct");
|
||||
const all = xhr.getAllResponseHeaders().toLowerCase();
|
||||
assert_true(all.includes(`content-type: ${type || ""}`), "Expected Content-Type in getAllResponseHeaders()");
|
||||
assert_true(all.includes(`content-length: ${content_length}`), "Expected Content-Length in getAllResponseHeaders()");
|
||||
assert_true(all.includes(`content-range: ${content_range}`), "Expected Content-Range in getAllResponseHeaders()")
|
||||
}, name);
|
||||
});
|
||||
|
||||
unsupportedBlobRange.forEach(({ name, data, type, range }) => {
|
||||
promise_test(t => {
|
||||
const blob = new Blob(data, { "type" : type });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
t.add_cleanup(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", blobURL, false);
|
||||
xhr.setRequestHeader("Range", range);
|
||||
assert_throws_dom("NetworkError", () => xhr.send());
|
||||
|
||||
xhr.open("GET", blobURL);
|
||||
xhr.setRequestHeader("Range", range);
|
||||
xhr.responseType = "text";
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = reject;
|
||||
xhr.onerror = resolve;
|
||||
xhr.send();
|
||||
});
|
||||
}, name);
|
||||
});
|
Loading…
Reference in a new issue