| // 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 <algorithm> |
| #include <array> |
| #include <cassert> |
| #include <cerrno> |
| #include <climits> |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cstring> |
| #include <deque> |
| #include <dirent.h> |
| #include <fcntl.h> |
| #include <forward_list> |
| #include <functional> |
| #include <getopt.h> |
| #include <limits> |
| #include <list> |
| #include <memory> |
| #include <numeric> |
| #include <set> |
| #include <string> |
| #include <sys/mman.h> |
| #include <sys/stat.h> |
| #include <sys/uio.h> |
| #include <unistd.h> |
| #include <utility> |
| |
| #include <fbl/macros.h> |
| #include <fbl/unique_fd.h> |
| #include <lib/cksum.h> |
| #include <lz4/lz4frame.h> |
| #include <zircon/boot/bootdata.h> |
| |
| namespace { |
| |
| bool Aligned(uint32_t length) { |
| return BOOTDATA_ALIGN(length) == length; |
| } |
| |
| // It's not clear where this magic number comes from. |
| constexpr size_t kLZ4FMaxHeaderFrameSize = 128; |
| |
| // 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)) { |
| assert(size > 0); |
| return {const_cast<void*>(static_cast<const void*>(buffer)), size}; |
| } |
| |
| class AppendBuffer { |
| public: |
| explicit AppendBuffer(size_t size) : |
| buffer_(std::make_unique<uint8_t[]>(size)), ptr_(buffer_.get()) { |
| } |
| |
| size_t size() const { |
| return ptr_ - buffer_.get(); |
| } |
| |
| iovec get() { |
| return Iovec(buffer_.get(), size()); |
| } |
| |
| std::unique_ptr<uint8_t[]> release() { |
| ptr_ = nullptr; |
| return std::move(buffer_); |
| } |
| |
| template<typename T> |
| void Append(const T* data, size_t bytes = sizeof(T)) { |
| ptr_ = static_cast<uint8_t*>(memcpy(static_cast<void*>(ptr_), |
| static_cast<const void*>(data), |
| bytes)) + bytes; |
| } |
| |
| void Pad(size_t bytes) { |
| ptr_ = static_cast<uint8_t*>(memset(static_cast<void*>(ptr_), 0, |
| bytes)) + bytes; |
| } |
| |
| private: |
| std::unique_ptr<uint8_t[]> buffer_; |
| uint8_t* ptr_ = nullptr; |
| }; |
| |
| class Item; |
| |
| class OutputStream { |
| public: |
| OutputStream() = delete; |
| |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(OutputStream); |
| |
| 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<uint8_t[]> owned = nullptr) { |
| assert(buffer.iov_len > 0); |
| if (buffer.iov_len + total_ > UINT32_MAX - sizeof(bootdata_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)); |
| } |
| } |
| |
| // Take ownership of the Item after it has called this->Write |
| // repeatedly, using pointers into buffers owned by the Item. |
| void OwnItem(std::unique_ptr<Item> item) { |
| if (Buffering()) { |
| // Keep the Item alive as long as we might be buffering pointers |
| // into memory it owns. |
| owned_items_.push_front(std::move(item)); |
| } |
| } |
| |
| 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(); |
| owned_items_.clear(); |
| } |
| |
| // Emit a placeholder. The return value will be passed to PatchHeader. |
| uint32_t PlaceHeader() { |
| uint32_t pos = WritePosition(); |
| static const bootdata_t dummy = {}; |
| Write(Iovec(&dummy)); |
| return pos; |
| } |
| |
| // Replace a placeholder with a real header. |
| void PatchHeader(const bootdata_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<uint8_t[]>(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 uint8_t*>(&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<uint8_t[]>> owned_buffers_; |
| std::forward_list<std::unique_ptr<Item>> owned_items_; |
| 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), write_pos_ - read_pos); |
| if (wrote < 0) { |
| perror("writev to output file"); |
| exit(1); |
| } |
| flushed_ += wrote; |
| assert(static_cast<off_t>(flushed_) == lseek(fd_.get(), 0, SEEK_CUR)); |
| // Skip all the buffers that were wholly written. |
| while (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<uint8_t*>(read_pos->iov_base) + wrote); |
| } |
| return read_pos; |
| } |
| }; |
| |
| class Checksummer { |
| public: |
| void Write(const iovec& buffer) { |
| crc_ = crc32(crc_, static_cast<const uint8_t*>(buffer.iov_base), |
| buffer.iov_len); |
| } |
| |
| void FinalizeHeader(bootdata_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; |
| }; |
| |
| // This tells LZ4f_compressUpdate it can keep a pointer to data. |
| constexpr const LZ4F_compressOptions_t kCompressOpt = { 1, {} }; |
| |
| class Compressor { |
| public: |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Compressor); |
| Compressor() = default; |
| |
| #define LZ4F_CALL(func, ...) \ |
| [&](){ \ |
| auto result = func(__VA_ARGS__); \ |
| if (LZ4F_isError(result)) { \ |
| fprintf(stderr, "%s: %s\n", #func, LZ4F_getErrorName(result)); \ |
| exit(1); \ |
| } \ |
| return result; \ |
| }() |
| |
| void Init(OutputStream* out, const bootdata_t& header) { |
| header_ = header; |
| assert(header_.flags & BOOTDATA_BOOTFS_FLAG_COMPRESSED); |
| assert(header_.flags & BOOTDATA_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(); |
| |
| prefs_.frameInfo.contentSize = header_.length; |
| |
| prefs_.frameInfo.blockSizeID = LZ4F_max64KB; |
| prefs_.frameInfo.blockMode = LZ4F_blockIndependent; |
| |
| // LZ4 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. |
| prefs_.compressionLevel = 4; |
| |
| LZ4F_CALL(LZ4F_createCompressionContext, &ctx_, LZ4F_VERSION); |
| |
| // Record the original uncompressed size in header_.extra. |
| // WriteBuffer will accumulate the compressed size in header_.length. |
| header_.extra = header_.length; |
| header_.length = 0; |
| |
| // This might start writing compression format headers before it |
| // receives any data. |
| auto buffer = GetBuffer(kLZ4FMaxHeaderFrameSize); |
| size_t size = LZ4F_CALL(LZ4F_compressBegin, ctx_, |
| buffer.data.get(), buffer.size, &prefs_); |
| assert(size <= buffer.size); |
| WriteBuffer(out, std::move(buffer), size); |
| } |
| |
| ~Compressor() { |
| LZ4F_CALL(LZ4F_freeCompressionContext, ctx_); |
| } |
| |
| // NOTE: Input buffer may be referenced for the life of the Compressor! |
| void Write(OutputStream* out, const iovec& input) { |
| auto buffer = GetBuffer(LZ4F_compressBound(input.iov_len, &prefs_)); |
| size_t actual_size = LZ4F_CALL(LZ4F_compressUpdate, |
| ctx_, buffer.data.get(), buffer.size, |
| input.iov_base, input.iov_len, |
| &kCompressOpt); |
| WriteBuffer(out, std::move(buffer), actual_size); |
| } |
| |
| uint32_t Finish(OutputStream* out) { |
| // Write the closing chunk from the compressor. |
| auto buffer = GetBuffer(LZ4F_compressBound(0, &prefs_)); |
| size_t actual_size = LZ4F_CALL(LZ4F_compressEnd, |
| ctx_, buffer.data.get(), buffer.size, |
| &kCompressOpt); |
| |
| WriteBuffer(out, std::move(buffer), actual_size); |
| |
| // Complete the checksum. |
| crc_.FinalizeHeader(&header_); |
| |
| // Write the header back where its place was held. |
| out->PatchHeader(header_, header_pos_); |
| return header_.length; |
| } |
| |
| private: |
| struct Buffer { |
| // Move-only type: after moving, data is nullptr and size is 0. |
| Buffer() = default; |
| Buffer(std::unique_ptr<uint8_t[]> 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<uint8_t[]> data; |
| size_t size = 0; |
| } unused_buffer_; |
| bootdata_t header_; |
| Checksummer crc_; |
| LZ4F_compressionContext_t ctx_; |
| LZ4F_preferences_t prefs_{}; |
| uint32_t header_pos_ = 0; |
| // IOV_MAX buffers might be live at once. |
| static constexpr const size_t kMinBufferSize = (128 << 20) / IOV_MAX; |
| |
| 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<uint8_t[]>(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); |
| } |
| } |
| |
| #undef LZ4F_CALL |
| }; |
| |
| const size_t Compressor::kMinBufferSize; |
| |
| class FileContents { |
| public: |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FileContents); |
| FileContents() = default; |
| |
| 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_); |
| return *this; |
| } |
| |
| ~FileContents() { |
| if (mapped_) { |
| munmap(mapped_, mapped_size_); |
| } |
| } |
| |
| size_t exact_size() const { return exact_size_; } |
| size_t mapped_size() const { return mapped_size_; } |
| |
| 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 >= BOOTFS_PAGE_SIZE); |
| assert(pagesize % BOOTFS_PAGE_SIZE == 0); |
| return pagesize; |
| }(); |
| |
| 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(size_t offset, size_t length) const { |
| assert(length > 0); |
| assert(offset < exact_size_); |
| assert(exact_size_ - offset >= length); |
| return Iovec(static_cast<const uint8_t*>(mapped_) + offset, length); |
| } |
| |
| const iovec PageRoundedView(size_t offset, size_t length) const { |
| assert(length > 0); |
| assert(offset < mapped_size_); |
| assert(mapped_size_ - offset >= length); |
| return Iovec(static_cast<const uint8_t*>(mapped_) + offset, length); |
| } |
| |
| private: |
| void* mapped_ = nullptr; |
| size_t mapped_size_ = 0; |
| size_t exact_size_ = 0; |
| }; |
| |
| class FileOpener { |
| public: |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FileOpener); |
| FileOpener() = default; |
| |
| void Init(const char* output_file, const char* depfile) { |
| if (depfile) { |
| depfile_ = fopen(depfile, "w"); |
| if (!depfile_) { |
| perror(depfile); |
| exit(1); |
| } |
| fprintf(depfile_, "%s:", output_file); |
| } |
| } |
| |
| fbl::unique_fd Open(const char* file, struct stat* st = nullptr) { |
| fbl::unique_fd fd(open(file, O_RDONLY)); |
| if (!fd) { |
| perror(file); |
| exit(1); |
| } |
| if (st && fstat(fd.get(), st) < 0) { |
| perror("fstat"); |
| exit(1); |
| } |
| if (depfile_) { |
| fprintf(depfile_, " %s", file); |
| } |
| return fd; |
| } |
| |
| fbl::unique_fd Open(const std::string& file, struct stat* st = nullptr) { |
| return Open(file.c_str(), st); |
| } |
| |
| ~FileOpener() { |
| if (depfile_) { |
| fputc('\n', depfile_); |
| fclose(depfile_); |
| } |
| } |
| |
| private: |
| FILE* depfile_ = nullptr; |
| }; |
| |
| void RequireRegularFile(const struct stat& st, const char* file) { |
| if (!S_ISREG(st.st_mode)) { |
| fprintf(stderr, "%s: not a regular file\n", file); |
| exit(1); |
| } |
| } |
| |
| class GroupFilter { |
| public: |
| DISALLOW_COPY_ASSIGN_AND_MOVE(GroupFilter); |
| GroupFilter() = default; |
| |
| void SetFilter(const char* groups) { |
| if (!strcmp(groups, "all")) { |
| groups_.reset(); |
| } else { |
| groups_ = std::make_unique<std::set<std::string>>(); |
| while (const char *p = strchr(groups, ',')) { |
| groups_->emplace(groups, p - groups); |
| groups = p + 1; |
| } |
| groups_->emplace(groups); |
| } |
| } |
| |
| bool AllowsAll() const { |
| return !groups_; |
| } |
| |
| bool Allows(const std::string& group) const { |
| return AllowsAll() || groups_->find(group) != groups_->end(); |
| } |
| |
| private: |
| std::unique_ptr<std::set<std::string>> groups_; |
| }; |
| |
| // Base class for ManifestInputFileGenerator and DirectoryInputFileGenerator. |
| // These both deliver target name -> file contents mappings until they don't. |
| struct InputFileGenerator { |
| struct value_type { |
| std::string target; |
| FileContents file; |
| }; |
| virtual ~InputFileGenerator() = default; |
| virtual bool Next(FileOpener*, const GroupFilter&, value_type*) = 0; |
| }; |
| |
| class ManifestInputFileGenerator : public InputFileGenerator { |
| public: |
| explicit ManifestInputFileGenerator(FileContents file) : |
| file_(std::move(file)) { |
| read_ptr_ = static_cast<const char*>( |
| file_.View(0, file_.exact_size()).iov_base); |
| eof_ = read_ptr_ + file_.exact_size(); |
| } |
| |
| ~ManifestInputFileGenerator() override = default; |
| |
| bool Next(FileOpener* opener, const GroupFilter& filter, |
| value_type* value) override { |
| while (read_ptr_ != eof_) { |
| auto eol = static_cast<const char*>( |
| memchr(read_ptr_, '\n', eof_ - read_ptr_)); |
| if (!eol) { |
| fprintf(stderr, "manifest file does not end with newline\n"); |
| exit(1); |
| } |
| auto line = read_ptr_; |
| read_ptr_ = eol + 1; |
| |
| auto eq = static_cast<const char*>(memchr(line, '=', eol - line)); |
| if (!eq) { |
| fprintf(stderr, "manifest entry has no '=' separator: %.*s\n", |
| static_cast<int>(eol - line), line); |
| exit(1); |
| } |
| |
| line = AllowEntry(filter, line, eq, eol); |
| if (line) { |
| std::string target(line, eq - line); |
| std::string source(eq + 1, eol - (eq + 1)); |
| struct stat st; |
| auto fd = opener->Open(source, &st); |
| RequireRegularFile(st, source.c_str()); |
| auto file = FileContents::Map(fd, st, source.c_str()); |
| *value = value_type{std::move(target), std::move(file)}; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private: |
| FileContents file_; |
| const char* read_ptr_ = nullptr; |
| const char* eof_ = nullptr; |
| |
| // Returns the beginning of the `target=source` portion of the entry |
| // if the entry is allowed by the filter, otherwise nullptr. |
| static const char* AllowEntry(const GroupFilter& filter, const char* start, |
| const char* eq, const char* eol) { |
| if (*start != '{') { |
| // This entry doesn't specify a group. |
| return filter.AllowsAll() ? start : nullptr; |
| } |
| auto end_group = static_cast<const char*>( |
| memchr(start + 1, '}', eq - start)); |
| if (!end_group) { |
| fprintf(stderr, |
| "manifest entry has '{' but no '}': %.*s\n", |
| static_cast<int>(eol - start), start); |
| exit(1); |
| } |
| std::string group(start, end_group - start); |
| return filter.Allows(group) ? end_group + 1 : nullptr; |
| } |
| }; |
| |
| class DirectoryInputFileGenerator : public InputFileGenerator { |
| public: |
| DirectoryInputFileGenerator(fbl::unique_fd fd, const char* dirname) : |
| source_prefix_(dirname) { |
| walk_pos_.emplace_front(MakeUniqueDir(std::move(fd)), 0); |
| } |
| |
| ~DirectoryInputFileGenerator() override = default; |
| |
| bool Next(FileOpener* opener, const GroupFilter&, |
| value_type* value) override { |
| do { |
| const dirent* d = readdir(walk_pos_.front().dir.get()); |
| if (!d) { |
| Ascend(); |
| continue; |
| } |
| if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) { |
| continue; |
| } |
| std::string target = walk_prefix_ + d->d_name; |
| std::string source = source_prefix_ + target; |
| struct stat st; |
| auto fd = opener->Open(source, &st); |
| if (S_ISDIR(st.st_mode)) { |
| Descend(std::move(fd), d->d_name); |
| } else { |
| RequireRegularFile(st, source.c_str()); |
| auto file = FileContents::Map(std::move(fd), st, |
| source.c_str()); |
| *value = value_type{std::move(target), std::move(file)}; |
| return true; |
| } |
| } while (!walk_pos_.empty()); |
| return false; |
| } |
| |
| private: |
| // std::unique_ptr for fdopendir/closedir. |
| static void DeleteUniqueDir(DIR* dir) { |
| closedir(dir); |
| } |
| using UniqueDir = std::unique_ptr<DIR, decltype(&DeleteUniqueDir)>; |
| UniqueDir MakeUniqueDir(fbl::unique_fd fd) { |
| DIR* dir = fdopendir(fd.release()); |
| if (!dir) { |
| perror("fdopendir"); |
| exit(1); |
| } |
| return UniqueDir(dir, &DeleteUniqueDir); |
| } |
| |
| // State of our depth-first directory tree walk. |
| struct WalkState { |
| WalkState(UniqueDir d, size_t len) : |
| dir(std::move(d)), parent_prefix_len(len) { |
| } |
| UniqueDir dir; |
| size_t parent_prefix_len; |
| }; |
| |
| const std::string source_prefix_; |
| std::forward_list<WalkState> walk_pos_; |
| std::string walk_prefix_; |
| |
| void Descend(fbl::unique_fd fd, const char* name) { |
| size_t parent = walk_prefix_.size(); |
| walk_prefix_ += name; |
| walk_prefix_ += "/"; |
| walk_pos_.emplace_front(MakeUniqueDir(std::move(fd)), parent); |
| } |
| |
| void Ascend() { |
| walk_prefix_.resize(walk_pos_.front().parent_prefix_len); |
| walk_pos_.pop_front(); |
| } |
| }; |
| |
| void DescribeHeader(uint32_t pos, uint32_t length, const char* type) { |
| printf("%08x: %08x %s\n", pos, length, type); |
| } |
| |
| class Item { |
| public: |
| // Only the static methods below can create an Item. |
| Item() = delete; |
| |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Item); |
| |
| uint32_t type() const { |
| return header_.type; |
| } |
| |
| uint32_t PayloadSize() const { |
| return header_.length; |
| } |
| |
| uint32_t TotalSize() const { |
| return sizeof(header_) + BOOTDATA_ALIGN(PayloadSize()); |
| } |
| |
| void Describe(uint32_t pos) const { |
| switch (header_.type) { |
| case BOOTDATA_BOOTFS_BOOT: |
| printf("%08x: %08x BOOTFS @/boot (size=%08x)\n", |
| pos, header_.length, header_.extra); |
| break; |
| case BOOTDATA_BOOTFS_SYSTEM: |
| printf("%08x: %08x BOOTFS @/system (size=%08x)\n", |
| pos, header_.length, header_.extra); |
| break; |
| case BOOTDATA_RAMDISK: |
| printf("%08x: %08x RAMDISK (size=%08x)\n", |
| pos, header_.length, header_.extra); |
| break; |
| |
| #define BOOTDATA_CASE(type) \ |
| case BOOTDATA_##type: \ |
| DescribeHeader(pos, header_.length, #type); \ |
| break |
| BOOTDATA_CASE(KERNEL); |
| BOOTDATA_CASE(CMDLINE); |
| BOOTDATA_CASE(ACPI_RSDP); |
| BOOTDATA_CASE(FRAMEBUFFER); |
| BOOTDATA_CASE(DEBUG_UART); |
| BOOTDATA_CASE(PLATFORM_ID); |
| BOOTDATA_CASE(LASTLOG_NVRAM); |
| BOOTDATA_CASE(LASTLOG_NVRAM2); |
| BOOTDATA_CASE(E820_TABLE); |
| BOOTDATA_CASE(EFI_MEMORY_MAP); |
| BOOTDATA_CASE(EFI_SYSTEM_TABLE); |
| BOOTDATA_CASE(LAST_CRASHLOG); |
| BOOTDATA_CASE(IGNORE); |
| #undef BOOTDATA_CASE |
| |
| default: |
| printf("%08x: %08x UNKNOWN (type=%08x)\n", |
| pos, header_.length, header_.type); |
| break; |
| } |
| |
| if (header_.flags & BOOTDATA_FLAG_CRC32) { |
| printf(" : MAGIC=%08x CRC=%08x\n", |
| header_.magic, header_.crc32); |
| |
| Checksummer crc; |
| for (const auto& chunk : payload_) { |
| crc.Write(chunk); |
| } |
| bootdata_t check_header = header_; |
| crc.FinalizeHeader(&check_header); |
| |
| if (check_header.crc32 != header_.crc32) { |
| fprintf(stderr, "error: CRC %08x does not match header\n", |
| check_header.crc32); |
| } |
| } else { |
| printf(" : MAGIC=%08x NO CRC\n", header_.magic); |
| } |
| } |
| |
| // 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 (e.g. via OutputStream::OwnItem). |
| void Stream(OutputStream* out) { |
| assert(Aligned(out->WritePosition())); |
| uint32_t wrote = compress_ ? StreamCompressed(out) : StreamRaw(out); |
| assert(out->WritePosition() % BOOTDATA_ALIGN(1) == |
| wrote % BOOTDATA_ALIGN(1)); |
| uint32_t aligned = BOOTDATA_ALIGN(wrote); |
| if (aligned > wrote) { |
| static const uint8_t padding[BOOTDATA_ALIGN(1)]{}; |
| 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<uint8_t[]> buffer) { |
| buffers_.push_front(std::move(buffer)); |
| } |
| void OwnFile(FileContents file) { |
| files_.push_front(std::move(file)); |
| } |
| |
| // Create from in-core data. |
| static std::unique_ptr<Item> CreateFromBuffer( |
| uint32_t type, std::unique_ptr<uint8_t[]> payload, size_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(Iovec(payload.get(), size)); |
| crc.FinalizeHeader(&item->header_); |
| return item; |
| } |
| |
| // Create from local scratch data. |
| template<typename T> |
| static std::unique_ptr<Item> Create(uint32_t type, const T& payload) { |
| auto buffer = std::make_unique<uint8_t[]>(sizeof(payload)); |
| memcpy(buffer.get(), &payload, sizeof(payload)); |
| return CreateFromBuffer(type, std::move(buffer), sizeof(payload)); |
| } |
| |
| // Create from raw file contents. |
| static std::unique_ptr<Item> CreateFromFile( |
| FileContents file, uint32_t type, bool compress, bool null_terminate) { |
| size_t size = file.exact_size() + (null_terminate ? 1 : 0); |
| auto item = MakeItem(NewHeader(type, 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(0, file.exact_size())); |
| } |
| |
| if (!compress) { |
| // Compute the checksum now so the item is ready to write out. |
| Checksummer crc; |
| crc.Write(file.View(0, file.exact_size())); |
| if (null_terminate) { |
| crc.Write(Iovec("", 1)); |
| } |
| } |
| |
| // The item now owns the file mapping that its payload points into. |
| item->OwnFile(std::move(file)); |
| |
| return item; |
| } |
| |
| // Create from an existing fully-baked item in an input file. |
| static std::unique_ptr<Item> CreateFromItem(const FileContents& file, |
| uint32_t offset) { |
| if (offset > file.exact_size() || |
| file.exact_size() - offset < sizeof(bootdata_t)) { |
| fprintf(stderr, "input file too short for next header\n"); |
| exit(1); |
| } |
| const bootdata_t* header = static_cast<const bootdata_t*>( |
| file.View(offset, sizeof(bootdata_t)).iov_base); |
| offset += sizeof(bootdata_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 a BOOTFS item. |
| static std::unique_ptr<Item> CreateBootFS(FileOpener* opener, |
| const GroupFilter& filter, |
| InputFileGenerator* files, |
| uint32_t type, bool compress) { |
| auto item = MakeItem(NewHeader(type, 0), compress); |
| |
| // Collect the names and exact sizes here and the contents in payload_. |
| struct Entry { |
| std::string name; |
| uint32_t data_len = 0; |
| }; |
| std::deque<Entry> entries; |
| size_t dirsize = 0, bodysize = 0; |
| InputFileGenerator::value_type next; |
| while (files->Next(opener, filter, &next)) { |
| // Accumulate the space needed for each bootfs_entry_t. |
| dirsize += (sizeof(bootfs_entry_t) + |
| BOOTFS_ALIGN(next.target.size() + 1)); |
| Entry entry; |
| entry.name.swap(next.target); |
| entry.data_len = static_cast<uint32_t>(next.file.exact_size()); |
| if (entry.data_len != next.file.exact_size()) { |
| fprintf(stderr, "input file size exceeds format maximum\n"); |
| exit(1); |
| } |
| uint32_t size = BOOTFS_PAGE_ALIGN(entry.data_len); |
| bodysize += size; |
| item->payload_.emplace_back(next.file.PageRoundedView(0, size)); |
| entries.push_back(std::move(entry)); |
| item->OwnFile(std::move(next.file)); |
| } |
| |
| // Now we can calculate the final sizes. |
| const bootfs_header_t header = { |
| BOOTFS_MAGIC, // magic |
| static_cast<uint32_t>(dirsize), // dirsize |
| 0, // reserved0 |
| 0, // reserved1 |
| }; |
| size_t header_size = BOOTFS_PAGE_ALIGN(sizeof(header) + dirsize); |
| item->header_.length = static_cast<uint32_t>(header_size + bodysize); |
| if (item->header_.length != header_size + bodysize) { |
| fprintf(stderr, "BOOTFS image size exceeds format maximum\n"); |
| exit(1); |
| } |
| |
| // Now fill a buffer with the BOOTFS header and directory entries. |
| AppendBuffer buffer(header_size); |
| buffer.Append(&header); |
| uint32_t data_off = static_cast<uint32_t>(header_size); |
| for (const auto& file : item->payload_) { |
| const auto& entry = entries.front(); |
| const bootfs_entry_t entry_hdr = { |
| static_cast<uint32_t>(entry.name.size() + 1), // name_len |
| entry.data_len, // data_len |
| data_off, // data_off |
| }; |
| data_off += static_cast<uint32_t>(file.iov_len); |
| buffer.Append(&entry_hdr); |
| buffer.Append(entry.name.c_str(), entry_hdr.name_len); |
| buffer.Pad(BOOTFS_ALIGN(entry_hdr.name_len) - entry_hdr.name_len); |
| entries.pop_front(); |
| } |
| assert(data_off == item->header_.length); |
| // Zero fill to the end of the page. |
| buffer.Pad(header_size - buffer.size()); |
| |
| if (!compress) { |
| // Checksum the BOOTFS image right now: header and then payload. |
| Checksummer crc; |
| crc.Write(buffer.get()); |
| for (const auto& file : item->payload_) { |
| crc.Write(file); |
| } |
| 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; |
| } |
| |
| // Create a BOOTFS item from a directory tree. |
| static std::unique_ptr<Item> ImportDirectory( |
| FileOpener* opener, const char* dirname, |
| uint32_t type, bool compress) { |
| DirectoryInputFileGenerator files(opener->Open(dirname), dirname); |
| return CreateBootFS(opener, GroupFilter(), &files, type, compress); |
| } |
| |
| // Create a BOOTFS item from a manifest file. |
| static std::unique_ptr<Item> ImportManifest( |
| FileOpener* opener, FileContents file, const GroupFilter& filter, |
| uint32_t type, bool compress) { |
| ManifestInputFileGenerator files(std::move(file)); |
| return CreateBootFS(opener, filter, &files, type, compress); |
| } |
| |
| private: |
| bootdata_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<FileContents> files_; |
| std::forward_list<std::unique_ptr<uint8_t[]>> buffers_; |
| const bool compress_; |
| |
| static constexpr bootdata_t NewHeader(uint32_t type, uint32_t size) { |
| return { |
| type, // type |
| size, // length |
| 0, // extra |
| BOOTDATA_FLAG_V2 | BOOTDATA_FLAG_CRC32, // flags |
| 0, // reserved0 |
| 0, // reserved1 |
| BOOTITEM_MAGIC, // magic |
| 0, // crc32 |
| }; |
| } |
| |
| Item(const bootdata_t& header, bool compress) : |
| header_(header), compress_(compress) { |
| if (compress_) { |
| // We'll compress and checksum on the way out. |
| header_.flags |= BOOTDATA_BOOTFS_FLAG_COMPRESSED; |
| } |
| } |
| |
| static std::unique_ptr<Item> MakeItem(const bootdata_t& header, |
| bool compress = false) { |
| return std::unique_ptr<Item>(new Item(header, compress)); |
| } |
| |
| uint32_t StreamRaw(OutputStream* out) { |
| // The header is already fully baked. |
| out->Write(Iovec(&header_, sizeof(header_))); |
| // The payload goes out as is. |
| do { |
| out->Write(payload_.front()); |
| payload_.pop_front(); |
| } while (!payload_.empty()); |
| return sizeof(header_) + header_.length; |
| } |
| |
| uint32_t StreamCompressed(OutputStream* out) { |
| // Compress and checksum the payload. |
| Compressor compressor; |
| 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); |
| } |
| }; |
| |
| bool ImportFile(const FileContents& file, const char* filename, |
| std::list<std::unique_ptr<Item>>* items) { |
| if (file.exact_size() <= (sizeof(bootdata_t) * 2)) { |
| return false; |
| } |
| const bootdata_t* header = static_cast<const bootdata_t*>( |
| file.View(0, sizeof(bootdata_t)).iov_base); |
| if (!(header->type == BOOTDATA_CONTAINER && |
| header->extra == BOOTDATA_MAGIC && |
| header->magic == BOOTITEM_MAGIC)) { |
| return false; |
| } |
| size_t file_size = file.exact_size() - sizeof(bootdata_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, "bootdata misaligned\n"); |
| exit(1); |
| } |
| uint32_t pos = sizeof(bootdata_t); |
| do { |
| auto item = Item::CreateFromItem(file, pos); |
| pos += item->TotalSize(); |
| items->push_back(std::move(item)); |
| } while (pos < file.exact_size()); |
| return true; |
| } |
| |
| // Returns nullptr if complete, else an explanatory string. |
| const char* IncompleteImage(const std::list<std::unique_ptr<Item>>& items) { |
| if (items.front()->type() != BOOTDATA_KERNEL) { |
| return "first item not KERNEL"; |
| } |
| if (std::none_of(items.begin(), items.end(), |
| [](const std::unique_ptr<Item>& item) { |
| return item->type() == BOOTDATA_BOOTFS_BOOT; |
| })) { |
| return "no /boot BOOTFS item"; |
| } |
| return nullptr; |
| } |
| |
| constexpr const char kOptString[] = "-ho:d:T:g:tBcuC:"; |
| constexpr const option kLongOpts[] = { |
| {"help", no_argument, nullptr, 'h'}, |
| {"output", required_argument, nullptr, 'o'}, |
| {"depfile", required_argument, nullptr, 'd'}, |
| {"target", required_argument, nullptr, 'T'}, |
| {"groups", required_argument, nullptr, 'g'}, |
| {"list", no_argument, nullptr, 't'}, |
| {"complete", no_argument, nullptr, 'B'}, |
| {"compressed", no_argument, nullptr, 'c'}, |
| {"uncompressed", no_argument, nullptr, 'u'}, |
| {"cmdline", required_argument, nullptr, 'C'}, |
| {nullptr, no_argument, nullptr, 0}, |
| }; |
| |
| void usage(const char* progname) { |
| fprintf(stderr, "\ |
| Usage: %s {--output=FILE | -o FILE} [--depfile=FILE | -d FILE] ...\n\ |
| %s {--list | -t} ...\n\ |
| \n\ |
| Remaining arguments are interpersed switches and input files:\n\ |
| --help, -h print this message\n\ |
| --output=FILE, -o FILE output file name\n\ |
| --depfile=FILE, -d FILE makefile dependency output file name\n\ |
| --list, -t list input BOOTDATA item headers\n\ |
| --complete, -B verify result is a complete boot image\n\ |
| --groups=GROUPS, -g GROUPS comma-separated list of manifest groups\n\ |
| --compressed, -c compress BOOTFS/RAMDISK images (default)\n\ |
| --uncompressed, -u do not compress BOOTFS/RAMDISK images\n\ |
| --target=boot, -T boot BOOTFS to be unpacked at /boot (default)\n\ |
| --target=system, -T system BOOTFS to be unpacked at /system\n\ |
| --target=ramdisk, -T ramdisk input files are raw RAMDISK images\n\ |
| --target=zbi, -T zbi input files must be BOOTDATA files\n\ |
| @DIRECTORY populate BOOTFS from DIRECTORY\n\ |
| FILE read BOOTDATA file or BOOTFS manifest\n\ |
| \n\ |
| Each manifest or directory populates a distinct BOOTFS item, tagged for\n\ |
| unpacking based on the most recent `--target` switch. Files with\n\ |
| BOOTDATA_CONTAINER headers are incomplete boot files; others are manifests.\n\ |
| ", |
| progname, progname); |
| } |
| |
| } // anonymous namespace |
| |
| int main(int argc, char** argv) { |
| FileOpener opener; |
| GroupFilter filter; |
| const char* outfile = nullptr; |
| const char* depfile = nullptr; |
| uint32_t target = BOOTDATA_BOOTFS_BOOT; |
| bool compressed = true; |
| bool list_contents = false; |
| bool verify_complete = false; |
| std::list<std::unique_ptr<Item>> items; |
| |
| int opt; |
| while ((opt = getopt_long(argc, argv, |
| kOptString, kLongOpts, nullptr)) != -1) { |
| // A non-option argument (1) is an input, handled below. |
| // All other cases continue the loop and don't break the switch. |
| switch (opt) { |
| case 1: |
| break; |
| |
| case 'o': |
| if (outfile) { |
| fprintf(stderr, "only one output file\n"); |
| exit(1); |
| } |
| if (!items.empty()) { |
| fprintf(stderr, "--output or -o must precede inputs\n"); |
| exit(1); |
| } |
| outfile = optarg; |
| continue; |
| |
| case 'd': |
| if (depfile) { |
| fprintf(stderr, "only one depfile\n"); |
| exit(1); |
| } |
| if (!outfile) { |
| fprintf(stderr, |
| "--output -or -o must precede --depfile or -d\n"); |
| exit(1); |
| } |
| if (!items.empty()) { |
| fprintf(stderr, "--depfile or -d must precede inputs\n"); |
| exit(1); |
| } |
| opener.Init(outfile, depfile); |
| continue; |
| |
| case 'R': |
| target = BOOTDATA_RAMDISK; |
| continue; |
| |
| case 'T': |
| if (!strcmp(optarg, "boot")) { |
| target = BOOTDATA_BOOTFS_BOOT; |
| } else if (!strcmp(optarg, "system")) { |
| target = BOOTDATA_BOOTFS_SYSTEM; |
| } else if (!strcmp(optarg, "ramdisk")) { |
| target = BOOTDATA_RAMDISK; |
| } else if (!strcmp(optarg, "zbi")) { |
| target = BOOTDATA_CONTAINER; |
| } else { |
| fprintf(stderr, "\ |
| --target requires boot, system, ramdisk, or zbi\n"); |
| exit(1); |
| } |
| continue; |
| |
| case 'g': |
| filter.SetFilter(optarg); |
| continue; |
| |
| case 't': |
| list_contents = true; |
| continue; |
| |
| case 'B': |
| verify_complete = true; |
| continue; |
| |
| case 'c': |
| compressed = true; |
| continue; |
| |
| case 'u': |
| compressed = false; |
| continue; |
| |
| case 'C': { |
| struct stat st; |
| auto fd = opener.Open(optarg, &st); |
| RequireRegularFile(st, optarg); |
| auto file = FileContents::Map(std::move(fd), st, optarg); |
| items.push_back( |
| Item::CreateFromFile(std::move(file), BOOTDATA_CMDLINE, |
| false, true)); |
| continue; |
| } |
| |
| case 'h': |
| default: |
| usage(argv[0]); |
| exit(opt == 'h' ? 0 : 1); |
| } |
| assert(opt == 1); |
| |
| if (optarg[0] == '@') { |
| if (target == BOOTDATA_RAMDISK) { |
| fprintf(stderr, |
| "%s: can't import directory to --target=ramdisk\n", |
| &optarg[1]); |
| exit(1); |
| } |
| items.push_back(Item::ImportDirectory(&opener, &optarg[1], |
| target, compressed)); |
| } else { |
| struct stat st; |
| auto fd = opener.Open(optarg, &st); |
| RequireRegularFile(st, optarg); |
| auto file = FileContents::Map(std::move(fd), st, optarg); |
| if (target == BOOTDATA_RAMDISK) { |
| // Under --target=ramdisk, any input file is a raw image. |
| items.push_back(Item::CreateFromFile( |
| std::move(file), BOOTDATA_RAMDISK, |
| compressed, false)); |
| } else if (ImportFile(file, optarg, &items)) { |
| // It's another file in BOOTDATA format. The last |
| // item will own the file buffer, so it lives until |
| // all earlier items are exhausted. |
| items.back()->OwnFile(std::move(file)); |
| } else if (target == BOOTDATA_CONTAINER) { |
| fprintf(stderr, "%s: not a Zircon Boot container\n", optarg); |
| exit(1); |
| } else { |
| // It must be a manifest file. |
| items.push_back(Item::ImportManifest( |
| &opener, std::move(file), |
| filter, target, compressed)); |
| } |
| } |
| } |
| |
| if (items.empty()) { |
| fprintf(stderr, "no inputs\n"); |
| exit(1); |
| } |
| |
| if (verify_complete) { |
| const char* incomplete = IncompleteImage(items); |
| if (incomplete) { |
| fprintf(stderr, "incomplete image: %s\n", incomplete); |
| exit(1); |
| } |
| } |
| |
| if (list_contents) { |
| if (outfile || depfile) { |
| fprintf(stderr, "no output file or depfile with --list or -t\n"); |
| exit(1); |
| } |
| const char* incomplete = IncompleteImage(items); |
| if (incomplete) { |
| printf("INCOMPLETE: %s\n", incomplete); |
| } else { |
| puts("COMPLETE: bootable image"); |
| } |
| // Contents start after the BOOTDATA_CONTAINER header. |
| uint32_t pos = sizeof(bootdata_t); |
| do { |
| const auto& item = items.front(); |
| item->Describe(pos); |
| pos += item->TotalSize(); |
| items.pop_front(); |
| } while (!items.empty()); |
| } else { |
| if (!outfile) { |
| fprintf(stderr, "no output file\n"); |
| exit(1); |
| } |
| fbl::unique_fd fd(open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0666)); |
| if (!fd) { |
| perror(outfile); |
| exit(1); |
| } |
| OutputStream out(std::move(fd)); |
| uint32_t header_start = out.PlaceHeader(); |
| uint32_t payload_start = out.WritePosition(); |
| assert(Aligned(payload_start)); |
| do { |
| // Pop an item off and stream it out. Then transfer the |
| // item's ownership to the OutputStream while it stores |
| // pointers into Item buffers in its write queue. |
| auto item = std::move(items.front()); |
| items.pop_front(); |
| item->Stream(&out); |
| out.OwnItem(std::move(item)); |
| } while (!items.empty()); |
| const bootdata_t header = { |
| BOOTDATA_CONTAINER, // type |
| out.WritePosition() - payload_start, // length |
| BOOTDATA_MAGIC, // extra |
| BOOTDATA_FLAG_V2, // flags |
| 0, // reserved0 |
| 0, // reserved1 |
| BOOTITEM_MAGIC, // magic |
| BOOTITEM_NO_CRC32, // crc32 |
| }; |
| assert(Aligned(header.length)); |
| out.PatchHeader(header, header_start); |
| } |
| |
| return 0; |
| } |