blob: e537016188c178c1946743fa355a984e32beb9bf [file] [log] [blame]
// Copyright 2018 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 <dirent.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <getopt.h>
#include <lib/cksum.h>
#include <lib/zbitl/item.h>
#include <lib/zbitl/json.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>
#include <zircon/boot/bootfs.h>
#include <zircon/boot/image.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <filesystem>
#include <forward_list>
#include <functional>
#include <limits>
#include <list>
#include <map>
#include <memory>
#include <numeric>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#include <fbl/auto_call.h>
#include <fbl/macros.h>
#include <fbl/unique_fd.h>
#include <lz4/lz4frame.h>
#include <rapidjson/document.h>
#include <rapidjson/filewritestream.h>
#include <rapidjson/prettywriter.h>
#include <zstd/zstd.h>
namespace {
const char* const kCmdlineWS = " \t\r\n";
// The size of the buffer that's used for reading/writing JSON in streaming mode.
// The optimal size is application specific, we use a reasonable default.
constexpr size_t kJsonBufferSize = 4096;
bool Aligned(uint32_t length) { return length % ZBI_ALIGNMENT == 0; }
// iovec.iov_base is void* but we only use pointers to const.
template <typename T>
iovec Iovec(const T* buffer, size_t size = sizeof(T)) {
return {const_cast<void*>(static_cast<const void*>(buffer)), size};
}
class AppendBuffer final {
public:
explicit AppendBuffer(size_t size)
: buffer_(std::make_unique<std::byte[]>(size)), ptr_(buffer_.get()) {}
size_t size() const { return ptr_ - buffer_.get(); }
iovec get() { return Iovec(buffer_.get(), size()); }
std::unique_ptr<std::byte[]> release() {
ptr_ = nullptr;
return std::move(buffer_);
}
template <typename T>
void Append(const T* data, size_t bytes = sizeof(T)) {
if (bytes != 0) {
ptr_ = static_cast<std::byte*>(
memcpy(static_cast<void*>(ptr_), static_cast<const void*>(data), bytes)) +
bytes;
}
}
void Pad(size_t bytes) {
ptr_ = static_cast<std::byte*>(memset(static_cast<void*>(ptr_), 0, bytes)) + bytes;
}
private:
std::unique_ptr<std::byte[]> buffer_;
std::byte* ptr_ = nullptr;
};
class Item;
using ItemPtr = std::unique_ptr<Item>;
class OutputStream final {
public:
OutputStream() = delete;
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(OutputStream);
OutputStream(OutputStream&&) = default;
explicit OutputStream(fbl::unique_fd fd) : fd_(std::move(fd)) {}
~OutputStream() { Flush(); }
// Queue the iovec for output. The second argument can transfer
// ownership of the memory that buffer.iov_base points into. This
// object may refer to buffer.iov_base until Flush() completes.
void Write(const iovec& buffer, std::unique_ptr<std::byte[]> owned = nullptr) {
if (buffer.iov_len == 0) {
return;
}
if (buffer.iov_len + total_ > UINT32_MAX - sizeof(zbi_header_t) + 1) {
fprintf(stderr, "output size exceeds format maximum\n");
exit(1);
}
total_ += static_cast<uint32_t>(buffer.iov_len);
*write_pos_++ = buffer;
if (write_pos_ == iov_.end()) {
Flush();
} else if (owned) {
owned_buffers_.push_front(std::move(owned));
}
}
uint32_t WritePosition() const { return total_; }
void Flush() {
auto read_pos = iov_.begin();
while (read_pos != write_pos_) {
read_pos = WriteBuffers(read_pos);
}
write_pos_ = iov_.begin();
owned_buffers_.clear();
}
// Emit a placeholder. The return value will be passed to PatchHeader.
uint32_t PlaceHeader() {
uint32_t pos = WritePosition();
static const zbi_header_t dummy = {};
Write(Iovec(&dummy));
return pos;
}
// Replace a placeholder with a real header.
void PatchHeader(const zbi_header_t& header, uint32_t place) {
assert(place < total_);
assert(total_ - place >= sizeof(header));
if (flushed_ <= place) {
// We haven't actually written it yet, so just update it in
// memory. A placeholder always has its own iovec, so just
// skip over earlier ones until we hit the right offset.
auto it = iov_.begin();
for (place -= flushed_; place > 0; place -= it++->iov_len) {
assert(it != write_pos_);
assert(place >= it->iov_len);
}
assert(it->iov_len == sizeof(header));
auto buffer = std::make_unique<std::byte[]>(sizeof(header));
it->iov_base = memcpy(buffer.get(), &header, sizeof(header));
owned_buffers_.push_front(std::move(buffer));
} else {
assert(flushed_ >= place + sizeof(header));
// Overwrite the earlier part of the file with pwrite. This
// does not affect the current lseek position for the next writev.
auto buf = reinterpret_cast<const std::byte*>(&header);
size_t len = sizeof(header);
while (len > 0) {
ssize_t wrote = pwrite(fd_.get(), buf, len, place);
if (wrote < 0) {
perror("pwrite on output file");
exit(1);
}
len -= wrote;
buf += wrote;
place += wrote;
}
}
}
private:
using IovecArray = std::array<iovec, IOV_MAX>;
IovecArray iov_;
IovecArray::iterator write_pos_ = iov_.begin();
// iov_[n].iov_base might point into these buffers. They're just
// stored here to own the buffers until iov_ is flushed.
std::forward_list<std::unique_ptr<std::byte[]>> owned_buffers_;
fbl::unique_fd fd_;
uint32_t flushed_ = 0;
uint32_t total_ = 0;
bool Buffering() const { return write_pos_ != iov_.begin(); }
IovecArray::iterator WriteBuffers(IovecArray::iterator read_pos) {
assert(read_pos != write_pos_);
ssize_t wrote = writev(fd_.get(), &(*read_pos), static_cast<int>(write_pos_ - read_pos));
if (wrote < 0) {
perror("writev to output file");
exit(1);
}
flushed_ += wrote;
#ifndef NDEBUG
off_t pos = lseek(fd_.get(), 0, SEEK_CUR);
#endif
assert(static_cast<off_t>(flushed_) == pos || (pos == -1 && errno == ESPIPE));
// Skip all the buffers that were wholly written.
while ((size_t)wrote >= read_pos->iov_len) {
wrote -= read_pos->iov_len;
++read_pos;
if (wrote == 0) {
break;
}
assert(read_pos != write_pos_);
}
if (wrote > 0) {
// writev wrote only part of this buffer. Do the rest next time.
read_pos->iov_len -= wrote;
read_pos->iov_base = static_cast<void*>(static_cast<std::byte*>(read_pos->iov_base) + wrote);
}
return read_pos;
}
};
class FileWriter final {
public:
FileWriter(const char* outfile, std::filesystem::path prefix)
: prefix_(std::move(prefix)), outfile_(outfile) {
if (prefix_.empty()) {
prefix_ = ".";
}
}
unsigned int NextFileNumber() const { return files_ + 1; }
OutputStream RawFile(const char* name) {
++files_;
if (outfile_) {
if (files_ > 1) {
fprintf(stderr, "--output (-o) cannot write second file %s\n", name);
exit(1);
} else {
return CreateFile(outfile_);
}
} else {
auto file = prefix_ / name;
return CreateFile(file.c_str());
}
}
template <typename T1, typename T2>
void HardLink(const T1& target, const T2& link) {
const auto target_path = prefix_ / target;
const auto link_path = prefix_ / link;
auto linkit = [&]() {
std::error_code ec;
std::filesystem::create_hard_link(target_path, link_path, ec);
return ec;
};
std::error_code ec = linkit();
if (ec) {
switch (ec.value()) {
case ENOENT:
MakeDirs(link_path);
ec = linkit();
break;
case EEXIST:
std::filesystem::remove(link_path, ec);
ec = linkit();
break;
}
}
if (ec) {
fprintf(stderr, "cannot link %s to %s: %s\n", target_path.c_str(), link_path.c_str(),
ec.message().c_str());
exit(1);
}
}
private:
std::filesystem::path prefix_;
const char* outfile_ = nullptr;
unsigned int files_ = 0;
void MakeDirs(std::filesystem::path path) {
path.remove_filename();
std::error_code ec;
if (!std::filesystem::create_directories(path, ec) && ec) {
fprintf(stderr, "cannot create directory %s: %s\n", path.c_str(), ec.message().c_str());
exit(1);
}
}
OutputStream CreateFile(const char* outfile) {
auto openit = [outfile]() {
return fbl::unique_fd(open(outfile, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0666));
};
fbl::unique_fd fd = openit();
if (!fd) {
switch (errno) {
case ENOENT: {
MakeDirs(outfile);
fd = openit();
break;
}
case EEXIST:
// Remove the file in case it exists. This makes it safe to do
// e.g. `zbi -o boot.zbi boot.zbi --entry=bin/foo=my/foo` to
// modify a file "in-place" because the input `boot.zbi` will
// already have been opened before the new `boot.zbi` is
// created.
remove(outfile);
fd = openit();
break;
}
}
if (!fd) {
fprintf(stderr, "cannot create %s: %s\n", outfile, strerror(errno));
exit(1);
}
return OutputStream(std::move(fd));
}
};
class NameMatcher final {
public:
NameMatcher(const char* const* patterns, int count) : begin_(patterns), end_(&patterns[count]) {
assert(count >= 0);
assert(!patterns[count]);
}
NameMatcher(char** argv, int argi, int argc) : NameMatcher(&argv[argi], argc - argi) {}
unsigned int names_checked() const { return names_checked_; }
unsigned int names_matched() const { return names_matched_; }
bool MatchesAll(void) const { return begin_ == end_; }
// Not const because it keeps stats.
bool Matches(const char* name, bool casefold = false) {
++names_checked_;
if (MatchesAll() || PatternMatch(name, casefold)) {
++names_matched_;
return true;
} else {
return false;
}
}
void Summary(const char* verbed, const char* items, bool verbose) {
if (!MatchesAll()) {
if (names_checked() == 0) {
fprintf(stderr, "no %s\n", items);
exit(1);
} else if (names_matched() == 0) {
fprintf(stderr, "no matching %s\n", items);
exit(1);
} else if (verbose) {
printf("%s %u of %u %s\n", verbed, names_matched(), names_checked(), items);
}
}
}
private:
const char* const* const begin_ = nullptr;
const char* const* const end_ = nullptr;
unsigned int names_checked_ = 0;
unsigned int names_matched_ = 0;
bool PatternMatch(const char* name, bool casefold) const {
bool excludes = false, included = false;
for (auto next = begin_; next != end_; ++next) {
auto ptn = *next;
if (ptn[0] == '!' || ptn[0] == '^') {
excludes = true;
} else {
included = (included || fnmatch(ptn, name, casefold ? FNM_CASEFOLD : 0) == 0);
}
}
if (included && excludes) {
for (auto next = begin_; next != end_; ++next) {
auto ptn = *next;
if (ptn[0] == '!' || ptn[0] == '^') {
++ptn;
if (fnmatch(ptn, name, casefold ? FNM_CASEFOLD : 0) == 0) {
return false;
}
}
}
}
return included;
}
};
class Checksummer final {
public:
void Write(const iovec& buffer) {
crc_ = crc32(crc_, static_cast<const uint8_t*>(buffer.iov_base), buffer.iov_len);
}
void Write(const std::list<const iovec>& list) {
for (const auto& buffer : list) {
Write(buffer);
}
}
void FinalizeHeader(zbi_header_t* header) {
header->crc32 = 0;
uint32_t header_crc = crc32(0, reinterpret_cast<const uint8_t*>(header), sizeof(*header));
header->crc32 = crc32_combine(header_crc, crc_, header->length);
}
private:
uint32_t crc_ = 0;
};
template <typename Func, typename... Args>
auto Lz4fCall(Func f, Args... args) {
auto result = f(args...);
if (LZ4F_isError(result)) {
fprintf(stderr, "LZ4F failure: %s\n", LZ4F_getErrorName(result));
exit(1);
}
return result;
}
template <typename Func, typename... Args>
auto ZstdCall(const char* what, Func f, Args... args) {
auto result = f(args...);
if (ZSTD_isError(result)) {
fprintf(stderr, "ZSTD %s failure: %s\n", what, ZSTD_getErrorName(result));
exit(1);
}
return result;
}
class Compressor final {
// Private forward declarations;
class Lz4f;
class Zstd;
public:
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Compressor);
enum Algo {
kNone,
kLz4f,
kZstd,
kInvalid,
};
struct Config {
// Default for -c with no argument (or no switches at all).
using Default = Zstd;
Algo algo_ = Default::kAlgo;
int level_ = Default::DefaultLevel();
static constexpr Config None() { return Config{kNone, 0}; }
operator bool() const { return algo_ != kNone; }
void clear() { algo_ = kNone; }
template <typename T>
void Set(int level = T::DefaultLevel()) {
algo_ = T::kAlgo;
level_ = level;
}
template <typename T>
void SetMax() {
Set<T>(T::MaxLevel());
}
bool Parse(const char* arg) {
int level;
if (!arg) {
*this = {};
} else if (!strcasecmp(arg, "none")) {
*this = None();
} else if (!strcasecmp(arg, "lz4f.max")) {
SetMax<Lz4f>();
} else if (!strcasecmp(arg, "lz4f")) {
Set<Lz4f>();
} else if (!strcasecmp(arg, "lz4f.max")) {
SetMax<Lz4f>();
} else if (sscanf(arg, "%*1[lL]%*1[zZ]4%*1[fF].%i", &level) == 1) {
Set<Lz4f>(level);
} else if (!strcasecmp(arg, "zstd")) {
Set<Zstd>();
} else if (!strcasecmp(arg, "zstd.max")) {
SetMax<Zstd>();
} else if (!strcasecmp(arg, "zstd.overclock")) {
Set<Zstd>(Zstd::OverclockLevel());
} else if (sscanf(arg, "%*1[Zz]%*1[Ss]%*1[Tt]%*1[Dd].%i", &level) == 1) {
Set<Zstd>(level);
} else if (!strcasecmp(arg, "max")) {
SetMax<Default>();
} else if (sscanf(arg, "%i", &level) == 1) {
Set<Default>(level);
} else {
return false;
}
return true;
}
};
explicit Compressor(const Config& config) : config_(config) {
switch (config_.algo_) {
case kLz4f:
algo_.emplace<Lz4f>();
break;
case kZstd:
algo_.emplace<Zstd>();
break;
default:
abort();
}
}
void Init(OutputStream* out, const zbi_header_t& header);
void Write(OutputStream* out, const iovec& input);
uint32_t Finish(OutputStream* out);
private:
struct Buffer {
// Move-only type: after moving, data is nullptr and size is 0.
Buffer() = default;
Buffer(std::unique_ptr<std::byte[]> buffer, size_t max_size)
: data(std::move(buffer)), size(max_size) {}
Buffer(Buffer&& other) { *this = std::move(other); }
Buffer& operator=(Buffer&& other) {
data = std::move(other.data);
size = other.size;
other.size = 0;
return *this;
}
std::unique_ptr<std::byte[]> data;
size_t size = 0;
};
auto BufferGetter() {
return [this](size_t x) { return GetBuffer(x); };
}
auto BufferPutter(OutputStream* out) {
return [out, this](auto buffer, size_t size) {
assert(size <= buffer.size);
WriteBuffer(out, std::move(buffer), size);
};
}
class Lz4f {
public:
static constexpr Algo kAlgo = kLz4f;
Lz4f() = default;
// LZ4F compression levels 1-3 are for "fast" compression, and 4-16 are
// for higher compression. The additional compression going from 4 to
// 16 is not worth the extra time needed during compression.
static constexpr int DefaultLevel() { return 4; }
static int MaxLevel() { return LZ4F_compressionLevel_max(); }
~Lz4f() { Lz4fCall(LZ4F_freeCompressionContext, ctx_); }
template <typename T1, typename T2>
void Init(T1 get_buffer, T2 put_buffer, int level, size_t uncompressed_size) {
prefs_.frameInfo.contentSize = uncompressed_size;
prefs_.frameInfo.blockSizeID = LZ4F_max64KB;
prefs_.frameInfo.blockMode = LZ4F_blockIndependent;
prefs_.compressionLevel = level;
Lz4fCall(LZ4F_createCompressionContext, &ctx_, LZ4F_VERSION);
// This might start writing compression format headers before it
// receives any data.
auto buffer = get_buffer(kLz4FMaxHeaderFrameSize);
size_t wrote = Lz4fCall(LZ4F_compressBegin, ctx_, buffer.data.get(), buffer.size, &prefs_);
put_buffer(std::move(buffer), wrote);
}
template <typename T1, typename T2>
void Update(T1 get_buffer, T2 put_buffer, const iovec& input) {
auto buffer = get_buffer(LZ4F_compressBound(input.iov_len, &prefs_));
size_t wrote = Lz4fCall(LZ4F_compressUpdate, ctx_, buffer.data.get(), buffer.size,
input.iov_base, input.iov_len, &kCompressOpt);
put_buffer(std::move(buffer), wrote);
}
template <typename T1, typename T2>
void Finish(T1 get_buffer, T2 put_buffer) {
auto buffer = get_buffer(LZ4F_compressBound(0, &prefs_));
size_t wrote =
Lz4fCall(LZ4F_compressEnd, ctx_, buffer.data.get(), buffer.size, &kCompressOpt);
put_buffer(std::move(buffer), wrote);
}
private:
LZ4F_compressionContext_t ctx_{};
LZ4F_preferences_t prefs_{};
// It's not clear where this magic number comes from.
static constexpr size_t kLz4FMaxHeaderFrameSize = 128;
};
class Zstd {
public:
static constexpr Algo kAlgo = kZstd;
// Quite good compression, quite fast. Compression gets better up to
// 10 or so, but slower. Level 19 is quite slow but best compression,
// substantially better than level 5 or 10.
static constexpr int DefaultLevel() { return 4; }
// "The library supports regular compression levels from 1 up to
// ZSTD_maxCLevel()."
static int OverclockLevel() { return ZSTD_maxCLevel(); }
// "Levels >= 20, labeled `--ultra`, should be used with caution, as
// they require more memory." So sayeth <zstd/zstd.h>.
static constexpr int MaxLevel() { return 19; }
Zstd() = default;
~Zstd() { ZstdCall("free", ZSTD_freeCCtx, ctx_); }
template <typename T1, typename T2>
void Init(T1 get_buffer, T2 put_buffer, int level, size_t uncompressed_size) {
ctx_ = ZSTD_createCCtx();
if (!ctx_) {
fprintf(stderr, "out of memory\n");
exit(1);
}
ZstdCall("nbWorkers", ZSTD_CCtx_setParameter, ctx_, ZSTD_c_nbWorkers,
std::thread::hardware_concurrency());
ZstdCall("compressionLevel", ZSTD_CCtx_setParameter, ctx_, ZSTD_c_compressionLevel, level);
if (level >= DefaultLevel()) {
ZstdCall("enableLongDistanceMatching", ZSTD_CCtx_setParameter, ctx_,
ZSTD_c_enableLongDistanceMatching, 1);
}
ZstdCall("PledgedSrcSize", ZSTD_CCtx_setPledgedSrcSize, ctx_, uncompressed_size);
}
template <typename T1, typename T2>
void Update(T1 get_buffer, T2 put_buffer, const iovec& input) {
auto buffer = get_buffer(ZSTD_compressBound(input.iov_len));
ZSTD_outBuffer out = {
buffer.data.get(),
buffer.size,
0,
};
ZSTD_inBuffer in = {
input.iov_base,
input.iov_len,
0,
};
do {
ZstdCall("compress", ZSTD_compressStream2, ctx_, &out, &in, ZSTD_e_continue);
} while (in.pos < in.size);
put_buffer(std::move(buffer), out.pos);
}
template <typename T1, typename T2>
void Finish(T1 get_buffer, T2 put_buffer) {
size_t left;
do {
auto buffer = get_buffer(ZSTD_CStreamOutSize());
ZSTD_outBuffer out = {
buffer.data.get(),
buffer.size,
0,
};
ZSTD_inBuffer in = {};
left = ZstdCall("finish", ZSTD_compressStream2, ctx_, &out, &in, ZSTD_e_end);
put_buffer(std::move(buffer), out.pos);
} while (left > 0);
}
private:
ZSTD_CCtx* ctx_ = nullptr;
};
using AlgoData = std::variant<Lz4f, Zstd>;
Config config_;
AlgoData algo_{};
Buffer unused_buffer_;
zbi_header_t header_;
Checksummer crc_;
uint32_t header_pos_ = 0;
// IOV_MAX buffers might be live at once.
static constexpr const size_t kMinBufferSize = (128 << 20) / IOV_MAX;
// This tells LZ4f_compressUpdate it can keep a pointer to data.
static constexpr const LZ4F_compressOptions_t kCompressOpt = {1, {}};
Buffer GetBuffer(size_t max_size) {
if (unused_buffer_.size >= max_size) {
// We have an old buffer that will do fine.
return std::move(unused_buffer_);
} else {
// Get a new buffer.
max_size = std::max(max_size, kMinBufferSize);
return {std::make_unique<std::byte[]>(max_size), max_size};
}
}
void WriteBuffer(OutputStream* out, Buffer buffer, size_t actual_size) {
if (actual_size > 0) {
header_.length += actual_size;
const iovec iov{buffer.data.get(), actual_size};
crc_.Write(iov);
out->Write(iov, std::move(buffer.data));
buffer.size = 0;
} else {
// The compressor often delivers zero bytes for an input chunk.
// Stash the unused buffer for next time to cut down on new/delete.
unused_buffer_ = std::move(buffer);
}
}
};
void Compressor::Init(OutputStream* out, const zbi_header_t& header) {
header_ = header;
assert(header_.flags & ZBI_FLAG_STORAGE_COMPRESSED);
assert(header_.flags & ZBI_FLAG_CRC32);
// Write a place-holder for the header, which we will go back
// and fill in once we know the payload length and CRC.
header_pos_ = out->PlaceHeader();
// Record the original uncompressed size in header_.extra.
// WriteBuffer will accumulate the compressed size in header_.length.
header_.extra = header_.length;
header_.length = 0;
std::visit(
[&](auto&& v) { v.Init(BufferGetter(), BufferPutter(out), config_.level_, header_.extra); },
algo_);
}
// NOTE: Input buffer may be referenced for the life of the Compressor!
void Compressor::Write(OutputStream* out, const iovec& input) {
std::visit([&](auto&& v) { v.Update(BufferGetter(), BufferPutter(out), input); }, algo_);
}
uint32_t Compressor::Finish(OutputStream* out) {
// Write the closing chunk from the compressor.
std::visit([&](auto&& v) { v.Finish(BufferGetter(), BufferPutter(out)); }, algo_);
// Complete the checksum.
crc_.FinalizeHeader(&header_);
// Write the header back where its place was held.
out->PatchHeader(header_, header_pos_);
return header_.length;
}
struct Decompressor {
using Function = std::unique_ptr<std::byte[]>(const std::list<const iovec>& payload,
uint32_t decompressed_length);
Function* decompress;
uint32_t magic;
};
Decompressor::Function DecompressLz4f, DecompressZstd;
constexpr Decompressor kDecompressors[] = {
{DecompressLz4f, 0x184D2204},
{DecompressZstd, 0xFD2FB528},
};
std::unique_ptr<std::byte[]> Decompress(const std::list<const iovec>& payload,
uint32_t decompressed_length) {
if (payload.empty() || payload.front().iov_len < sizeof(uint32_t)) {
fprintf(stderr, "compressed payload too small for header\n");
exit(1);
}
const uint32_t magic = *static_cast<const uint32_t*>(payload.front().iov_base);
for (const auto d : kDecompressors) {
if (d.magic == magic) {
return d.decompress(payload, decompressed_length);
}
}
fprintf(stderr, "compressed payload magic number %#x not recognized\n", magic);
exit(1);
}
std::unique_ptr<std::byte[]> DecompressLz4f(const std::list<const iovec>& payload,
uint32_t decompressed_length) {
auto buffer = std::make_unique<std::byte[]>(decompressed_length);
LZ4F_decompressionContext_t ctx;
Lz4fCall(LZ4F_createDecompressionContext, &ctx, LZ4F_VERSION);
auto cleanup = fbl::MakeAutoCall([&]() { Lz4fCall(LZ4F_freeDecompressionContext, ctx); });
std::byte* dst = buffer.get();
size_t dst_size = decompressed_length;
for (const auto& iov : payload) {
auto src = static_cast<const std::byte*>(iov.iov_base);
size_t src_size = iov.iov_len;
do {
if (dst_size == 0) {
fprintf(stderr, "decompression produced too much data\n");
exit(1);
}
size_t nwritten = dst_size, nread = src_size;
static constexpr const LZ4F_decompressOptions_t kDecompressOpt{};
Lz4fCall(LZ4F_decompress, ctx, dst, &nwritten, src, &nread, &kDecompressOpt);
assert(nread <= src_size);
src += nread;
src_size -= nread;
assert(nwritten <= dst_size);
dst += nwritten;
dst_size -= nwritten;
} while (src_size > 0);
}
if (dst_size > 0) {
fprintf(stderr, "decompression produced too little data by %zu bytes\n", dst_size);
exit(1);
}
return buffer;
}
std::unique_ptr<std::byte[]> DecompressZstd(const std::list<const iovec>& payload,
uint32_t decompressed_length) {
auto buffer = std::make_unique<std::byte[]>(decompressed_length);
auto stream = ZSTD_createDStream();
if (!stream) {
fprintf(stderr, "out of memory\n");
exit(1);
}
auto cleanup = fbl::MakeAutoCall([&]() { ZstdCall("free", ZSTD_freeDStream, stream); });
ZstdCall("init", ZSTD_initDStream, stream);
ZSTD_outBuffer out = {
buffer.get(),
decompressed_length,
0,
};
for (const auto& iov : payload) {
ZSTD_inBuffer in = {
iov.iov_base,
iov.iov_len,
0,
};
while (in.pos < in.size) {
if (out.pos == out.size) {
fprintf(stderr, "decompression produced too much data\n");
exit(1);
}
ZstdCall("decompress", ZSTD_decompressStream, stream, &out, &in);
}
}
if (out.pos < out.size) {
fprintf(stderr, "decompression produced too little data by %zu bytes\n", out.size - out.pos);
exit(1);
}
return buffer;
}
template <typename T>
std::size_t HashValue(const T& x) {
return std::hash<std::decay_t<T>>()(x);
}
class FileContents final {
public:
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FileContents);
FileContents() = default;
// Get unowned file contents from a BOOTFS image.
// The entry has been validated against the payload size.
FileContents(const zbi_bootfs_dirent_t& entry, const std::byte* bootfs_payload)
: mapped_(const_cast<void*>(static_cast<const void*>(bootfs_payload + entry.data_off))),
mapped_size_(ZBI_BOOTFS_PAGE_ALIGN(entry.data_len)),
exact_size_(entry.data_len),
owned_(false) {}
// Get unowned file contents from a string.
// This object won't support PageRoundedView.
FileContents(const char* buffer, bool null_terminate)
: mapped_(const_cast<char*>(buffer)),
mapped_size_(strlen(buffer) + 1),
exact_size_(mapped_size_ - (null_terminate ? 0 : 1)),
owned_(false) {}
FileContents(FileContents&& other) { *this = std::move(other); }
FileContents& operator=(FileContents&& other) {
std::swap(mapped_, other.mapped_);
std::swap(mapped_size_, other.mapped_size_);
std::swap(exact_size_, other.exact_size_);
std::swap(owned_, other.owned_);
return *this;
}
~FileContents() {
if (owned_ && mapped_) {
munmap(mapped_, mapped_size_);
}
}
size_t exact_size() const { return exact_size_; }
size_t mapped_size() const { return mapped_size_; }
// Equality means pointer equality, so two separately-reached
// slices of the same piece of an input BOOTFS, for example.
bool operator==(const FileContents& other) const {
return (exact_size() == other.exact_size() && mapped_ == other.mapped_);
}
bool operator!=(const FileContents& other) const { return !(*this == other); }
struct Hash {
std::size_t operator()(const FileContents& file) const { return HashValue(file.mapped_); }
};
static FileContents Map(const fbl::unique_fd& fd, const struct stat& st, const char* filename) {
// st_size is off_t, everything else is size_t.
const size_t size = st.st_size;
static_assert(
std::numeric_limits<decltype(st.st_size)>::max() <= std::numeric_limits<size_t>::max(),
"size_t < off_t?");
static size_t pagesize = []() -> size_t {
size_t pagesize = sysconf(_SC_PAGE_SIZE);
assert(pagesize >= ZBI_BOOTFS_PAGE_SIZE);
assert(pagesize % ZBI_BOOTFS_PAGE_SIZE == 0);
return pagesize;
}();
if (size == 0) {
return {};
}
void* map = mmap(nullptr, size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd.get(), 0);
if (map == MAP_FAILED) {
fprintf(stderr, "mmap: %s: %s\n", filename, strerror(errno));
exit(1);
}
assert(map);
FileContents result;
result.mapped_ = map;
result.exact_size_ = size;
result.mapped_size_ = (size + pagesize - 1) & -pagesize;
return result;
}
const iovec View() const { return View(0, exact_size()); }
const iovec View(size_t offset, size_t length) const {
assert(offset <= exact_size_);
assert(exact_size_ - offset >= length);
return Iovec(static_cast<const std::byte*>(mapped_) + offset, length);
}
const iovec PageRoundedView(size_t offset, size_t length) const {
assert(offset <= mapped_size_);
assert(mapped_size_ - offset >= length);
return Iovec(static_cast<const std::byte*>(mapped_) + offset, length);
}
private:
void* mapped_ = nullptr;
size_t mapped_size_ = 0;
size_t exact_size_ = 0;
bool owned_ = true;
};
// File represents one node in the BOOTFS directory graph. It holds either a
// FileContents (for a file) or a Directory (for a directory).
class File;
// Directory represents a subdirectory in the BOOTFS directory graph.
// It maps names (with no slashes) to File nodes.
using Directory = std::map<std::string, const File*>;
class File final {
public:
File() = default;
explicit File(std::unique_ptr<const FileContents> file) : file_(std::move(file)) {}
explicit File(std::unique_ptr<Directory> dir) : dir_(std::move(dir)) {}
operator bool() const { return dir_ || file_; }
bool operator==(const File& other) const {
assert(!dir_);
assert(!other.dir_);
return *file_ == *other.file_;
}
bool operator!=(const File& other) const { return !(*this == other); }
struct Hash {
std::size_t operator()(const File& file) const {
assert(!file.dir_);
return FileContents::Hash()(*file.file_);
}
};
bool IsDir() const { return bool(dir_); }
auto AsDir() const { return dir_.get(); }
auto AsContents() const { return file_.get(); }
private:
std::unique_ptr<const FileContents> file_;
std::unique_ptr<Directory> dir_;
};
// Treat a Directory tree like a list of leaves.
class DirectoryTree final {
public:
DirectoryTree(Directory* root) : root_(root) {}
class const_iterator {
public:
using value_type = std::pair<std::filesystem::path, const File*>;
const_iterator() = default;
bool operator==(const const_iterator& other) const {
if (other.pos_.empty()) {
return pos_.empty();
}
return !pos_.empty() && pos_.front().pos == other.pos_.front().pos;
}
bool operator!=(const const_iterator& other) const { return !(*this == other); }
value_type operator*() const {
return {
std::accumulate(pos_.crbegin(), pos_.crend(), std::filesystem::path(),
[](const auto& acc, const auto& elt) { return acc / elt.pos->first; }),
pos_.front().pos->second};
}
const_iterator& operator++() {
++pos_.front().pos;
AfterAdvance();
return *this;
}
// Remove the current entry from its directory and advance past it.
void Remove() {
pos_.front().pos = pos_.front().dir->erase(pos_.front().pos);
AfterAdvance();
}
private:
struct DirectoryPosition {
Directory* dir = nullptr;
Directory::iterator pos;
};
std::list<DirectoryPosition> pos_;
// Only DirectoryTree can use the non-default constructor.
friend DirectoryTree;
explicit const_iterator(Directory* dir) {
Descend(dir);
AfterAdvance();
}
void Descend(Directory* dir) { pos_.push_front({dir, dir->begin()}); }
// If the current entry is a directory, go down a level.
// The iterator never yields a directory, only a leaf file.
bool DescendIfDirectory() {
const File* current_entry = pos_.front().pos->second;
if (current_entry->IsDir()) {
Descend(current_entry->AsDir());
return true;
}
return false;
}
void AfterAdvance() {
do {
// While the current position is at the end of its directory,
// go up a level and advance.
while (pos_.front().pos == pos_.front().dir->end()) {
pos_.pop_front();
if (pos_.empty()) {
return;
}
++pos_.front().pos;
}
// Descend and iterate if now at a directory.
} while (DescendIfDirectory());
}
};
using iterator = const_iterator;
const_iterator begin() const { return const_iterator(root_); }
const_iterator end() const { return {}; }
private:
Directory* root_;
};
struct PathHash final {
std::size_t operator()(const std::filesystem::path& file) const {
return HashValue(file.native());
}
};
// This is used for all opening of files and directories for input.
// It tracks all files opened so a depfile can be written at the end.
//
// The opener caches FileContents objects representing every file mapped
// in. These objects live in the cache for the lifetime of the opener.
// Opener methods return const FileContents* raw pointers to indicate
// they are never owned by the caller.
//
// The opener caches on multiple levels:
// * input file names are cached so reuse doesn't hit the filesystem at all
// * opened files' contents are cached by file identity so multiple
// input file names reaching the same actual file (via different
// unnormalized paths or links) reuse the same mapped contents
// * directories read are cached fully
// * TODO(mcgrathr): identical contents from disparate sources
class FileOpener final {
public:
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FileOpener);
FileOpener() = default;
template <typename T>
void ChangeDirectory(const T& dir) {
cwd_ /= dir;
}
// The returned FileContents is cached and lives forever (for the lifetime
// of the FileOpener).
const FileContents* OpenFile(std::filesystem::path file) {
file.make_preferred();
auto& cache = name_cache_[file];
if (!cache) {
struct stat st;
auto [cached_file, fd] = Open(file, &st);
OpenFile(cached_file, std::move(fd), st, file);
cache = cached_file;
}
return cache->AsContents();
}
// Like OpenFile, but also accept a directory.
const File* OpenFileOrDir(std::filesystem::path file, bool ignore_missing = false) {
file.make_preferred();
auto& cache = name_cache_[file];
if (!cache) {
struct stat st;
auto [cached_file, fd] = Open(file, &st, ignore_missing);
if (!cached_file) {
assert(ignore_missing);
return nullptr;
}
if (S_ISDIR(st.st_mode)) {
OpenDirectory(cached_file, std::move(fd), std::move(file));
} else {
OpenFile(cached_file, std::move(fd), st, std::move(file));
}
cache = cached_file;
}
return cache;
}
// Construct a new "unowned" FileContents in place. The returned
// pointer lives for the lifetime of the FileOpener. Hence, the
// true owner of the data this FileContents points to must be kept
// alive for the lifetime of the FileOpener.
template <typename... Args>
const File* Emplace(Args&&... args) {
auto [it, fresh] =
memory_cache_.emplace(std::make_unique<FileContents>(std::forward<Args>(args)...));
return &*it;
}
void WriteDepfile(const char* output_file, const char* depfile) {
if (depfile) {
auto f = fopen(depfile, "w");
if (!f) {
perror(depfile);
exit(1);
}
fprintf(f, "%s:", output_file);
for (const auto& [file, _] : name_cache_) {
fprintf(f, " %s", file.c_str());
}
putc('\n', f);
fclose(f);
}
}
private:
class FileId final {
public:
explicit FileId(const struct stat& st) : dev_(st.st_dev), ino_(st.st_ino) {}
bool operator==(const FileId& other) const { return dev_ == other.dev_ && ino_ == other.ino_; }
bool operator!=(const FileId& other) const { return !(*this == other); }
bool operator<(const FileId& other) const {
return (dev_ < other.dev_ || (dev_ == other.dev_ && ino_ < other.ino_));
}
private:
decltype((struct stat){}.st_dev) dev_;
decltype((struct stat){}.st_ino) ino_;
};
// Cache of contents by file identity. The cache owns the File
// objects, so they all live forever and raw const File* pointers
// are used to access them.
std::map<FileId, File> file_cache_;
// Cache of contents by file name. These point into the file_cache_.
std::unordered_map<std::filesystem::path, const File*, PathHash> name_cache_;
// These are created by Emplace() and kept here both to de-duplicate them
// and to tie their lifetimes to the FileOpener (to parallel file_cache_).
// De-duplication here only actually occurs for files extracted from an
// input BOOTFS in Item::ReadBootFS in case the input filesystem used
// "hard links" (i.e. multiple directory entries pointing to the same
// region of the image).
std::unordered_set<File, File::Hash> memory_cache_;
// State of -C switches.
std::filesystem::path cwd_{"."};
std::pair<File*, fbl::unique_fd> Open(const std::filesystem::path& file, struct stat* st,
bool ignore_missing = false) {
auto path = cwd_ / file;
fbl::unique_fd fd(open(path.c_str(), O_RDONLY));
if (!fd) {
if (errno == ENOENT && ignore_missing) {
return {};
}
perror(file.c_str());
exit(1);
}
if (fstat(fd.get(), st) < 0) {
perror("fstat");
exit(1);
}
return {&file_cache_[FileId(*st)], std::move(fd)};
}
void OpenFile(File* cached, fbl::unique_fd fd, const struct stat& st,
std::filesystem::path file) {
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "%s: not a regular file\n", file.c_str());
exit(1);
}
*cached = File(
std::make_unique<const FileContents>(FileContents::Map(std::move(fd), st, file.c_str())));
}
void OpenDirectory(File* cached, fbl::unique_fd fd, std::filesystem::path file) {
DIR* dir = fdopendir(fd.release());
if (!dir) {
perror("fdopendir");
exit(1);
}
auto dirmap = std::make_unique<Directory>();
const dirent* d;
while ((d = readdir(dir)) != nullptr) {
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) {
continue;
}
file /= d->d_name;
(*dirmap)[d->d_name] = OpenFileOrDir(file);
file.remove_filename();
}
closedir(dir);
*cached = File(std::move(dirmap));
}
};
class Item final {
public:
// Only the static methods below can create an Item.
Item() = delete;
DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Item);
static const char* TypeName(uint32_t zbi_type) { return ItemTypeInfo(zbi_type).name; }
static const char* TypeExtension(uint32_t zbi_type) { return ItemTypeInfo(zbi_type).extension; }
static bool ParseTypeName(const char* name, uint32_t* abi_type) {
for (const auto& t : kItemTypes_) {
if (!strcasecmp(t.name, name)) {
*abi_type = t.type;
return true;
}
}
int i = 0;
return sscanf(name, "%x%n", abi_type, &i) == 1 && name[i] == '\0';
}
static std::filesystem::path ExtractedFileName(unsigned int n, uint32_t zbi_type, bool raw) {
std::filesystem::path path;
char buf[32];
const auto info = ItemTypeInfo(zbi_type);
if (info.name) {
snprintf(buf, sizeof(buf), "%03u.", n);
std::string name(buf);
name += info.name;
for (auto& c : name) {
c = static_cast<unsigned char>(std::tolower(c));
}
path = std::move(name);
} else {
snprintf(buf, sizeof(buf), "%03u.%08x", n, zbi_type);
path = buf;
}
return path.replace_extension((raw && info.extension) ? info.extension : ".zbi");
}
static void PrintTypeUsage(FILE* out) {
fprintf(out,
"\
TYPE can be hexadecimal or a name string (case-insensitive).\n\
Extracted items use the file names shown below:\n\
--type --extract-item --extract-raw\n\
");
for (const auto& t : kItemTypes_) {
const auto zbi_name = ExtractedFileName(1, t.type, false);
const auto raw_name = ExtractedFileName(1, t.type, true);
fprintf(out, " %-20s %-26s %s\n", t.name, zbi_name.c_str(), raw_name.c_str());
}
}
uint32_t type() const { return header_.type; }
uint32_t PayloadSize() const { return header_.length; }
uint32_t TotalSize() const { return sizeof(header_) + ZBI_ALIGN(PayloadSize()); }
zbi_header_t CheckHeader() const {
if (header_.flags & ZBI_FLAG_CRC32) {
Checksummer crc;
crc.Write(payload_);
zbi_header_t check_header = header_;
crc.FinalizeHeader(&check_header);
if (!compress_ && check_header.crc32 != header_.crc32) {
fprintf(stderr, "error: CRC %08x does not match header\n", check_header.crc32);
}
return check_header;
} else {
return header_;
}
}
void Describe(uint32_t pos) const {
zbi_header_t header = CheckHeader();
const char* type_name = TypeName(type());
if (!type_name) {
printf("%08x: %08x UNKNOWN (type=%08x)\n", pos, header.length, header.type);
} else if (zbitl::TypeIsStorage(type())) {
printf("%08x: %08x %s (size=%08x)\n", pos, header.length, type_name, header.extra);
} else {
printf("%08x: %08x %s\n", pos, header.length, type_name);
}
if (header.flags & ZBI_FLAG_CRC32) {
printf(" : MAGIC=%08x CRC=%08x\n", header.magic, header.crc32);
} else {
printf(" : MAGIC=%08x NO CRC\n", header.magic);
}
}
bool AlreadyCompressed() const {
return (header_.flags & ZBI_FLAG_STORAGE_COMPRESSED) && !compress_;
}
int Show() {
if (header_.length > 0) {
if (AlreadyCompressed()) {
return CreateFromCompressed(*this)->Show();
}
switch (header_.type) {
case ZBI_TYPE_STORAGE_BOOTFS:
return ShowBootFS();
case ZBI_TYPE_CMDLINE:
return ShowCmdline();
}
}
return 0;
}
void EmitJsonContents(rapidjson::PrettyWriter<rapidjson::FileWriteStream>& writer,
const char* key) {
if (AlreadyCompressed()) {
CreateFromCompressed(*this)->EmitJsonContents(writer, key);
} else {
if (header_.type == ZBI_TYPE_STORAGE_BOOTFS) {
writer.Key(key);
EmitJsonBootFS(writer);
} else if (auto ext = TypeExtension(header_.type); ext != nullptr && !strcmp(ext, ".txt")) {
writer.Key(key);
EmitJsonCmdline(writer);
}
}
}
void EmitJson(rapidjson::PrettyWriter<rapidjson::FileWriteStream>& writer) {
zbitl::JsonWriteItemWithContents(
writer,
[](auto&& writer, auto&& key, auto&& header, auto&& payload) {
payload->EmitJsonContents(writer, key);
},
CheckHeader(), this);
}
// Streaming exhausts the item's payload. The OutputStream will now
// have pointers into buffers owned by this Item, so this Item must be
// kept alive until out->Flush() runs (while *this is alive, to be safe).
void Stream(OutputStream* out) {
assert(Aligned(out->WritePosition()));
uint32_t wrote = compress_ ? StreamCompressed(out) : StreamRaw(out);
assert(out->WritePosition() % ZBI_ALIGNMENT == wrote % ZBI_ALIGNMENT);
uint32_t aligned = ZBI_ALIGN(wrote);
if (aligned > wrote) {
static const std::byte padding[ZBI_ALIGNMENT]{};
out->Write(Iovec(padding, aligned - wrote));
}
assert(Aligned(out->WritePosition()));
}
// The buffer will be released when this Item is destroyed. This item
// and items earlier on the list can hold pointers into the buffer.
void OwnBuffer(std::unique_ptr<std::byte[]> buffer) { buffers_.push_front(std::move(buffer)); }
// Consume another Item while keeping its owned buffers and files alive.
void TakeOwned(ItemPtr other) {
if (other) {
buffers_.splice_after(buffers_.before_begin(), other->buffers_);
}
}
// Create from in-core data.
static ItemPtr CreateFromBuffer(uint32_t type, std::unique_ptr<std::byte[]> payload,
uint32_t size) {
auto item = MakeItem(NewHeader(type, size));
item->payload_.emplace_front(Iovec(payload.get(), size));
item->OwnBuffer(std::move(payload));
Checksummer crc;
crc.Write(item->payload_);
crc.FinalizeHeader(&item->header_);
return item;
}
// Create from local scratch data.
template <typename T>
static ItemPtr Create(uint32_t type, const T& payload) {
auto buffer = std::make_unique<std::byte[]>(sizeof(payload));
memcpy(buffer.get(), &payload, sizeof(payload));
return CreateFromBuffer(type, std::move(buffer), sizeof(payload));
}
// Create from raw file contents.
static ItemPtr CreateFromFile(const File* filenode, uint32_t type, Compressor::Config compress) {
bool null_terminate = type == ZBI_TYPE_CMDLINE;
if (!zbitl::TypeIsStorage(type)) {
compress.clear();
}
const auto file = filenode->AsContents();
size_t size = file->exact_size() + (null_terminate ? 1 : 0);
if (size > UINT32_MAX) {
fprintf(stderr, "input file too large\n");
exit(1);
}
auto item = MakeItem(NewHeader(type, static_cast<uint32_t>(size)), compress);
// If we need some zeros, see if they're already right there
// in the last mapped page past the exact end of the file.
if (size <= file->mapped_size()) {
// Use the padding that's already there.
item->payload_.emplace_front(file->PageRoundedView(0, size));
} else {
// No space, so we need a separate padding buffer.
if (null_terminate) {
item->payload_.emplace_front(Iovec("", 1));
}
item->payload_.emplace_front(file->View());
}
if (!compress) {
// Compute the checksum now so the item is ready to write out.
Checksummer crc;
crc.Write(file->View());
if (null_terminate) {
crc.Write(Iovec("", 1));
}
crc.FinalizeHeader(&item->header_);
}
return item;
}
// Create from an existing fully-baked item in an input file.
static ItemPtr CreateFromItem(const FileContents* file, uint32_t offset) {
if (offset > file->exact_size() || file->exact_size() - offset < sizeof(zbi_header_t)) {
fprintf(stderr, "input file too short for next header\n");
exit(1);
}
const zbi_header_t* header =
static_cast<const zbi_header_t*>(file->View(offset, sizeof(zbi_header_t)).iov_base);
offset += sizeof(zbi_header_t);
if (file->exact_size() - offset < header->length) {
fprintf(stderr, "input file too short for payload of %u bytes\n", header->length);
exit(1);
}
auto item = MakeItem(*header);
item->payload_.emplace_front(file->View(offset, header->length));
return item;
}
// Create by decompressing a fully-baked item that is compressed.
static ItemPtr CreateFromCompressed(const Item& compressed,
Compressor::Config compress = Compressor::Config::None()) {
assert(compressed.AlreadyCompressed());
auto item = MakeItem(compressed.header_, compress);
item->header_.flags &= ~ZBI_FLAG_STORAGE_COMPRESSED;
item->header_.length = item->header_.extra;
auto buffer = Decompress(compressed.payload_, item->header_.length);
item->payload_.emplace_front(Iovec(buffer.get(), item->header_.length));
item->OwnBuffer(std::move(buffer));
if (compress) {
// This item will be compressed afresh on output.
item->header_.flags |= ZBI_FLAG_STORAGE_COMPRESSED;
}
return item;
}
// Same, but consumes the compressed item while keeping its
// owned buffers alive in the new uncompressed item.
static ItemPtr CreateFromCompressed(ItemPtr compressed,
Compressor::Config compress = Compressor::Config::None()) {
auto uncompressed = CreateFromCompressed(*compressed, compress);
uncompressed->TakeOwned(std::move(compressed));
return uncompressed;
}
// Create a BOOTFS item.
static ItemPtr CreateBootFS(Directory* root, Compressor::Config compress) {
auto item = MakeItem(NewHeader(ZBI_TYPE_STORAGE_BOOTFS, 0), compress);
// Collect the names and contents, calculating the final directory size.
std::vector<std::pair<std::string, const FileContents*>> entries;
std::unordered_map<const FileContents*, uint32_t> files;
size_t dirsize = 0;
for (const auto& [path, file] : DirectoryTree{root}) {
auto name = path.generic_string();
const auto contents = file->AsContents();
// Accumulate the space needed for each zbi_bootfs_dirent_t.
dirsize += ZBI_BOOTFS_DIRENT_SIZE(name.size() + 1);
entries.emplace_back(std::move(name), contents);
files.emplace(contents, 0);
}
// Now fill a buffer with the BOOTFS header and directory entries,
// appending each unique file to the payload.
const zbi_bootfs_header_t header = {
ZBI_BOOTFS_MAGIC, // magic
static_cast<uint32_t>(dirsize), // dirsize
0, // reserved0
0, // reserved1
};
size_t header_size = ZBI_BOOTFS_PAGE_ALIGN(sizeof(header) + dirsize);
AppendBuffer buffer(header_size);
buffer.Append(&header);
uint32_t data_off = static_cast<uint32_t>(header_size);
for (const auto& [name, contents] : entries) {
// Place the file contents if this is the first name for them.
uint32_t* location = &files[contents];
if (*location == 0) {
size_t layout_size =
((contents->exact_size() + ZBI_BOOTFS_PAGE_SIZE - 1) & -size_t{ZBI_BOOTFS_PAGE_SIZE});
if (layout_size > std::numeric_limits<uint32_t>::max()) {
fprintf(stderr, "input file size exceeds format maximum\n");
exit(1);
}
if (data_off + layout_size > std::numeric_limits<uint32_t>::max()) {
fprintf(stderr, "BOOTFS image size exceeds format maximum\n");
exit(1);
}
*location = data_off;
data_off += layout_size;
item->payload_.emplace_back(contents->PageRoundedView(0, layout_size));
}
// Emit the directory entry.
const zbi_bootfs_dirent_t entry_hdr = {
static_cast<uint32_t>(name.size() + 1), // name_len
static_cast<uint32_t>(contents->exact_size()), // data_len
*location, // data_off
};
buffer.Append(&entry_hdr);
buffer.Append(name.c_str(), entry_hdr.name_len);
buffer.Pad(ZBI_BOOTFS_DIRENT_SIZE(entry_hdr.name_len) -
offsetof(zbi_bootfs_dirent_t, name[entry_hdr.name_len]));
}
// Zero fill to the end of the page.
buffer.Pad(header_size - buffer.size());
// Only now do we know the total size of the image.
item->header_.length = data_off;
if (!compress) {
// Checksum the BOOTFS image right now: header and then payload.
Checksummer crc;
crc.Write(buffer.get());
crc.Write(item->payload_);
crc.FinalizeHeader(&item->header_);
}
// Put the header at the front of the payload.
item->payload_.emplace_front(buffer.get());
item->OwnBuffer(buffer.release());
return item;
}
// Returns [iterator, owner] where `owner` must be kept alive as long as
// any of the FileContents generated by the iterator is alive.
static auto ReadBootFS(ItemPtr item) {
if (item->AlreadyCompressed()) {
item = CreateFromCompressed(std::move(item));
}
BootFSDirectoryIterator it;
int status = BootFSDirectoryIterator::Create(item.get(), &it);
if (status) {
exit(status);
}
return std::make_pair(std::move(it), std::move(item));
}
void ExtractItem(FileWriter* writer, NameMatcher* matcher) {
auto path = ExtractedFileName(writer->NextFileNumber(), type(), false);
auto name = path.c_str();
if (matcher->Matches(name, true)) {
WriteZBI(writer, name, (Item* const[]){this});
}
}
void ExtractRaw(FileWriter* writer, NameMatcher* matcher) {
auto path = ExtractedFileName(writer->NextFileNumber(), type(), true);
auto name = path.c_str();
if (matcher->Matches(name, true)) {
if (type() == ZBI_TYPE_CMDLINE) {
// Drop a trailing NUL.
iovec iov = payload_.back();
auto str = static_cast<const char*>(iov.iov_base);
if (str[iov.iov_len - 1] == '\0') {
payload_.pop_back();
--iov.iov_len;
payload_.push_back(iov);
}
}
if (AlreadyCompressed()) {
auto uncompressed = CreateFromCompressed(*this);
// The uncompressed item must outlive the OutputStream.
auto out = writer->RawFile(name);
uncompressed->StreamRawPayload(&out);
} else {
auto out = writer->RawFile(name);
StreamRawPayload(&out);
}
}
}
template <typename ItemList>
static void WriteZBI(FileWriter* writer, const char* name, const ItemList& items) {
auto out = writer->RawFile(name);
uint32_t header_start = out.PlaceHeader();
uint32_t payload_start = out.WritePosition();
assert(Aligned(payload_start));
for (const auto& item : items) {
// The OutputStream stores pointers into Item buffers in its write
// queue until it goes out of scope below. The ItemList keeps all
// the items alive past then.
item->Stream(&out);
}
const zbi_header_t header = ZBI_CONTAINER_HEADER(out.WritePosition() - payload_start);
assert(Aligned(header.length));
out.PatchHeader(header, header_start);
}
void AppendPayload(std::string* buffer) const {
if (AlreadyCompressed()) {
CreateFromCompressed(*this)->AppendPayload(buffer);
} else {
for (const auto& iov : payload_) {
buffer->append(static_cast<const char*>(iov.iov_base), iov.iov_len);
}
}
}
static ItemPtr Recompress(ItemPtr item, Compressor::Config how) {
if (zbitl::TypeIsStorage(item->type())) {
if (item->AlreadyCompressed()) {
item = CreateFromCompressed(std::move(item), how);
} else if (how) {
auto old = std::move(item);
item = MakeItem(old->header_, how);
std::swap(old->payload_, item->payload_);
std::swap(old->buffers_, item->buffers_);
}
}
return item;
}
private:
zbi_header_t header_;
std::list<const iovec> payload_;
// The payload_ items might point into these buffers. They're just
// stored here to own the buffers until the payload is exhausted.
std::forward_list<std::unique_ptr<std::byte[]>> buffers_;
const Compressor::Config compress_;
struct ItemTypeInfo {
uint32_t type;
const char* name;
const char* extension;
};
static constexpr const ItemTypeInfo kItemTypes_[] = {
#define kITemTypes_Element(type, name, extension) {type, name, extension},
ZBI_ALL_TYPES(kITemTypes_Element)
#undef kitemtypes_element
};
static constexpr ItemTypeInfo ItemTypeInfo(uint32_t zbi_type) {
for (const auto& t : kItemTypes_) {
if (t.type == zbi_type) {
return t;
}
}
return {};
}
static constexpr zbi_header_t NewHeader(uint32_t type, uint32_t size) {
return {
type, // type
size, // length
0, // extra
ZBI_FLAG_VERSION | ZBI_FLAG_CRC32, // flags
0, // reserved0
0, // reserved1
ZBI_ITEM_MAGIC, // magic
0, // crc32
};
}
Item(const zbi_header_t& header, Compressor::Config compress)
: header_(header), compress_(compress) {
if (compress_) {
// We'll compress and checksum on the way out.
header_.flags |= ZBI_FLAG_STORAGE_COMPRESSED;
}
}
static ItemPtr MakeItem(const zbi_header_t& header,
Compressor::Config compress = Compressor::Config::None()) {
return ItemPtr(new Item(header, compress));
}
void StreamRawPayload(OutputStream* out) {
do {
out->Write(payload_.front());
payload_.pop_front();
} while (!payload_.empty());
}
uint32_t StreamRaw(OutputStream* out) {
// The header is already fully baked.
out->Write(Iovec(&header_, sizeof(header_)));
// The payload goes out as is.
StreamRawPayload(out);
return sizeof(header_) + header_.length;
}
uint32_t StreamCompressed(OutputStream* out) {
// Compress and checksum the payload.
Compressor compressor(compress_);
compressor.Init(out, header_);
do {
// The compressor streams the header and compressed payload out.
compressor.Write(out, payload_.front());
payload_.pop_front();
} while (!payload_.empty());
// This writes the final header as well as the last of the payload.
return compressor.Finish(out);
}
std::string Cmdline() const {
return std::accumulate(
payload_.begin(), payload_.end(), std::string(), [](std::string cmdline, const iovec& iov) {
return cmdline.append(static_cast<const char*>(iov.iov_base), iov.iov_len);
});
}
int ShowCmdline() const {
std::string cmdline = Cmdline();
size_t start = 0;
while (start < cmdline.size()) {
size_t word_end = cmdline.find_first_of(kCmdlineWS, start);
if (word_end == std::string::npos) {
if (cmdline[start] != '\0') {
printf(" : %s\n", cmdline.c_str() + start);
}
break;
}
if (word_end > start) {
printf(" : %.*s\n", static_cast<int>(word_end - start), cmdline.c_str() + start);
}
start = word_end + 1;
}
return 0;
}
void EmitJsonCmdline(rapidjson::PrettyWriter<rapidjson::FileWriteStream>& writer) {
std::string cmdline = Cmdline();
writer.String(cmdline.data(), static_cast<rapidjson::SizeType>(cmdline.size()));
}
const std::byte* payload_data() {
if (payload_.size() > 1) {
AppendBuffer buffer(PayloadSize());
for (const auto& iov : payload_) {
buffer.Append(iov.iov_base, iov.iov_len);
}
payload_.clear();
payload_.push_front(buffer.get());
OwnBuffer(buffer.release());
}
assert(payload_.size() == 1);
return static_cast<const std::byte*>(payload_.front().iov_base);
}
class BootFSDirectoryIterator final {
public:
operator bool() const { return left_ > 0; }
const zbi_bootfs_dirent_t& operator*() const {
auto entry = reinterpret_cast<const zbi_bootfs_dirent_t*>(next_);
assert(left_ >= sizeof(*entry));
return *entry;
}
const zbi_bootfs_dirent_t* operator->() const { return &**this; }
auto Open(FileOpener* opener, Item* fs) const {
if (!fs->CheckBootFSDirent(**this, false)) {
exit(1);
}
return opener->Emplace(**this, fs->payload_data());
}
BootFSDirectoryIterator& operator++() {
assert(left_ > 0);
if (left_ < sizeof(zbi_bootfs_dirent_t)) {
fprintf(stderr, "BOOTFS directory truncated\n");
left_ = 0;
} else {
size_t size = ZBI_BOOTFS_DIRENT_SIZE((*this)->name_len);
if (size > left_) {
fprintf(stderr, "BOOTFS directory truncated or bad name_len\n");
left_ = 0;
} else {
next_ += size;
left_ -= size;
}
}
return *this;
}
// The iterator itself is a container enough to use range-based for.
const BootFSDirectoryIterator& begin() { return *this; }
BootFSDirectoryIterator end() { return BootFSDirectoryIterator(); }
static int Create(Item* item, BootFSDirectoryIterator* it) {
zbi_bootfs_header_t superblock;
const uint32_t length = item->header_.length;
if (length < sizeof(superblock)) {
fprintf(stderr, "payload too short for BOOTFS header\n");
return 1;
}
memcpy(&superblock, item->payload_data(), sizeof(superblock));
if (superblock.magic != ZBI_BOOTFS_MAGIC) {
fprintf(stderr, "BOOTFS header magic %#x should be %#x\n", superblock.magic,
ZBI_BOOTFS_MAGIC);
return 1;
}
if (superblock.dirsize > length - sizeof(superblock)) {
fprintf(stderr, "BOOTFS header dirsize %u > payload size %zu\n", superblock.dirsize,
length - sizeof(superblock));
return 1;
}
it->next_ = item->payload_data() + sizeof(superblock);
it->left_ = superblock.dirsize;
return 0;
}
private:
const std::byte* next_ = nullptr;
uint32_t left_ = 0;
};
bool CheckBootFSDirent(const zbi_bootfs_dirent_t& entry, bool always_print) const {
const char* align_check =
entry.data_off % ZBI_BOOTFS_PAGE_SIZE == 0 ? "" : "[ERROR: misaligned offset] ";
const char* size_check =
(entry.data_off < header_.length && header_.length - entry.data_off >= entry.data_len)
? ""
: "[ERROR: offset+size too large] ";
bool ok = align_check[0] == '\0' && size_check[0] == '\0';
if (always_print || !ok) {
fprintf(always_print ? stdout : stderr, " : %08x %08x %s%s%.*s\n", entry.data_off,
entry.data_len, align_check, size_check, static_cast<int>(entry.name_len),
entry.name);
}
return ok;
}
int ShowBootFS() {
assert(!AlreadyCompressed());
BootFSDirectoryIterator dir;
int status = BootFSDirectoryIterator::Create(this, &dir);
for (const auto& entry : dir) {
if (!CheckBootFSDirent(entry, true)) {
status = 1;
}
}
return status;
}
void EmitJsonBootFS(rapidjson::PrettyWriter<rapidjson::FileWriteStream>& writer) {
BootFSDirectoryIterator dir;
int status = BootFSDirectoryIterator::Create(this, &dir);
if (status) {
exit(status);
}
rapidjson::Value files(rapidjson::kArrayType);
writer.StartArray();
for (const auto& entry : dir) {
writer.StartObject();
writer.Key("name");
writer.String(entry.name, entry.name_len - 1);
writer.Key("offset");
writer.Uint(entry.data_off);
writer.Key("length");
writer.Uint(entry.data_len);
writer.Key("size");
writer.Uint(ZBI_BOOTFS_PAGE_ALIGN(entry.data_len));
writer.EndObject();
}
writer.EndArray();
}
};
constexpr decltype(Item::kItemTypes_) Item::kItemTypes_;
// DirectoryTreeBuilder keeps pointers to elements, so this must be a
// container with stable element pointers across insertions.
using ItemList = std::deque<ItemPtr>;
const uint32_t kImageArchUndefined = ZBI_TYPE_DISCARD;
// Returns nullptr if complete, else an explanatory string.
const char* IncompleteImage(const ItemList& items, const uint32_t image_arch) {
if (items.empty()) {
return "empty ZBI";
}
if (!ZBI_IS_KERNEL_BOOTITEM(items.front()->type())) {
return "first item not KERNEL";
}
if (items.front()->type() != image_arch && image_arch != kImageArchUndefined) {
return "kernel arch mismatch";
}
auto count = std::count_if(items.begin(), items.end(), [](const ItemPtr& item) {
return item->type() == ZBI_TYPE_STORAGE_BOOTFS;
});
if (count == 0) {
return "no /boot BOOTFS item";
}
if (count > 1) {
return "multiple BOOTFS items";
}
return nullptr;
}
class DirectoryTreeBuilder final {
public:
DISALLOW_COPY_ASSIGN_AND_MOVE(DirectoryTreeBuilder);
DirectoryTreeBuilder() = delete;
explicit DirectoryTreeBuilder(FileOpener* opener) : opener_(opener) {}
Directory* tree() { return &tree_; }
void ReplaceFiles() { replace_ = true; }
const std::filesystem::path& SetPrefix(const std::filesystem::path& arg) {
if (arg.empty()) {
// Normalize to a nonempty prefix so /= works right.
// We'll normalize the concatenation before using it anyway.
prefix_ = ".";
} else {
prefix_ = arg.lexically_normal();
}
return prefix_;
}
// Note an input ZBI item in BOOTFS format. The argument is a stable
// pointer to an element in the caller's ItemList. We can freely null out
// the element now or on any later push_back or Insert call if we start
// building a directory tree.
void push_back(ItemPtr* item) {
const InputItem input{item, replace_};
if (tree_.empty()) {
// Just save the item for later.
items_.push_back(input);
} else {
// Already building a tree, so merge this right now.
Merge(input);
}
}
// Insert a file with complete target path, e.g. from a manifest entry.
void Insert(std::filesystem::path at, const File* file) { Insert(std::move(at), file, replace_); }
void ImportManifest(const FileContents& file, const char* manifest_name, bool ignore_missing) {
auto root = std::make_unique<Directory>();
auto read_ptr = static_cast<const char*>(file.View().iov_base);
const auto eof = read_ptr + file.exact_size();
for (unsigned int ln = 1; read_ptr != eof; ++ln) {
auto eol = static_cast<const char*>(memchr(read_ptr, '\n', eof - read_ptr));
auto line = read_ptr;
if (eol) {
read_ptr = eol + 1;
} else {
read_ptr = eol = eof;
}
auto eq = static_cast<const char*>(memchr(line, '=', eol - line));
if (!eq) {
fprintf(stderr, "%s:%u: manifest entry has no '=' separator: %.*s\n", manifest_name, ln,
static_cast<int>(eol - line), line);
exit(1);
}
auto file_or_dir = opener_->OpenFileOrDir({eq + 1, eol}, ignore_missing);
if (file_or_dir) {
Insert({line, eq}, file_or_dir, replace_);
}
}
}
void MergeRootDirectory(const Directory& dir) { MergeDirectory(&tree_, ".", dir, replace_); }
private:
Directory tree_;
std::deque<File> built_dirs_;
struct InputItem {
// This points into the input items list and can be nulled out
// to elide the item when it gets merged into the tree.
ItemPtr* item;
// True if --replace preceded the item.
bool replace;
};
std::deque<InputItem> items_;
// This holds items that have been merged in. They need to be kept
// alive here since the FileOpener now points into their contents.
std::forward_list<ItemPtr> merged_items_;
std::filesystem::path prefix_ = ".";
FileOpener* opener_;
bool replace_ = false;
static auto SubPath(const std::filesystem::path::const_iterator& first,
const std::filesystem::path::const_iterator& last) {
return std::accumulate(first, last, std::filesystem::path(),
std::divides<std::filesystem::path>());
}
// Insert a single node in a given directory. Inserting a directory
// where one already exists recurses to merge the new directory into
// the existing one. If file is nullptr then a new directory is
// created if needed. Returns the file inserted (passed or new directory).
const File* Insert(Directory* dir, std::filesystem::path path, const std::string& name,
const File* file, bool replace) {
if (name == "." || name == "..") {
fprintf(stderr, "%s: no . or .. allowed\n", (path / name).c_str());
exit(1);
}
if (!items_.empty()) {
// The new tree is being built, so old BOOTFS items must be merged.
Merge();
}
auto it = dir->try_emplace(name, file).first;
const auto old = it->second;
if (old != file) {
// There is already a different node at this name.
path /= name;
path = path.lexically_normal();
if (old->IsDir()) {
if (!file) {
// Just creating an intermediate directory, so the
// existing one is fine.
return old;
} else if (file->IsDir()) {
// Recurse on each entry in the incoming directory.
MergeDirectory(old->AsDir(), path, *file->AsDir(), replace);
return old;
} else if (!replace) {
fprintf(stderr,
"\
duplicate target path (directory vs file) without --replace: %s\n",
path.c_str());
exit(1);
}
} else if (!replace) {
fprintf(stderr, "duplicate target path without --replace: %s\n", path.c_str());
exit(1);
}
}
if (!file) {
// Make a new directory.
built_dirs_.emplace_back(std::make_unique<Directory>());
file = &built_dirs_.back();
}
// Replace the old file with the new one.
it->second = file;
return file;
}
void MergeDirectory(Directory* old, std::filesystem::path path, const Directory& dir,
bool replace) {
for (const auto& [child, entry] : dir) {
Insert(old, path, child, entry, replace);
}
}
// Insert a file with complete target path, e.g. from a manifest entry.
void Insert(const std::filesystem::path& at, const File* file, bool replace) {
Directory* dir = &tree_;
std::filesystem::path dirpath = ".";
const auto path = (prefix_ / at).lexically_normal();
auto it = path.begin();
while (true) {
const auto component = *it;
++it;
if (it == path.end()) {
Insert(dir, dirpath, component, file, replace);
break;
}
dir = Insert(dir, dirpath, component, nullptr, replace)->AsDir();
dirpath /= component;
}
}
// Merge a single old BOOTFS item into the new directory tree.
void Merge(const InputItem& input) {
// Null out the list entry.
ItemPtr old;
input.item->swap(old);
// Iterate through individual files in the BOOTFS in whatever order.
auto [it, fs] = Item::ReadBootFS(std::move(old));
while (it) {
Insert(it->name, it.Open(opener_, fs.get()), input.replace);
++it;
}
// Hold onto the item (original or decompressed version), since
// opener_->memory_cache_ now points into it.
merged_items_.push_front(std::move(fs));
}
// Merge all the old BOOTFS items into the new directory tree.
void Merge() {
// Clear the old list before any Insert calls reenter.
decltype(items_) items;
items_.swap(items);
// Merge each item;
while (!items.empty()) {
Merge(items.front());
items.pop_front();
}
}
};
bool ImportFile(const FileContents* file, const char* filename, ItemList* items,
DirectoryTreeBuilder* bootfs, std::optional<Compressor::Config> recompress) {
if (file->exact_size() < sizeof(zbi_header_t)) {
return false;
}
const zbi_header_t* header =
static_cast<const zbi_header_t*>(file->View(0, sizeof(zbi_header_t)).iov_base);
if (!(header->type == ZBI_TYPE_CONTAINER && header->extra == ZBI_CONTAINER_MAGIC &&
header->magic == ZBI_ITEM_MAGIC)) {
return false;
}
size_t file_size = file->exact_size() - sizeof(zbi_header_t);
if (file_size != header->length) {
fprintf(stderr, "%s: header size doesn't match file size\n", filename);
exit(1);
}
if (!Aligned(header->length)) {
fprintf(stderr, "ZBI item misaligned\n");
exit(1);
}
uint32_t pos = sizeof(zbi_header_t);
while (pos < file->exact_size()) {
auto item = Item::CreateFromItem(file, pos);
pos += item->TotalSize();
if (recompress) {
item = Item::Recompress(std::move(item), *recompress);
}
items->push_back(std::move(item));
if (items->back()->type() == ZBI_TYPE_STORAGE_BOOTFS) {
bootfs->push_back(&items->back());
}
}
return true;
}
enum LongOnlyOpt : int {
kOptRecompress = 0x100,
};
constexpr const char kOptString[] = "-B:c::C:d:D:e:Fij:xXRhto:p:T:uv";
constexpr const option kLongOpts[] = {
{"complete", required_argument, nullptr, 'B'},
{"compressed", optional_argument, nullptr, 'c'},
{"directory", required_argument, nullptr, 'C'},
{"depfile", required_argument, nullptr, 'd'},
{"entry", required_argument, nullptr, 'e'},
{"extract", no_argument, nullptr, 'x'},
{"extract-items", no_argument, nullptr, 'X'},
{"extract-raw", no_argument, nullptr, 'R'},
{"files", no_argument, nullptr, 'F'},
{"help", no_argument, nullptr, 'h'},
{"ignore-missing-files", no_argument, nullptr, 'i'},
{"json-output", required_argument, nullptr, 'j'},
{"list", no_argument, nullptr, 't'},
{"output", required_argument, nullptr, 'o'},
{"output-dir", required_argument, nullptr, 'D'},
{"prefix", required_argument, nullptr, 'p'},
{"type", required_argument, nullptr, 'T'},
{"uncompressed", no_argument, nullptr, 'u'},
{"verbose", no_argument, nullptr, 'v'},
{"recompress", no_argument, nullptr, kOptRecompress},
{"replace", no_argument, nullptr, 'r'},
{nullptr, no_argument, nullptr, 0},
};
constexpr const char kUsageFormatString[] =
"\
Usage: %s [OUTPUT...] INPUT... [-- PATTERN...]\n\
\n\
Diagnostic switches:\n\
--help, -h print this message\n\
--list, -t list input ZBI item headers; no --output\n\
--verbose, -v show contents (e.g. BOOTFS file names)\n\
--extract, -x extract BOOTFS files\n\
--extract-items, -X extract items as pseudo-files (see below)\n\
--extract-raw, -R extract original payloads, not ZBI format\n\
\n\
Output file switches:\n\
--output=FILE, -o FILE output file name\n\
--depfile=FILE, -d FILE makefile dependency output file name\n\
--output-dir=DIR, -D FILE extracted files go under DIR (default: .)\n\
--json-output=FILE, -j FILE record entries to a JSON file\n\
\n\
The `--output` FILE is always removed and created fresh after all input\n\
files have been opened. So it is safe to use the same file name as an input\n\
file and the `--output` FILE, to append more items.\n\
\n\
Input control switches apply to subsequent input arguments:\n\
--directory=DIR, -C DIR change directory to DIR\n\
--files, -F read BOOTFS manifest files (default)\n\
--prefix=PREFIX, -p PREFIX prepend PREFIX/ to target file names\n\
--replace, -r duplicate target file name OK (see below)\n\
--type=TYPE, -T TYPE input files are TYPE items (see below)\n\
--compressed[=HOW], -c [HOW] compress storage images (see below)\n\
--uncompressed, -u do not compress storage images\n\
--recompress recompress input items already compressed\n\
--ignore-missing-files, -i a manifest entry whose source file doesn't\n\
exist is ignored without error\n\
\n\
Input arguments:\n\
--entry=TEXT, -e TEXT like an input file containing only TEXT\n\
FILE input or manifest file\n\
DIRECTORY directory tree copied to BOOTFS PREFIX/\n\
\n\
The `--directory` or `-C` switch affects subsequent input arguments but\n\
it never affects output arguments, which are always relative to the original\n\
current working directory (`zbi` doesn't actually do `chdir()` at all).\n\
\n\
With `--files` or `-F` (the default state), files with ZBI_TYPE_CONTAINER\n\
headers are incomplete boot files and other files are BOOTFS manifest files.\n\
Each DIRECTORY is listed recursively and handled just like a manifest file\n\
using the path relative to DIRECTORY as the target name (before any PREFIX).\n\
Each `--prefix` or `-p` switch affects each file from a manifest or\n\
directory in subsequent FILE, DIRECTORY, or TEXT arguments.\n\
\n\
With `--type` or `-T`, input files are treated as TYPE instead of manifest\n\
files, and directories are not permitted. See below for the TYPE strings.\n\
\n\
ZBI items from input ZBI files are normally emitted unchanged. (However,\n\
see below about BOOTFS items.) With `--recompress`, input items of storage\n\
types well be decompressed (if needed) on input and then freshly compressed\n\
(or not) according to the preceding `--compressed=...` or `--uncompressed`.\n\
\n\
Format control switches (last switch affects all output):\n\
--complete=ARCH, -B ARCH verify result is a complete boot image\n\
--compressed[=HOW], -c [HOW] compress BOOTFS images (see below)\n\
--uncompressed, -u do not compress BOOTFS images\n\
\n\
HOW defaults to `zstd` and can be one of (case-insensitive):\n\
* `none` (same as `--uncompressed`)\n\
* `LEVEL` (an integer) or `max` (default algorithm, currently `zstd`)\n\
* `lz4f` or `lz4f.LEVEL` (an integer) or `lz4f.max`\n\
* `zstd` or `zstd.LEVEL` (an integer) or `zstd.max` or `zstd.overclock`\n\
The meaning of LEVEL depends on the algorithm. The default is chosen for\n\
good compression ratios with fast compression time. `max` is for the best\n\
compression ratios but much slower compression time (e.g. release builds).\n\
\n\
If there are no PATTERN arguments and no files named to add to the BOOTFS\n\
(via manifest file entries, nonempty directories, or `--entry` switches)\n\
then any ZBI input items of BOOTFS type are passed through as they are,\n\
except for possibly compressing raw `--type=bootfs` input items.\n\
In all other cases there is only a single BOOTFS item (if any) written out.\n\
So `-- \\*` will force merging when no individual files are being added.\n\
\n\
The BOOTFS image contains all files from BOOTFS items in ZBI input files,\n\
manifest files, directories, and `--entry` switches. The BOOTFS directory\n\
table is always sorted. By default it's an error to have duplicate target\n\
file names in the input (even with the same source). `--replace` or `-r`\n\
allows it: the last entry in input order wins.\n\
**TODO(mcgrathr):** not quite true yet\n\
\n\
Each argument after -- is a shell filename PATTERN (`*` matches even `/`)\n\
to filter the files that will be packed into BOOTFS, extracted, or listed.\n\
For a PATTERN that starts with `!` or `^` matching names are excluded after\n\
including matches for all positive PATTERN arguments. Note that PATTERN\n\
is compared to the final BOOTFS target file name with any PREFIX applied.\n\
\n\
When extracting a single file, `--output` or `-o` can be used.\n\
Otherwise multiple files are created with their BOOTFS file names\n\
relative to PREFIX (default empty, so in the current directory).\n\
Note that the last PREFIX on the command line affects extraction,\n\
though each PREFIX also (first) affects BOOTFS files added due to arguments\n\
that follow it. So if any PREFIX appears before such input arguments when\n\
extracting, the extracted file names will have a doubled PREFIX unless a\n\
`--prefix=.` or other PREFIX value follows the input arguments.\n\
\n\
With `--extract-items` or `-X`, instead of BOOTFS files the names are\n\
synthesized as shown below, numbered in the order items appear in the input\n\
starting with 001. Output files are ZBI files that can be input later.\n\
\n\
With `--extract-raw` or `-R`, each file is written with just the\n\
uncompressed payload of the item and no ZBI headers.\n\
";
void usage(const char* progname) {
fprintf(stderr, kUsageFormatString, progname);
Item::PrintTypeUsage(stderr);
}
} // anonymous namespace
int main(int argc, char** argv) {
FileOpener opener;
const char* outfile = nullptr;
const