FormatZlib.cpp (11884B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "FormatZlib.h" 8 9 #include "BaseAlgorithms.h" 10 #include "mozilla/dom/CompressionStreamBinding.h" 11 #include "mozilla/dom/TransformStreamDefaultController.h" 12 13 namespace mozilla::dom::compression { 14 15 NS_IMPL_CYCLE_COLLECTION_INHERITED(ZLibCompressionStreamAlgorithms, 16 TransformerAlgorithmsBase) 17 NS_IMPL_ADDREF_INHERITED(ZLibCompressionStreamAlgorithms, 18 TransformerAlgorithmsBase) 19 NS_IMPL_RELEASE_INHERITED(ZLibCompressionStreamAlgorithms, 20 TransformerAlgorithmsBase) 21 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ZLibCompressionStreamAlgorithms) 22 NS_INTERFACE_MAP_END_INHERITING(TransformerAlgorithmsBase) 23 24 NS_IMPL_CYCLE_COLLECTION_INHERITED(ZLibDecompressionStreamAlgorithms, 25 TransformerAlgorithmsBase) 26 NS_IMPL_ADDREF_INHERITED(ZLibDecompressionStreamAlgorithms, 27 TransformerAlgorithmsBase) 28 NS_IMPL_RELEASE_INHERITED(ZLibDecompressionStreamAlgorithms, 29 TransformerAlgorithmsBase) 30 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ZLibDecompressionStreamAlgorithms) 31 NS_INTERFACE_MAP_END_INHERITING(TransformerAlgorithmsBase) 32 33 inline uint8_t intoZLibFlush(Flush aFlush) { 34 switch (aFlush) { 35 case Flush::No: { 36 return Z_NO_FLUSH; 37 } 38 case Flush::Yes: { 39 return Z_FINISH; 40 } 41 default: { 42 MOZ_ASSERT_UNREACHABLE("Unknown flush mode"); 43 return Z_NO_FLUSH; 44 } 45 } 46 } 47 48 // From the docs in 49 // https://searchfox.org/mozilla-central/source/modules/zlib/src/zlib.h 50 inline int8_t ZLibWindowBits(CompressionFormat format) { 51 switch (format) { 52 case CompressionFormat::Deflate: 53 // The windowBits parameter is the base two logarithm of the window size 54 // (the size of the history buffer). It should be in the range 8..15 for 55 // this version of the library. Larger values of this parameter result 56 // in better compression at the expense of memory usage. 57 return 15; 58 case CompressionFormat::Deflate_raw: 59 // windowBits can also be –8..–15 for raw deflate. In this case, 60 // -windowBits determines the window size. 61 return -15; 62 case CompressionFormat::Gzip: 63 // windowBits can also be greater than 15 for optional gzip encoding. 64 // Add 16 to windowBits to write a simple gzip header and trailer around 65 // the compressed data instead of a zlib wrapper. 66 return 31; 67 default: 68 MOZ_ASSERT_UNREACHABLE("Unknown compression format"); 69 return 0; 70 } 71 } 72 73 Result<already_AddRefed<ZLibCompressionStreamAlgorithms>, nsresult> 74 ZLibCompressionStreamAlgorithms::Create(CompressionFormat format) { 75 RefPtr<ZLibCompressionStreamAlgorithms> alg = 76 new ZLibCompressionStreamAlgorithms(); 77 MOZ_TRY(alg->Init(format)); 78 return alg.forget(); 79 } 80 81 [[nodiscard]] nsresult ZLibCompressionStreamAlgorithms::Init( 82 CompressionFormat format) { 83 int8_t err = deflateInit2(&mZStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 84 ZLibWindowBits(format), 8 /* default memLevel */, 85 Z_DEFAULT_STRATEGY); 86 if (err == Z_MEM_ERROR) { 87 return NS_ERROR_OUT_OF_MEMORY; 88 } 89 MOZ_ASSERT(err == Z_OK); 90 return NS_OK; 91 } 92 93 // Shared by: 94 // https://wicg.github.io/compression/#compress-and-enqueue-a-chunk 95 // https://wicg.github.io/compression/#compress-flush-and-enqueue 96 void ZLibCompressionStreamAlgorithms::Compress( 97 JSContext* aCx, Span<const uint8_t> aInput, 98 JS::MutableHandleVector<JSObject*> aOutput, Flush aFlush, 99 ErrorResult& aRv) { 100 mZStream.avail_in = aInput.Length(); 101 mZStream.next_in = const_cast<uint8_t*>(aInput.Elements()); 102 103 do { 104 static uint16_t kBufferSize = 16384; 105 UniquePtr<uint8_t[], JS::FreePolicy> buffer( 106 static_cast<uint8_t*>(JS_malloc(aCx, kBufferSize))); 107 if (!buffer) { 108 aRv.ThrowTypeError("Out of memory"); 109 return; 110 } 111 112 mZStream.avail_out = kBufferSize; 113 mZStream.next_out = buffer.get(); 114 115 int8_t err = deflate(&mZStream, intoZLibFlush(aFlush)); 116 117 // From the manual: deflate() returns ... 118 switch (err) { 119 case Z_OK: 120 case Z_STREAM_END: 121 case Z_BUF_ERROR: 122 // * Z_OK if some progress has been made 123 // * Z_STREAM_END if all input has been consumed and all output has 124 // been produced (only when flush is set to Z_FINISH) 125 // * Z_BUF_ERROR if no progress is possible (for example avail_in or 126 // avail_out was zero). Note that Z_BUF_ERROR is not fatal, and 127 // deflate() can be called again with more input and more output space 128 // to continue compressing. 129 // 130 // (But of course no input should be given after Z_FINISH) 131 break; 132 case Z_STREAM_ERROR: 133 default: 134 // * Z_STREAM_ERROR if the stream state was inconsistent 135 // (which is fatal) 136 MOZ_ASSERT_UNREACHABLE("Unexpected compression error code"); 137 aRv.ThrowTypeError("Unexpected compression error"); 138 return; 139 } 140 141 // Stream should end only when flushed, see above 142 // The reverse is not true as zlib may have big data to be flushed that is 143 // larger than the buffer size 144 MOZ_ASSERT_IF(err == Z_STREAM_END, aFlush == Flush::Yes); 145 146 // At this point we either exhausted the input or the output buffer 147 MOZ_ASSERT(!mZStream.avail_in || !mZStream.avail_out); 148 149 size_t written = kBufferSize - mZStream.avail_out; 150 if (!written) { 151 break; 152 } 153 154 // Step 3: If buffer is empty, return. 155 // (We'll implicitly return when the array is empty.) 156 157 // Step 4: Split buffer into one or more non-empty pieces and convert them 158 // into Uint8Arrays. 159 // (The buffer is 'split' by having a fixed sized buffer above.) 160 161 JS::Rooted<JSObject*> view(aCx, nsJSUtils::MoveBufferAsUint8Array( 162 aCx, written, std::move(buffer))); 163 if (!view || !aOutput.append(view)) { 164 JS_ClearPendingException(aCx); 165 aRv.ThrowTypeError("Out of memory"); 166 return; 167 } 168 } while (mZStream.avail_out == 0); 169 // From the manual: 170 // If deflate returns with avail_out == 0, this function must be called 171 // again with the same value of the flush parameter and more output space 172 // (updated avail_out) 173 } 174 175 ZLibCompressionStreamAlgorithms::~ZLibCompressionStreamAlgorithms() { 176 deflateEnd(&mZStream); 177 }; 178 179 Result<already_AddRefed<ZLibDecompressionStreamAlgorithms>, nsresult> 180 ZLibDecompressionStreamAlgorithms::Create(CompressionFormat format) { 181 RefPtr<ZLibDecompressionStreamAlgorithms> alg = 182 new ZLibDecompressionStreamAlgorithms(); 183 MOZ_TRY(alg->Init(format)); 184 return alg.forget(); 185 } 186 187 [[nodiscard]] nsresult ZLibDecompressionStreamAlgorithms::Init( 188 CompressionFormat format) { 189 int8_t err = inflateInit2(&mZStream, ZLibWindowBits(format)); 190 if (err == Z_MEM_ERROR) { 191 return NS_ERROR_OUT_OF_MEMORY; 192 } 193 MOZ_ASSERT(err == Z_OK); 194 return NS_OK; 195 } 196 197 // Shared by: 198 // https://wicg.github.io/compression/#decompress-and-enqueue-a-chunk 199 // https://wicg.github.io/compression/#decompress-flush-and-enqueue 200 // All data errors throw TypeError by step 2: If this results in an error, 201 // then throw a TypeError. 202 bool ZLibDecompressionStreamAlgorithms::Decompress( 203 JSContext* aCx, Span<const uint8_t> aInput, 204 JS::MutableHandleVector<JSObject*> aOutput, Flush aFlush, 205 ErrorResult& aRv) { 206 mZStream.avail_in = aInput.Length(); 207 mZStream.next_in = const_cast<uint8_t*>(aInput.Elements()); 208 209 do { 210 UniquePtr<uint8_t[], JS::FreePolicy> buffer( 211 static_cast<uint8_t*>(JS_malloc(aCx, kBufferSize))); 212 if (!buffer) { 213 aRv.ThrowTypeError("Out of memory"); 214 return false; 215 } 216 217 mZStream.avail_out = kBufferSize; 218 mZStream.next_out = buffer.get(); 219 220 int8_t err = inflate(&mZStream, intoZLibFlush(aFlush)); 221 222 // From the manual: inflate() returns ... 223 switch (err) { 224 case Z_DATA_ERROR: 225 // Z_DATA_ERROR if the input data was corrupted (input stream not 226 // conforming to the zlib format or incorrect check value, in which 227 // case strm->msg points to a string with a more specific error) 228 aRv.ThrowTypeError("The input data is corrupted: "_ns + 229 nsDependentCString(mZStream.msg)); 230 return false; 231 case Z_MEM_ERROR: 232 // Z_MEM_ERROR if there was not enough memory 233 aRv.ThrowTypeError("Out of memory"); 234 return false; 235 case Z_NEED_DICT: 236 // Z_NEED_DICT if a preset dictionary is needed at this point 237 // 238 // From the `deflate` section of 239 // https://wicg.github.io/compression/#supported-formats: 240 // * The FDICT flag is not supported by these APIs, and will error the 241 // stream if set. 242 // And FDICT means preset dictionary per 243 // https://datatracker.ietf.org/doc/html/rfc1950#page-5. 244 aRv.ThrowTypeError( 245 "The stream needs a preset dictionary but such setup is " 246 "unsupported"); 247 return false; 248 case Z_STREAM_END: 249 // Z_STREAM_END if the end of the compressed data has been reached and 250 // all uncompressed output has been produced 251 // 252 // https://wicg.github.io/compression/#supported-formats has error 253 // conditions for each compression format when additional input comes 254 // after stream end. 255 // Note that additional calls for inflate() immediately emits 256 // Z_STREAM_END after this point. 257 mObservedStreamEnd = true; 258 break; 259 case Z_OK: 260 case Z_BUF_ERROR: 261 // * Z_OK if some progress has been made 262 // * Z_BUF_ERROR if no progress was possible or if there was not 263 // enough room in the output buffer when Z_FINISH is used. Note that 264 // Z_BUF_ERROR is not fatal, and inflate() can be called again with 265 // more input and more output space to continue decompressing. 266 // 267 // (But of course no input should be given after Z_FINISH) 268 break; 269 case Z_STREAM_ERROR: 270 default: 271 // * Z_STREAM_ERROR if the stream state was inconsistent 272 // (which is fatal) 273 MOZ_ASSERT_UNREACHABLE("Unexpected decompression error code"); 274 aRv.ThrowTypeError("Unexpected decompression error"); 275 return false; 276 } 277 278 // At this point we either exhausted the input or the output buffer, or 279 // met the stream end. 280 MOZ_ASSERT(!mZStream.avail_in || !mZStream.avail_out || mObservedStreamEnd); 281 282 size_t written = kBufferSize - mZStream.avail_out; 283 if (!written) { 284 break; 285 } 286 287 // Step 3: If buffer is empty, return. 288 // (We'll implicitly return when the array is empty.) 289 290 // Step 4: Split buffer into one or more non-empty pieces and convert them 291 // into Uint8Arrays. 292 // (The buffer is 'split' by having a fixed sized buffer above.) 293 294 JS::Rooted<JSObject*> view(aCx, nsJSUtils::MoveBufferAsUint8Array( 295 aCx, written, std::move(buffer))); 296 if (!view || !aOutput.append(view)) { 297 JS_ClearPendingException(aCx); 298 aRv.ThrowTypeError("Out of memory"); 299 return false; 300 } 301 } while (mZStream.avail_out == 0 && !mObservedStreamEnd); 302 // From the manual: 303 // * It must update next_out and avail_out when avail_out has dropped to 304 // zero. 305 // * inflate() should normally be called until it returns Z_STREAM_END or an 306 // error. 307 308 return mZStream.avail_in == 0; 309 } 310 311 ZLibDecompressionStreamAlgorithms::~ZLibDecompressionStreamAlgorithms() { 312 inflateEnd(&mZStream); 313 } 314 315 } // namespace mozilla::dom::compression