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 committed by Tim Ledbetter
parent 84f673515b
commit 13f349aea2
Notes: github-actions[bot] 2024-11-21 00:58:58 +00:00
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); auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
response->header_list()->append(move(content_type_header)); response->header_list()->append(move(content_type_header));
} }
// FIXME: 9. Otherwise: // 9. Otherwise:
else { else {
// 1. Set responses range-requested flag. // 1. Set responses range-requested flag.
response->set_range_requested(true);
// 2. Let rangeHeader be the result of getting `Range` from requests header list. // 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. // 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. // 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. // 5. Let (rangeStart, rangeEnd) be rangeValue.
auto& [range_start, range_end] = maybe_range_value.value();
// 6. If rangeStart is null: // 6. If rangeStart is null:
if (!range_start.has_value()) {
VERIFY(range_end.has_value());
// 1. Set rangeStart to fullLength rangeEnd. // 1. Set rangeStart to fullLength rangeEnd.
range_start = full_length - *range_end;
// 2. Set rangeEnd to rangeStart + rangeEnd 1. // 2. Set rangeEnd to rangeStart + rangeEnd 1.
range_end = *range_start + *range_end - 1;
}
// 7. Otherwise: // 7. Otherwise:
else {
// 1. If rangeStart is greater than or equal to fullLength, then return a network error. // 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. // 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. // 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. // 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. // 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. // 11. Let serializedSlicedLength be slicedBlobs size, serialized and isomorphic encoded.
// 12. Let contentRange be `bytes `. auto serialized_sliced_length = String::number(sliced_blob->size());
// 13. Append rangeStart, serialized and isomorphic encoded, to contentRange.
// 14. Append 0x2D (-) to contentRange. // 12. Let contentRange be the result of invoking build a content range given rangeStart, rangeEnd, and fullLength.
// 15. Append rangeEnd, serialized and isomorphic encoded to contentRange. auto content_range = Infrastructure::build_content_range(*range_start, *range_end, full_length);
// 16. Append 0x2F (/) to contentRange.
// 17. Append serializedFullLength to contentRange. // 13. Set responses status to 206.
// 18. Set responses status to 206. response->set_status(206);
// 19. Set responses status message to `Partial Content`.
// 20. Set responses header list to « (`Content-Length`, serializedSlicedLength), (`Content-Type`, type), (`Content-Range`, contentRange) ». // 14. Set responses status message to `Partial Content`.
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)); 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. // 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);
});