commit eea3a13b4405bc9635a93a1a8f1b5c99de183c55 parent f48ae44715954221d52e7fa05f5ce675ffda0640 Author: Kagami Sascha Rosylight <krosylight@proton.me> Date: Mon, 17 Nov 2025 13:11:07 +0000 Bug 1921583 - Part 2: Support brotli in DecompressionStream r=smaug,webidl Differential Revision: https://phabricator.services.mozilla.com/D269038 Diffstat:
13 files changed, 299 insertions(+), 1 deletion(-)
diff --git a/dom/compression/CompressionStream.cpp b/dom/compression/CompressionStream.cpp @@ -42,6 +42,12 @@ JSObject* CompressionStream::WrapObject(JSContext* aCx, // https://wicg.github.io/compression/#dom-compressionstream-compressionstream already_AddRefed<CompressionStream> CompressionStream::Constructor( const GlobalObject& aGlobal, CompressionFormat aFormat, ErrorResult& aRv) { + if (aFormat == CompressionFormat::Brotli) { + aRv.ThrowTypeError( + "'brotli' (value of argument 1) is not a valid value for enumeration " + "CompressionFormat."); + return nullptr; + } if (aFormat == CompressionFormat::Zstd) { aRv.ThrowTypeError( "'zstd' (value of argument 1) is not a valid value for enumeration " diff --git a/dom/compression/DecompressionStream.cpp b/dom/compression/DecompressionStream.cpp @@ -7,10 +7,12 @@ #include "mozilla/dom/DecompressionStream.h" #include "BaseAlgorithms.h" +#include "FormatBrotli.h" #include "FormatZlib.h" #include "FormatZstd.h" #include "js/TypeDecls.h" #include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/CompressionStreamBinding.h" #include "mozilla/dom/DecompressionStreamBinding.h" #include "mozilla/dom/ReadableStream.h" #include "mozilla/dom/TextDecoderStream.h" @@ -23,10 +25,15 @@ using namespace compression; /* * Constructs either a ZLibDecompressionStreamAlgorithms or a - * ZstdDecompressionStreamAlgorithms, based on the CompressionFormat. + * Brotli/ZstdDecompressionStreamAlgorithms, based on the CompressionFormat. */ static Result<already_AddRefed<DecompressionStreamAlgorithms>, nsresult> CreateDecompressionStreamAlgorithms(CompressionFormat aFormat) { + if (aFormat == CompressionFormat::Brotli) { + RefPtr<DecompressionStreamAlgorithms> brotliAlgos = + MOZ_TRY(BrotliDecompressionStreamAlgorithms::Create()); + return brotliAlgos.forget(); + } if (aFormat == CompressionFormat::Zstd) { RefPtr<DecompressionStreamAlgorithms> zstdAlgos = MOZ_TRY(ZstdDecompressionStreamAlgorithms::Create()); diff --git a/dom/compression/FormatBrotli.cpp b/dom/compression/FormatBrotli.cpp @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FormatBrotli.h" + +#include <memory> + +#include "BaseAlgorithms.h" +#include "brotli/decode.h" +#include "mozilla/dom/TransformStreamDefaultController.h" + +namespace mozilla::dom::compression { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(BrotliDecompressionStreamAlgorithms, + TransformerAlgorithmsBase) +NS_IMPL_ADDREF_INHERITED(BrotliDecompressionStreamAlgorithms, + TransformerAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(BrotliDecompressionStreamAlgorithms, + TransformerAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BrotliDecompressionStreamAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(TransformerAlgorithmsBase) + +Result<already_AddRefed<BrotliDecompressionStreamAlgorithms>, nsresult> +BrotliDecompressionStreamAlgorithms::Create() { + RefPtr<BrotliDecompressionStreamAlgorithms> alg = + new BrotliDecompressionStreamAlgorithms(); + MOZ_TRY(alg->Init()); + return alg.forget(); +} + +[[nodiscard]] nsresult BrotliDecompressionStreamAlgorithms::Init() { + mState = std::unique_ptr<BrotliDecoderStateStruct, BrotliDeleter>( + BrotliDecoderCreateInstance(nullptr, nullptr, nullptr)); + if (!mState) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +// Shared by: +// https://wicg.github.io/compression/#decompress-and-enqueue-a-chunk +// https://wicg.github.io/compression/#decompress-flush-and-enqueue +// All data errors throw TypeError by step 2: If this results in an error, +// then throw a TypeError. +bool BrotliDecompressionStreamAlgorithms::Decompress( + JSContext* aCx, Span<const uint8_t> aInput, + JS::MutableHandleVector<JSObject*> aOutput, Flush aFlush, + ErrorResult& aRv) { + size_t inputLength = aInput.Length(); + const uint8_t* inputBuffer = aInput.Elements(); + + do { + std::unique_ptr<uint8_t[], JS::FreePolicy> buffer( + static_cast<uint8_t*>(JS_malloc(aCx, kBufferSize))); + if (!buffer) { + aRv.ThrowTypeError("Out of memory"); + return false; + } + + size_t outputLength = kBufferSize; + uint8_t* outputBuffer = buffer.get(); + BrotliDecoderResult rv = + BrotliDecoderDecompressStream(mState.get(), &inputLength, &inputBuffer, + &outputLength, &outputBuffer, nullptr); + switch (rv) { + case BROTLI_DECODER_RESULT_ERROR: + aRv.ThrowTypeError("Brotli decompression error: "_ns + + nsDependentCString(BrotliDecoderErrorString( + BrotliDecoderGetErrorCode(mState.get())))); + return false; + case BROTLI_DECODER_RESULT_SUCCESS: + mObservedStreamEnd = true; + break; + case BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: + case BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected decompression error code"); + aRv.ThrowTypeError("Unexpected decompression error"); + return false; + } + + // Step 3: If buffer is empty, return. + // (We'll implicitly return when the array is empty.) + + // Step 4: Split buffer into one or more non-empty pieces and convert them + // into Uint8Arrays. + // (The buffer is 'split' by having a fixed sized buffer above.) + + size_t written = kBufferSize - outputLength; + if (written > 0) { + JS::Rooted<JSObject*> view(aCx, nsJSUtils::MoveBufferAsUint8Array( + aCx, written, std::move(buffer))); + if (!view || !aOutput.append(view)) { + JS_ClearPendingException(aCx); + aRv.ThrowTypeError("Out of memory"); + return false; + } + } + } while (BrotliDecoderHasMoreOutput(mState.get())); + + return inputLength == 0; +} + +void BrotliDecompressionStreamAlgorithms::BrotliDeleter::operator()( + BrotliDecoderStateStruct* aState) { + BrotliDecoderDestroyInstance(aState); +} + +} // namespace mozilla::dom::compression diff --git a/dom/compression/FormatBrotli.h b/dom/compression/FormatBrotli.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_COMPRESSION_FORMATBROTLI_H_ +#define DOM_COMPRESSION_FORMATBROTLI_H_ + +#include "BaseAlgorithms.h" + +struct BrotliDecoderStateStruct; + +// See the brotli manual +// https://searchfox.org/firefox-main/source/modules/brotli/include/brotli/decode.h + +namespace mozilla::dom::compression { + +class BrotliDecompressionStreamAlgorithms + : public DecompressionStreamAlgorithms { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(BrotliDecompressionStreamAlgorithms, + DecompressionStreamAlgorithms) + + static Result<already_AddRefed<BrotliDecompressionStreamAlgorithms>, nsresult> + Create(); + + private: + BrotliDecompressionStreamAlgorithms() = default; + + [[nodiscard]] nsresult Init(); + + // Shared by: + // https://wicg.github.io/compression/#decompress-and-enqueue-a-chunk + // https://wicg.github.io/compression/#decompress-flush-and-enqueue + // All data errors throw TypeError by step 2: If this results in an error, + // then throw a TypeError. + bool Decompress(JSContext* aCx, Span<const uint8_t> aInput, + JS::MutableHandleVector<JSObject*> aOutput, Flush aFlush, + ErrorResult& aRv) override; + + ~BrotliDecompressionStreamAlgorithms() = default; + + struct BrotliDeleter { + void operator()(BrotliDecoderStateStruct* aState); + }; + + std::unique_ptr<BrotliDecoderStateStruct, BrotliDeleter> mState; +}; + +} // namespace mozilla::dom::compression + +#endif // DOM_COMPRESSION_FORMATBROTLI_H_ diff --git a/dom/compression/moz.build b/dom/compression/moz.build @@ -16,6 +16,7 @@ UNIFIED_SOURCES += [ "BaseAlgorithms.cpp", "CompressionStream.cpp", "DecompressionStream.cpp", + "FormatBrotli.cpp", "FormatZlib.cpp", "FormatZstd.cpp", ] diff --git a/dom/webidl/CompressionStream.webidl b/dom/webidl/CompressionStream.webidl @@ -11,6 +11,9 @@ enum CompressionFormat { "deflate", "deflate-raw", "gzip", + "brotli", + + // Mozilla specific "zstd", }; diff --git a/testing/web-platform/meta/compression/compression-bad-chunks.any.js.ini b/testing/web-platform/meta/compression/compression-bad-chunks.any.js.ini @@ -1,8 +1,71 @@ [compression-bad-chunks.any.sharedworker.html] + [chunk of type undefined should error the stream for brotli] + expected: FAIL + + [chunk of type null should error the stream for brotli] + expected: FAIL + + [chunk of type numeric should error the stream for brotli] + expected: FAIL + + [chunk of type object, not BufferSource should error the stream for brotli] + expected: FAIL + + [chunk of type array should error the stream for brotli] + expected: FAIL + + [chunk of type SharedArrayBuffer should error the stream for brotli] + expected: FAIL + + [chunk of type shared Uint8Array should error the stream for brotli] + expected: FAIL + [compression-bad-chunks.any.serviceworker.html] + [chunk of type undefined should error the stream for brotli] + expected: FAIL + + [chunk of type null should error the stream for brotli] + expected: FAIL + + [chunk of type numeric should error the stream for brotli] + expected: FAIL + + [chunk of type object, not BufferSource should error the stream for brotli] + expected: FAIL + + [chunk of type array should error the stream for brotli] + expected: FAIL + + [chunk of type SharedArrayBuffer should error the stream for brotli] + expected: FAIL + + [chunk of type shared Uint8Array should error the stream for brotli] + expected: FAIL + [compression-bad-chunks.any.html] + [chunk of type undefined should error the stream for brotli] + expected: FAIL + + [chunk of type null should error the stream for brotli] + expected: FAIL + + [chunk of type numeric should error the stream for brotli] + expected: FAIL + + [chunk of type object, not BufferSource should error the stream for brotli] + expected: FAIL + + [chunk of type array should error the stream for brotli] + expected: FAIL + + [chunk of type SharedArrayBuffer should error the stream for brotli] + expected: FAIL + + [chunk of type shared Uint8Array should error the stream for brotli] + expected: FAIL + [compression-bad-chunks.any.shadowrealm.html] expected: @@ -12,6 +75,27 @@ [compression-bad-chunks.any.worker.html] expected: if (os == "android") and sessionHistoryInParent and not debug: [OK, TIMEOUT] + [chunk of type undefined should error the stream for brotli] + expected: FAIL + + [chunk of type null should error the stream for brotli] + expected: FAIL + + [chunk of type numeric should error the stream for brotli] + expected: FAIL + + [chunk of type object, not BufferSource should error the stream for brotli] + expected: FAIL + + [chunk of type array should error the stream for brotli] + expected: FAIL + + [chunk of type SharedArrayBuffer should error the stream for brotli] + expected: FAIL + + [chunk of type shared Uint8Array should error the stream for brotli] + expected: FAIL + [compression-bad-chunks.any.shadowrealm-in-window.html] expected: ERROR diff --git a/testing/web-platform/meta/compression/compression-output-length.any.js.ini b/testing/web-platform/meta/compression/compression-output-length.any.js.ini @@ -1,4 +1,7 @@ [compression-output-length.any.sharedworker.html] + [the length of brotli data should be shorter than that of the original data] + expected: FAIL + [compression-output-length.any.shadowrealm.html] expected: @@ -6,13 +9,22 @@ ERROR [compression-output-length.any.worker.html] + [the length of brotli data should be shorter than that of the original data] + expected: FAIL + [compression-output-length.any.html] + [the length of brotli data should be shorter than that of the original data] + expected: FAIL + [compression-output-length.any.serviceworker.html] expected: if (os == "android") and not sessionHistoryInParent and not debug: [OK, TIMEOUT] if (os == "mac") and not debug: [OK, ERROR] + [the length of brotli data should be shorter than that of the original data] + expected: FAIL + [compression-output-length.any.shadowrealm-in-shadowrealm.html] expected: ERROR diff --git a/testing/web-platform/tests/compression/decompression-buffersource.any.js b/testing/web-platform/tests/compression/decompression-buffersource.any.js @@ -9,6 +9,7 @@ const compressedBytes = [ 0x00, 0x06, 0x00, 0xf9, 0xff, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x01, 0x00, 0x00, 0xff, 0xff, ]], + ["brotli", [0x21, 0x08, 0x00, 0x04, 0x66, 0x6F, 0x6F, 0x03]] ]; // These chunk values below were chosen to make the length of the compressed // output be a multiple of 8 bytes. @@ -16,6 +17,7 @@ const expectedChunkValue = new Map(Object.entries({ "deflate": new TextEncoder().encode('a0123456'), "gzip": new TextEncoder().encode('a012'), "deflate-raw": new TextEncoder().encode('ABCDEF'), + "brotli": new TextEncoder().encode('foo'), })); const bufferSourceChunks = compressedBytes.map(([format, bytes]) => [format, [ diff --git a/testing/web-platform/tests/compression/decompression-corrupt-input.any.js b/testing/web-platform/tests/compression/decompression-corrupt-input.any.js @@ -222,6 +222,14 @@ const expectations = [ ] } ] + }, + { + format: 'brotli', + + // Decompresses to 'expected output'. + baseInput: brotliChunkValue, + + fields: [] } ]; diff --git a/testing/web-platform/tests/compression/decompression-empty-input.any.js b/testing/web-platform/tests/compression/decompression-empty-input.any.js @@ -6,6 +6,7 @@ const emptyValues = [ ["gzip", new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0])], ["deflate", new Uint8Array([120, 156, 3, 0, 0, 0, 0, 1])], ["deflate-raw", new Uint8Array([1, 0, 0, 255, 255])], + ["brotli", new Uint8Array([0xa1, 0x01])], ]; for (const [format, emptyValue] of emptyValues) { diff --git a/testing/web-platform/tests/compression/resources/decompression-input.js b/testing/web-platform/tests/compression/resources/decompression-input.js @@ -4,11 +4,17 @@ const deflateRawChunkValue = new Uint8Array([ 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, ]); +const brotliChunkValue = new Uint8Array([ + 0x21, 0x38, 0x00, 0x04, 0x65, 0x78, 0x70, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x20, 0x6F, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x03 +]); const compressedBytes = [ ["deflate", deflateChunkValue], ["gzip", gzipChunkValue], ["deflate-raw", deflateRawChunkValue], + ["brotli", brotliChunkValue], ]; const expectedChunkValue = new TextEncoder().encode('expected output'); diff --git a/testing/web-platform/tests/compression/resources/formats.js b/testing/web-platform/tests/compression/resources/formats.js @@ -2,4 +2,5 @@ const formats = [ "deflate", "deflate-raw", "gzip", + "brotli", ]