diff --git a/Tests/LibCompress/CMakeLists.txt b/Tests/LibCompress/CMakeLists.txt index 7d15595468..39a96ce6fc 100644 --- a/Tests/LibCompress/CMakeLists.txt +++ b/Tests/LibCompress/CMakeLists.txt @@ -3,6 +3,7 @@ set(TEST_SOURCES TestDeflate.cpp TestGzip.cpp TestLzma.cpp + TestPackBits.cpp TestXz.cpp TestZlib.cpp ) diff --git a/Tests/LibCompress/TestPackBits.cpp b/Tests/LibCompress/TestPackBits.cpp new file mode 100644 index 0000000000..cd32339cc8 --- /dev/null +++ b/Tests/LibCompress/TestPackBits.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include + +TEST_CASE(pack_bits) +{ + Array const compressed { + 0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, 0xAA + }; + + Array const raw { + 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, + 0x2A, 0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA + }; + + auto unpacked = TRY_OR_FAIL(Compress::PackBits::decode_all(compressed)); + EXPECT_EQ(unpacked.bytes(), raw); +} diff --git a/Userland/Libraries/LibCompress/CMakeLists.txt b/Userland/Libraries/LibCompress/CMakeLists.txt index 5c3bfaf10b..e903a1ba5d 100644 --- a/Userland/Libraries/LibCompress/CMakeLists.txt +++ b/Userland/Libraries/LibCompress/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES Deflate.cpp Lzma.cpp Lzma2.cpp + PackBitsDecoder.cpp Xz.cpp Zlib.cpp Gzip.cpp diff --git a/Userland/Libraries/LibCompress/PackBitsDecoder.cpp b/Userland/Libraries/LibCompress/PackBitsDecoder.cpp new file mode 100644 index 0000000000..537b4a3fcf --- /dev/null +++ b/Userland/Libraries/LibCompress/PackBitsDecoder.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "PackBitsDecoder.h" +#include + +namespace Compress::PackBits { + +ErrorOr decode_all(ReadonlyBytes bytes, Optional expected_output_size, CompatibilityMode mode) +{ + // This implementation uses unsigned values for the selector, as described in the PDF spec. + // Note that this remains compatible with other implementations based on signed numbers. + + auto memory_stream = make(bytes); + + ByteBuffer decoded_bytes; + + if (expected_output_size.has_value()) + TRY(decoded_bytes.try_ensure_capacity(*expected_output_size)); + + while (memory_stream->remaining() > 0 && decoded_bytes.size() < expected_output_size.value_or(NumericLimits::max())) { + auto const length = TRY(memory_stream->read_value()); + + if (length < 128) { + for (u8 i = 0; i <= length; ++i) + TRY(decoded_bytes.try_append(TRY(memory_stream->read_value()))); + } else if (length > 128) { + auto const next_byte = TRY(memory_stream->read_value()); + + for (u8 i = 0; i < 257 - length; ++i) + TRY(decoded_bytes.try_append(next_byte)); + } else { + VERIFY(length == 128); + if (mode == CompatibilityMode::PDF) + break; + } + } + + return decoded_bytes; +} + +} diff --git a/Userland/Libraries/LibCompress/PackBitsDecoder.h b/Userland/Libraries/LibCompress/PackBitsDecoder.h new file mode 100644 index 0000000000..1badb8dc60 --- /dev/null +++ b/Userland/Libraries/LibCompress/PackBitsDecoder.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Compress::PackBits { + +// This implements the PackBits compression scheme, aka run-length compression +// It is fairly simple and described here: https://web.archive.org/web/20080705155158/http://developer.apple.com/technotes/tn/tn1023.html +// But also in section: +// - 7.4.5 RunLengthDecode Filter of the PDF specification +// - Section 9: PackBits Compression of the TIFF specification + +enum class CompatibilityMode { + Original, // 128 is defined as no-op + PDF, // 128 is defined as end of stream +}; + +ErrorOr decode_all(ReadonlyBytes bytes, Optional expected_output_size = {}, CompatibilityMode mode = CompatibilityMode::Original); + +}