From c4b76bea13e27a3644c2c8627d33b8ef472f7b99 Mon Sep 17 00:00:00 2001 From: devgianlu Date: Sat, 1 Mar 2025 17:33:52 +0100 Subject: [PATCH] LibCompress: Introduce generic de/compressor using zlib This compressor decompressor is capable of handling zlib, gzip and deflate all in one. --- Libraries/LibCompress/CMakeLists.txt | 3 +- Libraries/LibCompress/GenericZlib.cpp | 219 ++++++++++++++++++++++++++ Libraries/LibCompress/GenericZlib.h | 101 ++++++++++++ 3 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibCompress/GenericZlib.cpp create mode 100644 Libraries/LibCompress/GenericZlib.h diff --git a/Libraries/LibCompress/CMakeLists.txt b/Libraries/LibCompress/CMakeLists.txt index 17cce020c7c..d5fb08674b4 100644 --- a/Libraries/LibCompress/CMakeLists.txt +++ b/Libraries/LibCompress/CMakeLists.txt @@ -1,8 +1,9 @@ set(SOURCES Deflate.cpp + GenericZlib.cpp + Gzip.cpp PackBitsDecoder.cpp Zlib.cpp - Gzip.cpp ) serenity_lib(LibCompress compress) diff --git a/Libraries/LibCompress/GenericZlib.cpp b/Libraries/LibCompress/GenericZlib.cpp new file mode 100644 index 00000000000..b8ff099744d --- /dev/null +++ b/Libraries/LibCompress/GenericZlib.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025, Altomani Gianluca + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include + +namespace Compress { + +static Error handle_zlib_error(int ret) +{ + switch (ret) { + case Z_ERRNO: + return Error::from_errno(errno); + case Z_DATA_ERROR: + // Z_DATA_ERROR if the input data was corrupted + return Error::from_string_literal("zlib data error"); + case Z_STREAM_ERROR: + // Z_STREAM_ERROR if the parameters are invalid, such as a null pointer to the structure + return Error::from_string_literal("zlib stream error"); + case Z_VERSION_ERROR: + // Z_VERSION_ERROR if the zlib library version is incompatible with the version assumed by the caller + return Error::from_string_literal("zlib version mismatch"); + case Z_MEM_ERROR: + // Z_MEM_ERROR if there was not enough memory + return Error::from_errno(ENOMEM); + default: + VERIFY_NOT_REACHED(); + } +} + +GenericZlibDecompressor::GenericZlibDecompressor(AK::FixedArray buffer, MaybeOwned stream, z_stream* zstream) + : m_stream(move(stream)) + , m_zstream(zstream) + , m_buffer(move(buffer)) +{ +} + +ErrorOr GenericZlibDecompressor::new_z_stream(int window_bits) +{ + auto zstream = new (nothrow) z_stream {}; + if (!zstream) + return Error::from_errno(ENOMEM); + + // The fields next_in, avail_in, zalloc, zfree and opaque must be initialized before by the caller. + zstream->next_in = nullptr; + zstream->avail_in = 0; + zstream->zalloc = nullptr; + zstream->zfree = nullptr; + zstream->opaque = nullptr; + + if (auto ret = inflateInit2(zstream, window_bits); ret != Z_OK) + return handle_zlib_error(ret); + + return zstream; +} + +GenericZlibDecompressor::~GenericZlibDecompressor() +{ + inflateEnd(m_zstream); + delete m_zstream; +} + +ErrorOr GenericZlibDecompressor::read_some(Bytes bytes) +{ + m_zstream->avail_out = bytes.size(); + m_zstream->next_out = bytes.data(); + + if (m_zstream->avail_in == 0) { + auto in = TRY(m_stream->read_some(m_buffer.span())); + m_zstream->avail_in = in.size(); + m_zstream->next_in = m_buffer.data(); + } + + auto ret = inflate(m_zstream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR) + return handle_zlib_error(ret); + + if (ret == Z_STREAM_END) { + inflateReset(m_zstream); + if (m_zstream->avail_in == 0) + m_eof = true; + } + + return bytes.slice(0, bytes.size() - m_zstream->avail_out); +} + +ErrorOr GenericZlibDecompressor::write_some(ReadonlyBytes) +{ + return Error::from_errno(EBADF); +} + +bool GenericZlibDecompressor::is_eof() const +{ + return m_eof; +} + +bool GenericZlibDecompressor::is_open() const +{ + return m_stream->is_open(); +} + +void GenericZlibDecompressor::close() +{ +} + +GenericZlibCompressor::GenericZlibCompressor(AK::FixedArray buffer, MaybeOwned stream, z_stream* zstream) + : m_stream(move(stream)) + , m_zstream(zstream) + , m_buffer(move(buffer)) +{ +} + +ErrorOr GenericZlibCompressor::new_z_stream(int window_bits, GenericZlibCompressionLevel compression_level) +{ + auto zstream = new (nothrow) z_stream {}; + if (!zstream) + return Error::from_errno(ENOMEM); + + // The fields zalloc, zfree and opaque must be initialized before by the caller. + zstream->zalloc = nullptr; + zstream->zfree = nullptr; + zstream->opaque = nullptr; + + int level = [&] { + switch (compression_level) { + case GenericZlibCompressionLevel::Fastest: + return Z_BEST_SPEED; + case GenericZlibCompressionLevel::Default: + return Z_DEFAULT_COMPRESSION; + case GenericZlibCompressionLevel::Best: + return Z_BEST_COMPRESSION; + default: + VERIFY_NOT_REACHED(); + } + }(); + + if (auto ret = deflateInit2(zstream, level, Z_DEFLATED, window_bits, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY); ret != Z_OK) + return handle_zlib_error(ret); + + return zstream; +} + +GenericZlibCompressor::~GenericZlibCompressor() +{ + deflateEnd(m_zstream); + delete m_zstream; +} + +ErrorOr GenericZlibCompressor::read_some(Bytes) +{ + return Error::from_errno(EBADF); +} + +ErrorOr GenericZlibCompressor::write_some(ReadonlyBytes bytes) +{ + m_zstream->avail_in = bytes.size(); + m_zstream->next_in = const_cast(bytes.data()); + + // If deflate returns with avail_out == 0, this function must be called again with the same value of the flush parameter + // and more output space (updated avail_out), until the flush is complete (deflate returns with non-zero avail_out). + do { + m_zstream->avail_out = m_buffer.size(); + m_zstream->next_out = m_buffer.data(); + + auto ret = deflate(m_zstream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_BUF_ERROR) + return handle_zlib_error(ret); + + auto have = m_buffer.size() - m_zstream->avail_out; + TRY(m_stream->write_until_depleted(m_buffer.span().slice(0, have))); + } while (m_zstream->avail_out == 0); + + VERIFY(m_zstream->avail_in == 0); + return bytes.size(); +} + +bool GenericZlibCompressor::is_eof() const +{ + return false; +} + +bool GenericZlibCompressor::is_open() const +{ + return m_stream->is_open(); +} + +void GenericZlibCompressor::close() +{ +} + +ErrorOr GenericZlibCompressor::finish() +{ + VERIFY(m_zstream->avail_in == 0); + + // If the parameter flush is set to Z_FINISH, pending input is processed, pending output is flushed and deflate returns with Z_STREAM_END + // if there was enough output space. If deflate returns with Z_OK or Z_BUF_ERROR, this function must be called again with Z_FINISH + // and more output space (updated avail_out) but no more input data, until it returns with Z_STREAM_END or an error. + while (true) { + m_zstream->avail_out = m_buffer.size(); + m_zstream->next_out = m_buffer.data(); + + auto ret = deflate(m_zstream, Z_FINISH); + if (ret == Z_STREAM_END || ret == Z_BUF_ERROR || ret == Z_OK) { + auto have = m_buffer.size() - m_zstream->avail_out; + TRY(m_stream->write_until_depleted(m_buffer.span().slice(0, have))); + + if (ret == Z_STREAM_END) + return {}; + } else { + return handle_zlib_error(ret); + } + } +} + +} diff --git a/Libraries/LibCompress/GenericZlib.h b/Libraries/LibCompress/GenericZlib.h new file mode 100644 index 00000000000..a2db6fefc80 --- /dev/null +++ b/Libraries/LibCompress/GenericZlib.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Altomani Gianluca + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +extern "C" { +typedef struct z_stream_s z_stream; +} + +namespace Compress { + +enum class GenericZlibCompressionLevel : u8 { + Fastest, + Default, + Best, +}; + +class GenericZlibDecompressor : public Stream { + AK_MAKE_NONCOPYABLE(GenericZlibDecompressor); + +public: + ~GenericZlibDecompressor() override; + + virtual ErrorOr read_some(Bytes) override; + virtual ErrorOr write_some(ReadonlyBytes) override; + virtual bool is_eof() const override; + virtual bool is_open() const override; + virtual void close() override; + +protected: + GenericZlibDecompressor(AK::FixedArray, MaybeOwned, z_stream*); + + static ErrorOr new_z_stream(int window_bits); + +private: + MaybeOwned m_stream; + z_stream* m_zstream; + + bool m_eof { false }; + + AK::FixedArray m_buffer; +}; + +class GenericZlibCompressor : public Stream { + AK_MAKE_NONCOPYABLE(GenericZlibCompressor); + +public: + ~GenericZlibCompressor() override; + + virtual ErrorOr read_some(Bytes) override; + virtual ErrorOr write_some(ReadonlyBytes) override; + virtual bool is_eof() const override; + virtual bool is_open() const override; + virtual void close() override; + ErrorOr finish(); + +protected: + GenericZlibCompressor(AK::FixedArray, MaybeOwned, z_stream*); + + static ErrorOr new_z_stream(int window_bits, GenericZlibCompressionLevel compression_level); + +private: + MaybeOwned m_stream; + z_stream* m_zstream; + + AK::FixedArray m_buffer; +}; + +template +ErrorOr decompress_all(ReadonlyBytes bytes) +{ + auto input_stream = make(bytes); + auto deflate_stream = TRY(T::create(MaybeOwned(move(input_stream)))); + return TRY(deflate_stream->read_until_eof(4096)); +} + +template +ErrorOr compress_all(ReadonlyBytes bytes, GenericZlibCompressionLevel compression_level) +{ + auto output_stream = TRY(try_make()); + auto gzip_stream = TRY(T::create(MaybeOwned { *output_stream }, compression_level)); + + TRY(gzip_stream->write_until_depleted(bytes)); + TRY(gzip_stream->finish()); + + auto buffer = TRY(ByteBuffer::create_uninitialized(output_stream->used_buffer_size())); + TRY(output_stream->read_until_filled(buffer.bytes())); + + return buffer; +} + +}