Commit 9efdfbab authored by Canon Mukai's avatar Canon Mukai Committed by Commit Bot

Implement DecompressionStream

This is behind a flag.
And I added some JavaScript tests.

Design Doc:
https://docs.google.com/document/d/1TovyqqeC3HoO0A4UUBKiCyhZlQSl7jM_F7KbWjK2Gcs/edit#heading=h.7nki9mck5t64

Bug: 999091
Change-Id: I4c34d9b80fe50242c6547662040439a541b652b0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1810447Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarAdam Rice <ricea@chromium.org>
Commit-Queue: Canon Mukai <canonmukai@google.com>
Cr-Commit-Position: refs/heads/master@{#701041}
parent 119b5472
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
namespace blink { namespace blink {
class ExceptionState; class ExceptionState;
class ScriptState;
class TransformStreamDefaultControllerInterface; class TransformStreamDefaultControllerInterface;
class Visitor; class Visitor;
......
...@@ -103,6 +103,7 @@ target("jumbo_" + modules_target_type, "modules") { ...@@ -103,6 +103,7 @@ target("jumbo_" + modules_target_type, "modules") {
"//third_party/blink/renderer/modules/credentialmanager", "//third_party/blink/renderer/modules/credentialmanager",
"//third_party/blink/renderer/modules/crypto", "//third_party/blink/renderer/modules/crypto",
"//third_party/blink/renderer/modules/csspaint", "//third_party/blink/renderer/modules/csspaint",
"//third_party/blink/renderer/modules/compression",
"//third_party/blink/renderer/modules/device_orientation", "//third_party/blink/renderer/modules/device_orientation",
"//third_party/blink/renderer/modules/document_metadata", "//third_party/blink/renderer/modules/document_metadata",
"//third_party/blink/renderer/modules/donottrack", "//third_party/blink/renderer/modules/donottrack",
......
import("//third_party/blink/renderer/modules/modules.gni")
blink_modules_sources("compression") {
sources = [
"decompression_stream.cc",
"decompression_stream.h",
"inflate_transformer.cc",
"inflate_transformer.h",
]
}
# CompressionStreams API
This directory contains the implementation of CompressionStream and
DecompressionStream.
## Design docs
See [DecompressionStream Design Docs](https://docs.google.com/document/d/1TovyqqeC3HoO0A4UUBKiCyhZlQSl7jM_F7KbWjK2Gcs/edit).
#include "third_party/blink/renderer/modules/compression/decompression_stream.h"
#include "third_party/blink/renderer/modules/compression/inflate_transformer.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
namespace blink {
DecompressionStream* DecompressionStream::Create(
ScriptState* script_state,
const AtomicString& format,
ExceptionState& exception_state) {
return MakeGarbageCollected<DecompressionStream>(script_state, format,
exception_state);
}
ReadableStream* DecompressionStream::readable() const {
return transform_->Readable();
}
WritableStream* DecompressionStream::writable() const {
return transform_->Writable();
}
void DecompressionStream::Trace(Visitor* visitor) {
visitor->Trace(transform_);
ScriptWrappable::Trace(visitor);
}
DecompressionStream::DecompressionStream(ScriptState* script_state,
const AtomicString& format,
ExceptionState& exception_state)
: transform_(MakeGarbageCollected<TransformStream>()) {
InflateTransformer::Algorithm algorithm =
InflateTransformer::Algorithm::kDeflate;
if (format == "gzip") {
algorithm = InflateTransformer::Algorithm::kGzip;
} else if (format != "deflate") {
exception_state.ThrowTypeError("Unsupported format");
return;
}
transform_->Init(
MakeGarbageCollected<InflateTransformer>(script_state, algorithm),
script_state, exception_state);
}
} // namespace blink
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_DECOMPRESSION_STREAM_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_DECOMPRESSION_STREAM_H_
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/core/streams/transform_stream.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
namespace blink {
class DecompressionStream final : public ScriptWrappable {
DEFINE_WRAPPERTYPEINFO();
public:
static DecompressionStream* Create(ScriptState*,
const AtomicString&,
ExceptionState&);
DecompressionStream(ScriptState*, const AtomicString&, ExceptionState&);
ReadableStream* readable() const;
WritableStream* writable() const;
void Trace(Visitor* visitor) override;
private:
const Member<TransformStream> transform_;
DISALLOW_COPY_AND_ASSIGN(DecompressionStream);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_DECOMPRESSION_STREAM_H_
[
Exposed=(Window,Worker),
Constructor(DOMString format),
ConstructorCallWith=ScriptState,
RaisesException=Constructor,
RuntimeEnabled=CompressionStreams
] interface DecompressionStream {
readonly attribute ReadableStream readable;
readonly attribute WritableStream writable;
};
#include "third_party/blink/renderer/modules/compression/inflate_transformer.h"
#include <string.h>
#include <algorithm>
#include <limits>
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h"
#include "third_party/blink/renderer/core/streams/transform_stream_default_controller_interface.h"
#include "third_party/blink/renderer/core/streams/transform_stream_transformer.h"
#include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/to_v8.h"
#include "v8/include/v8.h"
namespace blink {
InflateTransformer::InflateTransformer(ScriptState* script_state,
Algorithm algorithm)
: script_state_(script_state), out_buffer_(kBufferSize) {
memset(&stream_, 0, sizeof(z_stream));
int err;
constexpr int kWindowBits = 15;
constexpr int kUseGzip = 16;
switch (algorithm) {
case Algorithm::kDeflate:
err = inflateInit2(&stream_, kWindowBits);
break;
case Algorithm::kGzip:
err = inflateInit2(&stream_, kWindowBits + kUseGzip);
break;
}
DCHECK_EQ(Z_OK, err);
}
InflateTransformer::~InflateTransformer() {
if (!was_flush_called_) {
inflateEnd(&stream_);
}
}
void InflateTransformer::Transform(
v8::Local<v8::Value> chunk,
TransformStreamDefaultControllerInterface* controller,
ExceptionState& exception_state) {
// TODO(canonmukai): Change to MaybeShared<DOMUint8Array>
// when we are ready to support SharedArrayBuffer.
NotShared<DOMUint8Array> chunk_data = ToNotShared<NotShared<DOMUint8Array>>(
script_state_->GetIsolate(), chunk, exception_state);
if (exception_state.HadException()) {
return;
}
if (!chunk_data) {
exception_state.ThrowTypeError("chunk is not of type Uint8Array.");
return;
}
Inflate(chunk_data.View(), IsFinished(false), controller, exception_state);
}
void InflateTransformer::Flush(
TransformStreamDefaultControllerInterface* controller,
ExceptionState& exception_state) {
DCHECK(!was_flush_called_);
Inflate(nullptr, IsFinished(true), controller, exception_state);
inflateEnd(&stream_);
was_flush_called_ = true;
}
void InflateTransformer::Inflate(
const DOMUint8Array* data,
IsFinished finished,
TransformStreamDefaultControllerInterface* controller,
ExceptionState& exception_state) {
unsigned int out_buffer_size = static_cast<unsigned int>(kBufferSize);
if (data) {
stream_.avail_in = data->length();
stream_.next_in = data->DataMaybeShared();
} else {
stream_.avail_in = 0;
stream_.next_in = nullptr;
}
do {
stream_.avail_out = out_buffer_size;
stream_.next_out = out_buffer_.data();
int err = inflate(&stream_, finished ? Z_FINISH : Z_NO_FLUSH);
if (err != Z_OK && err != Z_STREAM_END && err != Z_BUF_ERROR) {
exception_state.ThrowTypeError("The compressed data was not valid.");
return;
}
wtf_size_t bytes = out_buffer_size - stream_.avail_out;
if (bytes) {
controller->Enqueue(
ToV8(DOMUint8Array::Create(out_buffer_.data(), bytes), script_state_),
exception_state);
if (exception_state.HadException()) {
return;
}
}
} while (stream_.avail_out == 0);
}
void InflateTransformer::Trace(Visitor* visitor) {
visitor->Trace(script_state_);
TransformStreamTransformer::Trace(visitor);
}
} // namespace blink
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_INFLATE_TRANSFORMER_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_INFLATE_TRANSFORMER_H_
#include "base/util/type_safety/strong_alias.h"
#include "third_party/blink/renderer/core/streams/transform_stream_transformer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "third_party/zlib/zlib.h"
namespace blink {
class InflateTransformer final : public TransformStreamTransformer {
public:
enum class Algorithm { kDeflate, kGzip };
InflateTransformer(ScriptState*, Algorithm algorithm);
~InflateTransformer() override;
void Transform(v8::Local<v8::Value> chunk,
TransformStreamDefaultControllerInterface*,
ExceptionState&) override;
void Flush(TransformStreamDefaultControllerInterface*,
ExceptionState&) override;
ScriptState* GetScriptState() override { return script_state_; }
void Trace(Visitor*) override;
private:
using IsFinished = util::StrongAlias<class IsFinishedTag, bool>;
void Inflate(const DOMUint8Array* data,
IsFinished,
TransformStreamDefaultControllerInterface*,
ExceptionState&);
Member<ScriptState> script_state_;
z_stream stream_;
Vector<Bytef> out_buffer_;
bool was_flush_called_ = false;
static constexpr wtf_size_t kBufferSize = 65536;
DISALLOW_COPY_AND_ASSIGN(InflateTransformer);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_COMPRESSION_INFLATE_TRANSFORMER_H_
...@@ -99,6 +99,7 @@ modules_idl_files = ...@@ -99,6 +99,7 @@ modules_idl_files =
"canvas/offscreencanvas2d/offscreen_canvas_rendering_context_2d.idl", "canvas/offscreencanvas2d/offscreen_canvas_rendering_context_2d.idl",
"clipboard/clipboard.idl", "clipboard/clipboard.idl",
"clipboard/clipboard_item.idl", "clipboard/clipboard_item.idl",
"compression/decompression_stream.idl",
"contacts_picker/contacts_manager.idl", "contacts_picker/contacts_manager.idl",
"content_index/content_index.idl", "content_index/content_index.idl",
"content_index/content_index_event.idl", "content_index/content_index_event.idl",
......
...@@ -303,6 +303,10 @@ ...@@ -303,6 +303,10 @@
name: "CompositorTouchAction", name: "CompositorTouchAction",
status: "test", status: "test",
}, },
{
name: "CompressionStreams",
status: "experimental",
},
{ {
name: "ComputedAccessibilityInfo", name: "ComputedAccessibilityInfo",
status: "experimental", status: "experimental",
......
// META: global=worker
'use strict';
const badChunks = [
{
name: 'undefined',
value: undefined
},
{
name: 'null',
value: null
},
{
name: 'numeric',
value: 3.14
},
{
name: 'object, not BufferSource',
value: {}
},
{
name: 'array',
value: [65]
},
{
name: 'SharedArrayBuffer',
// Use a getter to postpone construction so that all tests don't fail where
// SharedArrayBuffer is not yet implemented.
get value() {
return new SharedArrayBuffer();
}
},
{
name: 'shared Uint8Array',
get value() {
new Uint8Array(new SharedArrayBuffer())
}
},
{
name: 'invalid deflate bytes',
get value() {
new Uint8Array([0, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36])
}
},
{
name: 'invalid gzip bytes',
get value() {
new Uint8Array([0, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0])
}
},
];
for (const chunk of badChunks) {
promise_test(async t => {
const ds = new DecompressionStream('gzip');
const reader = ds.readable.getReader();
const writer = ds.writable.getWriter();
const writePromise = writer.write(chunk.value);
const readPromise = reader.read();
await promise_rejects(t, new TypeError(), writePromise, 'write should reject');
await promise_rejects(t, new TypeError(), readPromise, 'read should reject');
}, `chunk of type ${chunk.name} should error the stream for gzip`);
promise_test(async t => {
const ds = new DecompressionStream('deflate');
const reader = ds.readable.getReader();
const writer = ds.writable.getWriter();
const writePromise = writer.write(chunk.value);
const readPromise = reader.read();
await promise_rejects(t, new TypeError(), writePromise, 'write should reject');
await promise_rejects(t, new TypeError(), readPromise, 'read should reject');
}, `chunk of type ${chunk.name} should error the stream for deflate`);
}
// META: global=worker
'use strict';
test(t => {
assert_throws(new TypeError(), () => new DecompressionStream('a'), 'constructor should throw');
}, '"a" should cause the constructor to throw');
test(t => {
assert_throws(new TypeError(), () => new DecompressionStream(), 'constructor should throw');
}, 'no input should cause the constructor to throw');
test(t => {
assert_throws(new Error(), () => new DecompressionStream({ toString() { throw Error(); } }), 'constructor should throw');
}, 'non-string input should cause the constructor to throw');
// META: global=worker
'use strict';
const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]);
const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]);
const trueChunkValue = new TextEncoder().encode('expected output');
promise_test(async t => {
const ds = new DecompressionStream('deflate');
const reader = ds.readable.getReader();
const writer = ds.writable.getWriter();
const writePromise = writer.write(deflateChunkValue);
const { done, value } = await reader.read();
assert_array_equals(Array.from(value), trueChunkValue, "value should match");
}, 'decompressing deflated input should work');
promise_test(async t => {
const ds = new DecompressionStream('gzip');
const reader = ds.readable.getReader();
const writer = ds.writable.getWriter();
const writePromise = writer.write(gzipChunkValue);
const { done, value } = await reader.read();
assert_array_equals(Array.from(value), trueChunkValue, "value should match");
}, 'decompressing gzip input should work');
...@@ -384,6 +384,11 @@ interface DOMStringList ...@@ -384,6 +384,11 @@ interface DOMStringList
method constructor method constructor
method contains method contains
method item method item
interface DecompressionStream
attribute @@toStringTag
getter readable
getter writable
method constructor
interface DetectedBarcode interface DetectedBarcode
attribute @@toStringTag attribute @@toStringTag
getter boundingBox getter boundingBox
......
...@@ -340,6 +340,11 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -340,6 +340,11 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method constructor [Worker] method constructor
[Worker] method contains [Worker] method contains
[Worker] method item [Worker] method item
[Worker] interface DecompressionStream
[Worker] attribute @@toStringTag
[Worker] getter readable
[Worker] getter writable
[Worker] method constructor
[Worker] interface DedicatedWorkerGlobalScope : WorkerGlobalScope [Worker] interface DedicatedWorkerGlobalScope : WorkerGlobalScope
[Worker] attribute @@toStringTag [Worker] attribute @@toStringTag
[Worker] attribute PERSISTENT [Worker] attribute PERSISTENT
......
...@@ -1499,6 +1499,11 @@ interface DataTransferItemList ...@@ -1499,6 +1499,11 @@ interface DataTransferItemList
method clear method clear
method constructor method constructor
method remove method remove
interface DecompressionStream
attribute @@toStringTag
getter readable
getter writable
method constructor
interface DelayNode : AudioNode interface DelayNode : AudioNode
attribute @@toStringTag attribute @@toStringTag
getter delayTime getter delayTime
......
...@@ -340,6 +340,11 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -340,6 +340,11 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method constructor [Worker] method constructor
[Worker] method contains [Worker] method contains
[Worker] method item [Worker] method item
[Worker] interface DecompressionStream
[Worker] attribute @@toStringTag
[Worker] getter readable
[Worker] getter writable
[Worker] method constructor
[Worker] interface DetectedBarcode [Worker] interface DetectedBarcode
[Worker] attribute @@toStringTag [Worker] attribute @@toStringTag
[Worker] getter boundingBox [Worker] getter boundingBox
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment