Forráskód Böngészése

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

rmg-x 8 hónapja
szülő
commit
13f349aea2

+ 61 - 15
Libraries/LibWeb/Fetch/Fetching/Fetching.cpp

@@ -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 - 0
Tests/LibWeb/Text/expected/wpt-import/xhr/blob-range.any.txt

@@ -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 - 0
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.html

@@ -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 - 0
Tests/LibWeb/Text/input/wpt-import/xhr/blob-range.any.js

@@ -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);
+});