blob: 27a9701f6ef09797a45fd9d657450c66626b0c8a [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/storage/blobfs/compression/chunked.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/status.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <fs/trace.h>
#include <src/lib/chunked-compression/chunked-archive.h>
#include <src/lib/chunked-compression/chunked-decompressor.h>
#include <src/lib/chunked-compression/status.h>
#include <src/lib/chunked-compression/streaming-chunked-compressor.h>
#include "src/storage/blobfs/compression-settings.h"
namespace blobfs {
namespace {
using chunked_compression::CompressionParams;
using chunked_compression::Status;
using chunked_compression::ToZxStatus;
constexpr int kDefaultLevel = 14;
constexpr int kTargetFrameSize = 32 * 1024;
CompressionParams DefaultParams(size_t input_size) {
CompressionParams params;
params.compression_level = kDefaultLevel;
params.chunk_size = CompressionParams::ChunkSizeForInputSize(input_size, kTargetFrameSize);
return params;
}
} // namespace
// ChunkedCompressor
ChunkedCompressor::ChunkedCompressor(chunked_compression::StreamingChunkedCompressor compressor,
size_t input_len)
: compressor_(std::move(compressor)), input_len_(input_len) {}
zx_status_t ChunkedCompressor::Create(CompressionSettings settings, size_t input_size,
size_t* output_limit_out,
std::unique_ptr<ChunkedCompressor>* out) {
ZX_DEBUG_ASSERT(settings.compression_algorithm == CompressionAlgorithm::CHUNKED);
CompressionParams params = DefaultParams(input_size);
params.compression_level =
settings.compression_level ? *(settings.compression_level) : kDefaultLevel;
chunked_compression::StreamingChunkedCompressor compressor(params);
*output_limit_out = compressor.ComputeOutputSizeLimit(input_size);
*out =
std::unique_ptr<ChunkedCompressor>(new ChunkedCompressor(std::move(compressor), input_size));
return ZX_OK;
}
zx_status_t ChunkedCompressor::SetOutput(void* dst, size_t dst_len) {
if (dst_len < compressor_.ComputeOutputSizeLimit(input_len_)) {
return ZX_ERR_BUFFER_TOO_SMALL;
}
Status status = compressor_.Init(input_len_, dst, dst_len);
if (status != chunked_compression::kStatusOk) {
zx_status_t zstatus = ToZxStatus(status);
FX_LOGS(ERROR) << "Failed to initialize compressor: " << zstatus;
return zstatus;
}
return ZX_OK;
}
size_t ChunkedCompressor::BufferMax(size_t input_length) {
chunked_compression::CompressionParams params = DefaultParams(input_length);
return params.ComputeOutputSizeLimit(input_length);
}
size_t ChunkedCompressor::Size() const { return compressed_size_.value_or(0ul); }
zx_status_t ChunkedCompressor::Update(const void* input_data, size_t input_length) {
TRACE_DURATION("blobfs", "ChunkedCompressor::Update", "input_length", input_length);
if (compressor_.Update(input_data, input_length) != chunked_compression::kStatusOk) {
FX_LOGS(ERROR) << "Compression failed.";
return ZX_ERR_INTERNAL;
}
return ZX_OK;
}
zx_status_t ChunkedCompressor::End() {
TRACE_DURATION("blobfs", "ChunkedCompressor::End");
size_t sz;
Status status = compressor_.Final(&sz);
if (status != chunked_compression::kStatusOk) {
FX_LOGS(ERROR) << "Compression failed.";
return ZX_ERR_INTERNAL;
}
compressed_size_ = sz;
return ZX_OK;
}
// ChunkedDecompressor
zx_status_t ChunkedDecompressor::Decompress(void* uncompressed_buf, size_t* uncompressed_size,
const void* compressed_buf,
const size_t max_compressed_size) {
TRACE_DURATION("blobfs", "ChunkedCompressor::Decompress", "compressed_size", max_compressed_size);
chunked_compression::SeekTable seek_table;
chunked_compression::HeaderReader reader;
Status status =
reader.Parse(compressed_buf, max_compressed_size, max_compressed_size, &seek_table);
if (status != chunked_compression::kStatusOk) {
FX_LOGS(ERROR) << "Invalid archive header.";
return ToZxStatus(status);
}
size_t decompression_buf_size = *uncompressed_size;
if (decompressor_.Decompress(seek_table, compressed_buf, max_compressed_size, uncompressed_buf,
decompression_buf_size,
uncompressed_size) != chunked_compression::kStatusOk) {
FX_LOGS(ERROR) << "Failed to decompress archive.";
return ZX_ERR_IO_DATA_INTEGRITY;
}
return ZX_OK;
}
// SeekableChunkedDecompressor
zx_status_t SeekableChunkedDecompressor::CreateDecompressor(
const void* seek_table_buf, size_t max_seek_table_size, size_t max_compressed_size,
std::unique_ptr<SeekableDecompressor>* out) {
auto decompressor = std::make_unique<SeekableChunkedDecompressor>();
chunked_compression::HeaderReader reader;
Status status = reader.Parse(seek_table_buf, max_seek_table_size, max_compressed_size,
&decompressor->seek_table_);
if (status != chunked_compression::kStatusOk) {
return ToZxStatus(status);
}
*out = std::move(decompressor);
return ZX_OK;
}
zx_status_t SeekableChunkedDecompressor::DecompressRange(void* uncompressed_buf,
size_t* uncompressed_size,
const void* compressed_buf,
size_t max_compressed_size,
size_t offset) {
TRACE_DURATION("blobfs", "SeekableChunkedCompressor::DecompressRange", "length",
*uncompressed_size);
if (*uncompressed_size == 0) {
return ZX_ERR_INVALID_ARGS;
}
std::optional<unsigned> first_idx = seek_table_.EntryForDecompressedOffset(offset);
std::optional<unsigned> last_idx =
seek_table_.EntryForDecompressedOffset(offset + (*uncompressed_size) - 1);
if (!first_idx || !last_idx) {
return ZX_ERR_OUT_OF_RANGE;
}
size_t src_offset = 0;
size_t dst_offset = 0;
for (unsigned i = *first_idx; i <= *last_idx; ++i) {
const chunked_compression::SeekTableEntry& entry = seek_table_.Entries()[i];
ZX_DEBUG_ASSERT(src_offset + entry.compressed_size <= max_compressed_size);
ZX_DEBUG_ASSERT(dst_offset + entry.decompressed_size <= *uncompressed_size);
const uint8_t* src = static_cast<const uint8_t*>(compressed_buf) + src_offset;
uint8_t* dst = static_cast<uint8_t*>(uncompressed_buf) + dst_offset;
size_t bytes_in_frame;
chunked_compression::Status status =
decompressor_.DecompressFrame(seek_table_, i, src, max_compressed_size - src_offset, dst,
*uncompressed_size - dst_offset, &bytes_in_frame);
if (status != chunked_compression::kStatusOk) {
FX_LOGS(ERROR) << "DecompressFrame failed: " << status;
return ToZxStatus(status);
}
src_offset += entry.compressed_size;
dst_offset += bytes_in_frame;
}
ZX_ASSERT(dst_offset == *uncompressed_size);
return ZX_OK;
}
zx::status<CompressionMapping> SeekableChunkedDecompressor::MappingForDecompressedRange(
size_t offset, size_t len, size_t max_decompressed_len) {
if (max_decompressed_len == 0) {
return zx::error(ZX_ERR_INVALID_ARGS);
}
std::optional<unsigned> first_idx = seek_table_.EntryForDecompressedOffset(offset);
std::optional<unsigned> last_idx = seek_table_.EntryForDecompressedOffset(offset + len - 1);
if (!first_idx || !last_idx) {
return zx::error(ZX_ERR_OUT_OF_RANGE);
}
const chunked_compression::SeekTableEntry& first_entry = seek_table_.Entries()[*first_idx];
const chunked_compression::SeekTableEntry& last_entry = seek_table_.Entries()[*last_idx];
size_t compressed_end = last_entry.compressed_offset + last_entry.compressed_size;
size_t decompressed_end = last_entry.decompressed_offset + last_entry.decompressed_size;
if (compressed_end < first_entry.compressed_offset ||
decompressed_end < first_entry.decompressed_offset) {
// This likely indicates that the seek table was tampered. (Benign corruption would be caught by
// the header checksum, which is verified during header parsing.)
// Note that this condition is also checked by the underlying compression library during
// parsing, but we defensively check it here as well to prevent underflow.
return zx::error(ZX_ERR_IO_DATA_INTEGRITY);
}
// Return the computed range if its size falls within max_decompressed_len.
if (likely(decompressed_end - first_entry.decompressed_offset <= max_decompressed_len)) {
return zx::ok(CompressionMapping{
.compressed_offset = first_entry.compressed_offset,
.compressed_length = compressed_end - first_entry.compressed_offset,
.decompressed_offset = first_entry.decompressed_offset,
.decompressed_length = decompressed_end - first_entry.decompressed_offset,
});
}
size_t max_decompressed_end;
if (add_overflow(first_entry.decompressed_offset, max_decompressed_len, &max_decompressed_end)) {
// We're here because (decompressed_end - first_entry.decompressed_offset) is larger than
// max_decompressed_len. So by definition first_entry.decompressed_offset + max_decompressed_len
// cannot result in an overflow, as we know that decompressed_end is valid. This likely
// indicates some kind of corruption in the seek table.
return zx::error(ZX_ERR_IO_DATA_INTEGRITY);
}
// Start at the entry that contains the offset (max_decompressed_end - 1) and work backwards until
// we hit the required size constraint.
std::optional<unsigned> max_idx =
seek_table_.EntryForDecompressedOffset(max_decompressed_end - 1);
if (!max_idx) {
// This again cannot happen for similar reasons as the overflow check above.
return zx::error(ZX_ERR_IO_DATA_INTEGRITY);
}
unsigned idx = *max_idx;
while (idx >= first_idx) {
const chunked_compression::SeekTableEntry& max_entry = seek_table_.Entries()[idx];
compressed_end = max_entry.compressed_offset + max_entry.compressed_size;
decompressed_end = max_entry.decompressed_offset + max_entry.decompressed_size;
if (decompressed_end <= max_decompressed_end) {
return zx::ok(CompressionMapping{
.compressed_offset = first_entry.compressed_offset,
.compressed_length = compressed_end - first_entry.compressed_offset,
.decompressed_offset = first_entry.decompressed_offset,
.decompressed_length = decompressed_end - first_entry.decompressed_offset,
});
}
if (idx == 0) {
break;
}
--idx;
}
// We cannot accommodate even a single entry within max_decompressed_len.
return zx::error(ZX_ERR_OUT_OF_RANGE);
}
} // namespace blobfs