diff options
Diffstat (limited to 'js/src/vm/Compression.cpp')
-rw-r--r-- | js/src/vm/Compression.cpp | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/js/src/vm/Compression.cpp b/js/src/vm/Compression.cpp new file mode 100644 index 0000000000..eda3c9a522 --- /dev/null +++ b/js/src/vm/Compression.cpp @@ -0,0 +1,262 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * 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 "vm/Compression.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/MemoryChecking.h" +#include "mozilla/PodOperations.h" +#include "mozilla/ScopeExit.h" + +#include "js/Utility.h" +#include "util/Memory.h" + +using namespace js; + +static void* zlib_alloc(void* cx, uInt items, uInt size) { + return js_calloc(items, size); +} + +static void zlib_free(void* cx, void* addr) { js_free(addr); } + +Compressor::Compressor(const unsigned char* inp, size_t inplen) + : inp(inp), + inplen(inplen), + initialized(false), + finished(false), + currentChunkSize(0), + chunkOffsets() { + MOZ_ASSERT(inplen > 0, "data to compress can't be empty"); + + zs.opaque = nullptr; + zs.next_in = (Bytef*)inp; + zs.avail_in = 0; + zs.next_out = nullptr; + zs.avail_out = 0; + zs.zalloc = zlib_alloc; + zs.zfree = zlib_free; + zs.total_in = 0; + zs.total_out = 0; + zs.msg = nullptr; + zs.state = nullptr; + zs.data_type = 0; + zs.adler = 0; + zs.reserved = 0; + + // Reserve space for the CompressedDataHeader. + outbytes = sizeof(CompressedDataHeader); +} + +Compressor::~Compressor() { + if (initialized) { + int ret = deflateEnd(&zs); + if (ret != Z_OK) { + // If we finished early, we can get a Z_DATA_ERROR. + MOZ_ASSERT(ret == Z_DATA_ERROR); + MOZ_ASSERT(!finished); + } + } +} + +// According to the zlib docs, the default value for windowBits is 15. Passing +// -15 is treated the same, but it also forces 'raw deflate' (no zlib header or +// trailer). Raw deflate is necessary for chunked decompression. +static const int WindowBits = -15; + +bool Compressor::init() { + if (inplen >= UINT32_MAX) { + return false; + } + // zlib is slow and we'd rather be done compression sooner + // even if it means decompression is slower which penalizes + // Function.toString() + int ret = deflateInit2(&zs, Z_BEST_SPEED, Z_DEFLATED, WindowBits, 8, + Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + MOZ_ASSERT(ret == Z_MEM_ERROR); + return false; + } + initialized = true; + return true; +} + +void Compressor::setOutput(unsigned char* out, size_t outlen) { + MOZ_ASSERT(outlen > outbytes); + zs.next_out = out + outbytes; + zs.avail_out = outlen - outbytes; +} + +Compressor::Status Compressor::compressMore() { + MOZ_ASSERT(zs.next_out); + uInt left = inplen - (zs.next_in - inp); + if (left <= MAX_INPUT_SIZE) { + zs.avail_in = left; + } else if (zs.avail_in == 0) { + zs.avail_in = MAX_INPUT_SIZE; + } + + // Finish the current chunk if needed. + bool flush = false; + MOZ_ASSERT(currentChunkSize <= CHUNK_SIZE); + if (currentChunkSize + zs.avail_in >= CHUNK_SIZE) { + // Adjust avail_in, so we don't get chunks that are larger than + // CHUNK_SIZE. + zs.avail_in = CHUNK_SIZE - currentChunkSize; + MOZ_ASSERT(currentChunkSize + zs.avail_in == CHUNK_SIZE); + flush = true; + } + + MOZ_ASSERT(zs.avail_in <= left); + bool done = zs.avail_in == left; + + Bytef* oldin = zs.next_in; + Bytef* oldout = zs.next_out; + int ret = deflate(&zs, done ? Z_FINISH : (flush ? Z_FULL_FLUSH : Z_NO_FLUSH)); + outbytes += zs.next_out - oldout; + currentChunkSize += zs.next_in - oldin; + MOZ_ASSERT(currentChunkSize <= CHUNK_SIZE); + + if (ret == Z_MEM_ERROR) { + zs.avail_out = 0; + return OOM; + } + if (ret == Z_BUF_ERROR || (ret == Z_OK && zs.avail_out == 0)) { + // We have to resize the output buffer. Note that we're not done yet + // because ret != Z_STREAM_END. + MOZ_ASSERT(zs.avail_out == 0); + return MOREOUTPUT; + } + + if (done || currentChunkSize == CHUNK_SIZE) { + MOZ_ASSERT_IF(!done, flush); + MOZ_ASSERT(chunkSize(inplen, chunkOffsets.length()) == currentChunkSize); + if (!chunkOffsets.append(outbytes)) { + return OOM; + } + currentChunkSize = 0; + MOZ_ASSERT_IF(done, chunkOffsets.length() == (inplen - 1) / CHUNK_SIZE + 1); + } + + MOZ_ASSERT_IF(!done, ret == Z_OK); + MOZ_ASSERT_IF(done, ret == Z_STREAM_END); + return done ? DONE : CONTINUE; +} + +size_t Compressor::totalBytesNeeded() const { + return AlignBytes(outbytes, sizeof(uint32_t)) + sizeOfChunkOffsets(); +} + +void Compressor::finish(char* dest, size_t destBytes) { + MOZ_ASSERT(!chunkOffsets.empty()); + + CompressedDataHeader* compressedHeader = + reinterpret_cast<CompressedDataHeader*>(dest); + compressedHeader->compressedBytes = outbytes; + + size_t outbytesAligned = AlignBytes(outbytes, sizeof(uint32_t)); + + // Zero the padding bytes, the ImmutableStringsCache will hash them. + mozilla::PodZero(dest + outbytes, outbytesAligned - outbytes); + + uint32_t* destArr = reinterpret_cast<uint32_t*>(dest + outbytesAligned); + + MOZ_ASSERT(uintptr_t(dest + destBytes) == + uintptr_t(destArr + chunkOffsets.length())); + mozilla::PodCopy(destArr, chunkOffsets.begin(), chunkOffsets.length()); + + finished = true; +} + +bool js::DecompressString(const unsigned char* inp, size_t inplen, + unsigned char* out, size_t outlen) { + MOZ_ASSERT(inplen <= UINT32_MAX); + + // Mark the memory we pass to zlib as initialized for MSan. + MOZ_MAKE_MEM_DEFINED(out, outlen); + + z_stream zs; + zs.zalloc = zlib_alloc; + zs.zfree = zlib_free; + zs.opaque = nullptr; + zs.next_in = (Bytef*)inp; + zs.avail_in = inplen; + zs.next_out = out; + MOZ_ASSERT(outlen); + zs.avail_out = outlen; + int ret = inflateInit(&zs); + if (ret != Z_OK) { + MOZ_ASSERT(ret == Z_MEM_ERROR); + return false; + } + ret = inflate(&zs, Z_FINISH); + MOZ_ASSERT(ret == Z_STREAM_END); + ret = inflateEnd(&zs); + MOZ_ASSERT(ret == Z_OK); + return true; +} + +bool js::DecompressStringChunk(const unsigned char* inp, size_t chunk, + unsigned char* out, size_t outlen) { + MOZ_ASSERT(outlen <= Compressor::CHUNK_SIZE); + + const CompressedDataHeader* header = + reinterpret_cast<const CompressedDataHeader*>(inp); + + size_t compressedBytes = header->compressedBytes; + size_t compressedBytesAligned = AlignBytes(compressedBytes, sizeof(uint32_t)); + + const unsigned char* offsetBytes = inp + compressedBytesAligned; + const uint32_t* offsets = reinterpret_cast<const uint32_t*>(offsetBytes); + + uint32_t compressedStart = + chunk > 0 ? offsets[chunk - 1] : sizeof(CompressedDataHeader); + uint32_t compressedEnd = offsets[chunk]; + + MOZ_ASSERT(compressedStart < compressedEnd); + MOZ_ASSERT(compressedEnd <= compressedBytes); + + bool lastChunk = compressedEnd == compressedBytes; + + // Mark the memory we pass to zlib as initialized for MSan. + MOZ_MAKE_MEM_DEFINED(out, outlen); + + z_stream zs; + zs.zalloc = zlib_alloc; + zs.zfree = zlib_free; + zs.opaque = nullptr; + zs.next_in = (Bytef*)(inp + compressedStart); + zs.avail_in = compressedEnd - compressedStart; + zs.next_out = out; + MOZ_ASSERT(outlen); + zs.avail_out = outlen; + + // Bug 1505857 - Use 'volatile' so variable is preserved in crashdump + // when release-asserts below are tripped. + volatile int ret = inflateInit2(&zs, WindowBits); + if (ret != Z_OK) { + MOZ_ASSERT(ret == Z_MEM_ERROR); + return false; + } + + auto autoCleanup = mozilla::MakeScopeExit([&] { + mozilla::DebugOnly<int> ret = inflateEnd(&zs); + MOZ_ASSERT(ret == Z_OK); + }); + + if (lastChunk) { + ret = inflate(&zs, Z_FINISH); + MOZ_RELEASE_ASSERT(ret == Z_STREAM_END); + } else { + ret = inflate(&zs, Z_NO_FLUSH); + if (ret == Z_MEM_ERROR) { + return false; + } + MOZ_RELEASE_ASSERT(ret == Z_OK); + } + MOZ_ASSERT(zs.avail_in == 0); + MOZ_ASSERT(zs.avail_out == 0); + return true; +} |