LibWeb/Fetch: Implement blob range section of scheme fetch specification

This commit is contained in:
rmg-x 2024-11-18 17:34:30 -06:00
parent 3e049cd954
commit 61ce94c0f9
4 changed files with 359 additions and 15 deletions

View file

@ -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 responses range-requested flag.
response->set_range_requested(true);
// 2. Let rangeHeader be the result of getting `Range` from requests 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:
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:
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 responses body to slicedBodyWithTypes body.
response->set_body(sliced_body_with_type.body);
// 11. Let serializedSlicedLength be slicedBlobs 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 responses status to 206.
// 19. Set responses status message to `Partial Content`.
// 20. Set responses 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 responses status to 206.
response->set_status(206);
// 14. Set responses status message to `Partial Content`.
response->set_status_message(MUST(ByteBuffer::copy("Partial Content"sv.bytes())));
// 15. Set responses 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.

View 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

View 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>

View 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);
});