blob: e5f6ef1c5fed910de037e362489ca3ae8fc713a7 [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 <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <fbl/unique_fd.h>
#include <src/lib/chunked-compression/chunked-compressor.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>
namespace {
using chunked_compression::ChunkedCompressor;
using chunked_compression::ChunkedDecompressor;
using chunked_compression::CompressionParams;
using chunked_compression::HeaderReader;
using chunked_compression::SeekTable;
using chunked_compression::StreamingChunkedCompressor;
constexpr const char kAnsiUpLine[] = "\33[A";
constexpr const char kAnsiClearLine[] = "\33[2K\r";
constexpr size_t kTargetFrameSize = 32 * 1024;
// ProgressWriter writes live a progress indicator to stdout. Updates are written in-place
// (using ANSI control codes to rewrite the current line).
class ProgressWriter {
public:
explicit ProgressWriter(int refresh_hz = 60) : refresh_hz_(refresh_hz) {
last_report_ = std::chrono::steady_clock::time_point::min();
printf("\n");
}
void Update(const char* fmt, ...) {
auto now = std::chrono::steady_clock::now();
if (now < last_report_ + refresh_duration()) {
return;
}
last_report_ = now;
printf("%s%s", kAnsiUpLine, kAnsiClearLine);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
fflush(stdout);
}
void Final(const char* fmt, ...) {
printf("%s%s", kAnsiUpLine, kAnsiClearLine);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
fflush(stdout);
}
std::chrono::steady_clock::duration refresh_duration() const {
return std::chrono::seconds(1) / refresh_hz_;
}
private:
std::chrono::steady_clock::time_point last_report_;
int refresh_hz_;
};
void usage(const char* fname) {
fprintf(stderr, "Usage: %s [--level #] [--stream] [--checksum] (d | c) source dest\n", fname);
fprintf(stderr,
"\
c: Compress source, writing to dest.\n\
d: Decompress source, writing to dest.\n\
--stream: (compression only) Use stream compression\n\
--checksum: (compression only) Include a per-frame checksum\n\
--level #: Compression level\n");
}
// Opens |file|, truncates to |write_size|, and mmaps the file for writing.
// Returns the mapped buffer in |out_write_buf|, and the managed FD in |out_fd|.
int OpenAndMapForWriting(const char* file, size_t write_size, uint8_t** out_write_buf,
fbl::unique_fd* out_fd) {
fbl::unique_fd fd(open(file, O_RDWR | O_CREAT | O_TRUNC, 0644));
if (!fd.is_valid()) {
fprintf(stderr, "Failed to open '%s': %s\n", file, strerror(errno));
return 1;
}
if (ftruncate(fd.get(), write_size)) {
fprintf(stderr, "Failed to truncate '%s': %s\n", file, strerror(errno));
return 1;
}
void* data = nullptr;
if (write_size > 0) {
data = mmap(NULL, write_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
if (data == MAP_FAILED) {
fprintf(stderr, "mmap failed: %s\n", strerror(errno));
return 1;
}
}
*out_write_buf = static_cast<uint8_t*>(data);
*out_fd = std::move(fd);
return 0;
}
size_t GetFileSize(const char* file) {
struct stat info;
if (stat(file, &info) < 0) {
fprintf(stderr, "stat(%s) failed: %s\n", file, strerror(errno));
return 0;
}
if (!S_ISREG(info.st_mode)) {
fprintf(stderr, "%s is not a regular file\n", file);
return 0;
}
return info.st_size;
}
// Opens |file| and mmaps the file for reading.
// Returns the mapped buffer in |out_buf|, the size of the file in |out_size|, and the managed FD in
// |out_fd|.
int OpenAndMapForReading(const char* file, fbl::unique_fd* out_fd, const uint8_t** out_buf,
size_t* out_size) {
fbl::unique_fd fd(open(file, O_RDONLY));
if (!fd.is_valid()) {
fprintf(stderr, "Failed to open '%s': %s\n", file, strerror(errno));
return 1;
}
size_t size;
struct stat info;
if (fstat(fd.get(), &info) < 0) {
fprintf(stderr, "stat(%s) failed: %s\n", file, strerror(errno));
return 1;
}
if (!S_ISREG(info.st_mode)) {
fprintf(stderr, "%s is not a regular file\n", file);
return 1;
}
size = info.st_size;
void* data = nullptr;
if (size > 0) {
data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd.get(), 0);
if (data == MAP_FAILED) {
fprintf(stderr, "mmap failed: %s\n", strerror(errno));
return 1;
}
}
*out_fd = std::move(fd);
*out_buf = static_cast<uint8_t*>(data);
*out_size = size;
return 0;
}
// Reads |sz| bytes from |src| and compresses it, writing the output to |dst_file|.
int Compress(const uint8_t* src, size_t sz, const char* dst_file, int level, bool checksum) {
CompressionParams params;
params.frame_checksum = checksum;
params.compression_level = level;
params.chunk_size = CompressionParams::ChunkSizeForInputSize(sz, kTargetFrameSize);
size_t output_limit = params.ComputeOutputSizeLimit(sz);
ChunkedCompressor compressor(params);
fbl::unique_fd dst_fd;
uint8_t* write_buf;
if (OpenAndMapForWriting(dst_file, output_limit, &write_buf, &dst_fd)) {
return 1;
}
ProgressWriter progress;
compressor.SetProgressCallback([&](size_t bytes_read, size_t bytes_total, size_t bytes_written) {
progress.Update("%2.0f%% (%lu bytes written)\n",
static_cast<double>(bytes_read) / static_cast<double>(bytes_total) * 100,
bytes_written);
});
size_t compressed_size;
if (compressor.Compress(src, sz, write_buf, output_limit, &compressed_size) !=
chunked_compression::kStatusOk) {
return 1;
}
double compression_percent = static_cast<double>(sz) - static_cast<double>(compressed_size);
if (sz) {
compression_percent = compression_percent / static_cast<double>(sz) * 100;
} else {
compression_percent = 0;
}
progress.Final("Wrote %lu bytes (%2.0f%% compression)\n", compressed_size, compression_percent);
ftruncate(dst_fd.get(), compressed_size);
return 0;
}
// Reads |sz| bytes from |src_fd| and compresses it using a streaming compressor, writing the output
// to |dst_file|.
int CompressStream(fbl::unique_fd src_fd, size_t sz, const char* dst_file, int level,
bool checksum) {
CompressionParams params;
params.frame_checksum = checksum;
params.compression_level = level;
params.chunk_size = CompressionParams::ChunkSizeForInputSize(sz, kTargetFrameSize);
size_t output_limit = params.ComputeOutputSizeLimit(sz);
StreamingChunkedCompressor compressor(params);
fbl::unique_fd dst_fd;
uint8_t* write_buf;
if (OpenAndMapForWriting(dst_file, output_limit, &write_buf, &dst_fd)) {
return 1;
}
if (compressor.Init(sz, write_buf, output_limit) != chunked_compression::kStatusOk) {
fprintf(stderr, "Init failed\n");
return 1;
}
ProgressWriter progress;
compressor.SetProgressCallback([&](size_t bytes_read, size_t bytes_total, size_t bytes_written) {
progress.Update("%2.0f%% (%lu bytes written)\n",
static_cast<double>(bytes_read) / static_cast<double>(bytes_total) * 100,
bytes_written);
});
FILE* in = fdopen(src_fd.get(), "r");
ZX_ASSERT(in != nullptr);
const size_t chunk_size = 8192;
uint8_t buf[chunk_size];
size_t bytes_read = 0;
for (size_t off = 0; off < sz; off += chunk_size) {
size_t r = fread(buf, sizeof(uint8_t), chunk_size, in);
if (r == 0) {
int err = ferror(in);
if (err) {
fprintf(stderr, "fread failed: %s\n", strerror(err));
return err;
}
break;
}
if (compressor.Update(buf, r) != chunked_compression::kStatusOk) {
fprintf(stderr, "Update failed\n");
return 1;
}
bytes_read += r;
}
if (bytes_read < sz) {
fprintf(stderr, "Only read %lu bytes (expected %lu)\n", bytes_read, sz);
}
size_t compressed_size;
if (compressor.Final(&compressed_size) != chunked_compression::kStatusOk) {
fprintf(stderr, "Final failed\n");
return 1;
}
double compression_percent = static_cast<double>(sz) - static_cast<double>(compressed_size);
if (sz) {
compression_percent = compression_percent / static_cast<double>(sz) * 100;
} else {
compression_percent = 0;
}
progress.Final("Wrote %lu bytes (%2.0f%% compression)\n", compressed_size, compression_percent);
ftruncate(dst_fd.get(), compressed_size);
return 0;
}
// Reads |sz| bytes from |src| and decompresses them, writing the results to |dst_file|.
int Decompress(const uint8_t* src, size_t sz, const char* dst_file) {
SeekTable table;
HeaderReader reader;
if ((reader.Parse(src, sz, sz, &table)) != chunked_compression::kStatusOk) {
fprintf(stderr, "Failed to parse input file; not a chunked archive?\n");
return 1;
}
size_t output_size = ChunkedDecompressor::ComputeOutputSize(table);
fbl::unique_fd dst_fd;
uint8_t* write_buf;
if (OpenAndMapForWriting(dst_file, output_size, &write_buf, &dst_fd)) {
return 1;
}
ChunkedDecompressor decompressor;
size_t bytes_written;
if (decompressor.Decompress(table, src, sz, write_buf, output_size, &bytes_written) !=
chunked_compression::kStatusOk) {
return 1;
}
printf("Wrote %lu bytes (%2.0f%% compression)\n", bytes_written,
static_cast<double>(sz) / static_cast<double>(bytes_written) * 100);
return 0;
}
} // namespace
int main(int argc, char* const* argv) {
bool checksum = false;
bool stream = false;
int level = CompressionParams::DefaultCompressionLevel();
while (1) {
static struct option opts[] = {
{"stream", no_argument, nullptr, 's'},
{"level", required_argument, nullptr, 'l'},
{"checksum", no_argument, nullptr, 'c'},
{nullptr, 0, nullptr, 0},
};
int c = getopt_long(argc, argv, "sl:c", opts, nullptr);
if (c < 0) {
break;
}
switch (c) {
case 'l': {
char* endp;
long val = strtol(optarg, &endp, 10);
if (endp != optarg + strlen(optarg)) {
usage(argv[0]);
return 1;
} else if (level < CompressionParams::MinCompressionLevel() ||
level > CompressionParams::MaxCompressionLevel()) {
fprintf(stderr, "Invalid level %d, should be in range %d <= level <= %d\n", level,
CompressionParams::MinCompressionLevel(),
CompressionParams::MaxCompressionLevel());
return 1;
}
level = static_cast<int>(val);
break;
}
case 's': {
stream = true;
break;
}
case 'c': {
checksum = true;
break;
}
default:
usage(argv[0]);
return 1;
}
}
const char* bin_name = argv[0];
argc -= optind;
argv += optind;
if (argc < 3) {
usage(bin_name);
return 1;
}
const char* mode_str = argv[0];
const char* input_file = argv[1];
const char* output_file = argv[2];
enum Mode {
COMPRESS,
DECOMPRESS,
UNKNOWN,
};
Mode mode = Mode::UNKNOWN;
if (!strcmp(mode_str, "d")) {
mode = Mode::DECOMPRESS;
} else if (!strcmp(mode_str, "c")) {
mode = Mode::COMPRESS;
} else {
fprintf(stderr, "Invalid mode (should be 'd' or 'c').\n");
usage(bin_name);
return 1;
}
if (stream) {
if (mode == Mode::DECOMPRESS) {
printf("Ignoring --stream flag for decompression\n");
} else {
fbl::unique_fd fd(open(input_file, O_RDONLY));
if (!fd.is_valid()) {
fprintf(stderr, "Failed to open '%s': %s\n", input_file, strerror(errno));
return 1;
}
return CompressStream(std::move(fd), GetFileSize(input_file), output_file, level, checksum);
}
}
if (checksum && mode == Mode::DECOMPRESS) {
printf("Ignoring --checksum flag for decompression\n");
}
fbl::unique_fd src_fd;
const uint8_t* src_data;
size_t src_size;
if (OpenAndMapForReading(input_file, &src_fd, &src_data, &src_size)) {
return 1;
}
return mode == Mode::COMPRESS ? Compress(src_data, src_size, output_file, level, checksum)
: Decompress(src_data, src_size, output_file);
}