blob: 2377c182f12ac7053a795fc8ce26b55b6dea5b00 [file] [log] [blame]
// Copyright 2021 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.
#ifndef LIB_ZBITL_INTERNAL_CONTAINER_H_
#define LIB_ZBITL_INTERNAL_CONTAINER_H_
#include <inttypes.h>
#include <lib/fitx/result.h>
#include <lib/zbitl/checking.h>
#include <lib/zbitl/storage_traits.h>
#include <zircon/assert.h>
#include <functional>
#include <optional>
#include <type_traits>
#include <variant>
namespace zbitl {
// TODO(joshuaseaton): The following forward-declarations exist for friend
// statements that could otherwise be avoided; let's remove them for better
// layering hygiene.
// Forward-declared; defined below.
template <typename Storage>
class View;
// Forward-declared; defined in image.h.
template <typename Storage>
class Image;
namespace internal {
///
/// ExampleContainerTraits serves as an definitional examplar for how
/// "container traits" should be structured. Container traits provide types,
/// and static constants and methods that abstract how to parse and navigate
/// a particular container format (e.g., ZBI or BOOTFS).
///
/// An "item" is an entry within the container, which is expected to be encoded
/// by an ("item header", "payload") pair. The payload is the raw binary
/// content of the item, while the item header provides its metadata, most
/// important of which is the payload's size and its location in the container.
/// When parsing, the traits should provide a means of navigating from an item
/// header to either its payload or to the next item header.
///
/// The container is expected to have a special header at offset 0, its
/// "container header", giving metadata on the container itself, including its
/// total size. The first item header is expected to immediately follow the
/// container header.
struct ExampleContainerTraits {
/// The type of a container header, expected to be POD.
struct container_header_type {};
/// The type of an item header, expected to be POD.
struct item_header_type {};
/// The user-facing representation of an item header, which wraps the
/// format's raw item_header_type. Being a C-style struct with fields
/// possibly only relevant to a parser, the raw item header type may not be a
/// relatively useful type to expose to the user.
///
/// In practice, the wrapper either stores the `item_header_type` directly
/// or it holds a pointer into someplace owned or viewed by an associated
/// Storage object. In the latter case, i.e. when Storage represents
/// something already in memory, `item_header_wrapper` should be no larger
/// than a plain pointer.
template <typename StorageTraits>
class item_header_wrapper {
private:
using TraitsHeader = typename StorageTraits::template LocalizedReadResult<item_header_type>;
public:
/// Constructible from an item header, as it would result from a localized
/// read.
explicit item_header_wrapper(const TraitsHeader& header) {}
/// Default constructible, copyable, movable, copy-assignable, and move-
/// assignable.
item_header_wrapper() = default;
item_header_wrapper(const item_header_wrapper&) = default;
item_header_wrapper(item_header_wrapper&&) noexcept = default;
item_header_wrapper& operator=(const item_header_wrapper&) = default;
item_header_wrapper& operator=(item_header_wrapper&&) noexcept = default;
/// The header can be dereferenced as if the type were
/// `const item_header_t*` (i.e. `*header` or `header->member`).
const item_header_type& operator*() const { return std::declval<const item_header_wrapper&>(); }
const item_header_type* operator->() const { return nullptr; }
};
/// Error encapsulates errors encountered in navigating the container, either
/// those coming from the storage backend or from structural issues with the
/// container itself. ErrorTraits corresponds to the `ErrorTraits` member
/// type of a StorageTraits specialization; it serves as a template parameter
/// so that Error may be defined in terms of the associated storage error
/// type (e.g., as a member).
template <typename ErrorTraits>
struct Error {};
/// The name of the associated C++ container type. This is given as a C-style
/// string (as opposed to a std::string_view) as the constant is only meant
/// to provide context within printf() statements.
static constexpr const char* kContainerType = "zbitl::ExampleContainer";
/// The expected alignment - within the container - of an item header. Must
/// be a power of two.
static constexpr uint32_t kItemAlignment = 1;
/// Payloads are expected to be followed by padding up to a multiple of this
/// value. This quantity is unrelated to the size of the payload itself.
static constexpr uint32_t kPayloadPaddingAlignment = 1;
/// Whether the payloads lie within the container. A container format may not
/// include them properly and instead point to the data elsewhere in the
/// storage (as is the case with BOOTFS).
static constexpr bool kPayloadsAreContained = false;
/// Returns the size of a container, as it is encoded in the header. The size
/// includes that of the header. It is the responsibility of the caller to
/// validate the returned size against the actual storage capacity.
static uint32_t ContainerSize(const container_header_type& header) { return sizeof(header); }
/// Returns the exact size of an item's payload (excluding padding).
static uint32_t PayloadSize(const item_header_type& header) { return 0; }
/// Returns the offset at which a payload is to be found, given the
/// associated item header and that header's offset into the container.
static uint32_t PayloadOffset(const item_header_type& header, uint32_t item_offset) { return 0; }
/// Returns the offset of the next item header, given a current item header
/// and its offset into the container.
///
/// TODO(joshuaseaton): in general, a container header may affect navigation
static uint32_t NextItemOffset(const item_header_type& header, uint32_t item_offset) { return 0; }
/// Validates item and container headers, returning a description of the
/// failure in that event. The check is agnostic of storage capacity; for
/// example, whether any encoded lengths are sensible are left to the caller
/// to validate against the actual storage capacity.
static fitx::result<std::string_view> CheckContainerHeader(const container_header_type& header) {
return fitx::error{"unimplemented"};
}
static fitx::result<std::string_view> CheckItemHeader(const item_header_type& header) {
return fitx::error{"unimplemented"};
}
/// Converts the context of an iteration failure into an Error.
template <typename StorageTraits>
static Error<typename StorageTraits::ErrorTraits> ToError(
typename StorageTraits::storage_type& storage, //
std::string_view reason, //
/// If the error occurred within the context of a particular item, this
/// is its offset; else, for problems with the overall container, this is
/// zero.
uint32_t item_offset,
/// Offset at which the error occurred.
uint32_t error_offset,
/// If the error occurred within the context of a particular item, this
/// is a pointer to its header; else, for problems with the overall
/// container, this is nullptr. In particular, we expect `header` to be
/// null iff `item_offset` is zero. When `header` is obtained through an
/// iterator, the former's lifetime is expected to be tied to the
/// latter's.
///
/// std::optional<item_header_type> is not used here to account for any
/// flexible array members, which std::optional forbids.
const item_header_type* header = nullptr,
std::optional<typename StorageTraits::ErrorTraits::error_type> storage_error = std::nullopt) {
return {};
}
};
/// Container provides the main container business logic for iterating over,
/// error-checking, and generally inspecting supported container formats. À la
/// Curious Recurring Template Pattern, it is expected that concrete container
/// class implementations (i.e., `Derived` types) inherit from this class and
/// supply a `ContainerTraits` implementation that meets the specification of
/// `ExampleContainerTraits` above.
template <typename Derived, typename Storage, typename ContainerTraits>
class Container {
private:
using container_header_type = typename ContainerTraits::container_header_type;
using item_header_type = typename ContainerTraits::item_header_type;
public:
using storage_type = Storage;
using Traits = ExtendedStorageTraits<storage_type>;
using storage_error_type = typename Traits::ErrorTraits::error_type;
using Error = typename ContainerTraits::template Error<typename Traits::ErrorTraits>;
using item_header_wrapper = typename ContainerTraits::template item_header_wrapper<Traits>;
Container() = default;
Container(const Container&) = default;
Container& operator=(const Container&) = default;
// This is almost the same as the default move behavior. But it also
// explicitly resets the moved-from error state to kUnused so that the
// moved-from Container can be destroyed without checking it.
Container(Container&& other)
: storage_(std::move(other.storage_)), error_(std::move(other.error_)), limit_(other.limit_) {
other.error_ = Unused{};
other.limit_ = 0;
}
Container& operator=(Container&& other) {
error_ = std::move(other.error_);
other.error_ = Unused{};
storage_ = std::move(other.storage_);
limit_ = other.limit_;
other.limit_ = 0;
return *this;
}
explicit Container(storage_type storage) : storage_(std::move(storage)) {}
~Container() {
ZX_ASSERT_MSG(!std::holds_alternative<Error>(error_), "%s destroyed after error without check",
ContainerTraits::kContainerType);
ZX_ASSERT_MSG(!std::holds_alternative<NoError>(error_),
"%s destroyed after successful iteration without check",
ContainerTraits::kContainerType);
}
/// The payload type is provided by the StorageTraits specialization. It's
/// opaque to Container, but must be default-constructible, copy-
/// constructible, and copy-assignable. It's expected to have "view"-style
/// semantics, i.e. be small and not own any storage itself but only refer to
/// storage owned by the Storage object.
using payload_type = typename Traits::payload_type;
/// The element type is a trivial struct morally equivalent to
/// std::pair<item_header_wrapper, payload_type>. Both member types are
/// default-constructible, copy-constructible, and copy-assignable, so
/// value_type as a whole is as well.
struct value_type {
item_header_wrapper header;
payload_type payload;
};
/// Check the container for errors after using iterators. When begin() or
/// iterator::operator++() encounters an error, it simply returns end() so
/// that loops terminate normally. Thereafter, take_error() must be called
/// to check whether the loop terminated because it iterated past the last
/// item or because it encountered an error. Once begin() has been called,
/// take_error() must be called before the Container is destroyed, so no
/// error goes undetected. After take_error() is called the error state is
/// consumed and take_error() cannot be called again until another begin() or
/// iterator::operator++() call has been made.
[[nodiscard]] fitx::result<Error> take_error() {
ErrorState result = std::move(error_);
error_ = Taken{};
if (std::holds_alternative<Error>(result)) {
return fitx::error{std::move(std::get<Error>(result))};
}
ZX_ASSERT_MSG(!std::holds_alternative<Taken>(result), "%s::take_error() was already called",
ContainerTraits::kContainerType);
return fitx::ok();
}
/// If you explicitly don't care about any error that might have terminated
/// the last loop early, then call ignore_error() instead of take_error().
void ignore_error() { static_cast<void>(take_error()); }
/// Trivial accessors for the underlying Storage (view) object.
storage_type& storage() { return storage_; }
const storage_type& storage() const { return storage_; }
class iterator {
public:
/// The default-constructed iterator is invalid for all uses except
/// equality comparison.
iterator() = default;
iterator& operator=(const iterator&) = default;
bool operator==(const iterator& other) const {
return other.view_ == view_ && other.offset_ == offset_;
}
bool operator!=(const iterator& other) const { return !(*this == other); }
iterator& operator++() { // prefix
Assert(__func__);
view_->StartIteration();
const uint32_t next_item_offset = ContainerTraits::NextItemOffset(*value_.header, offset_);
Update(next_item_offset);
return *this;
}
iterator operator++(int) { // postfix
iterator old = *this;
++*this;
return old;
}
const Container::value_type& operator*() const {
Assert(__func__);
return value_;
}
const Container::value_type* operator->() const {
Assert(__func__);
return &value_;
}
uint32_t item_offset() const { return offset_; }
uint32_t payload_offset() const {
Assert(__func__);
return ContainerTraits::PayloadOffset(*(value_.header), offset_);
}
Derived& view() const {
ZX_ASSERT_MSG(view_, "%s on default-constructed %s::iterator", __func__,
ContainerTraits::kContainerType);
return *static_cast<Derived*>(view_);
}
// Iterator traits.
using iterator_category = std::input_iterator_tag;
using reference = Container::value_type&;
using value_type = Container::value_type;
using pointer = Container::value_type*;
using difference_type = size_t;
private:
// Assert accessed by View<StorageType>::EditHeader().
template <typename StorageType>
friend class ::zbitl::View;
// Private fields accessed by Image<StorageType>::Append().
template <typename StorageType>
friend class ::zbitl::Image;
// The default-constructed state is almost the same as the end() state:
// nothing but operator==() should ever be called if view_ is nullptr.
Container* view_ = nullptr;
// The offset into the ZBI of the current item's header. This is 0 in
// default-constructed iterators and kEnd_ in end() iterators, where
// operator*() can never be called. A valid non-end() iterator holds the
// header and payload (references) of the current item for operator*() to
// return. If offset_ is at the end of the container, then operator++()
// will yield end().
uint32_t offset_ = 0;
// end() uses a different offset_ value to distinguish a true end iterator
// from a particular view from a default-constructed iterator from nowhere.
static constexpr uint32_t kEnd_ = std::numeric_limits<uint32_t>::max();
// This is left uninitialized until a successful increment sets it.
// It is only examined by a dereference, which is invalid without
// a successful increment.
value_type value_{};
// This is called only by begin() and end().
friend class Container;
iterator(Container* view, bool is_end) : view_(view) {
ZX_DEBUG_ASSERT(view_);
if (is_end) {
offset_ = kEnd_;
} else {
Update(sizeof(container_header_type));
}
}
// Updates the state of the iterator to reflect a new offset.
void Update(uint32_t next_item_offset) {
ZX_DEBUG_ASSERT(next_item_offset >= sizeof(container_header_type));
ZX_DEBUG_ASSERT_MSG(next_item_offset <= view_->limit_,
"%s::iterator next_item_offset %#" PRIx32 " > limit_ %#" PRIx32,
ContainerTraits::kContainerType, next_item_offset, view_->limit_);
ZX_DEBUG_ASSERT(next_item_offset % ContainerTraits::kItemAlignment == 0);
if (next_item_offset == view_->limit_) {
// Reached the end.
*this = view_->end();
return;
}
if (view_->limit_ < next_item_offset ||
view_->limit_ - next_item_offset < sizeof(item_header_type)) {
Fail("container too short for next item header");
return;
}
if (auto header = view_->ItemHeader(next_item_offset); header.is_error()) {
// Failed to read the next header.
Fail("cannot read item header", std::move(header.error_value()));
return;
} else if (auto header_error = ContainerTraits::CheckItemHeader(header.value());
header_error.is_error()) {
Fail(header_error.error_value());
return;
} else {
value_.header = item_header_wrapper(header.value());
}
// If payloads lie within the container, we validate that this particular
// payload does indeed fit within; else, we can only check that it fits
// within the storage itself.
uint32_t payload_limit = view_->limit_;
if constexpr (!ContainerTraits::kPayloadsAreContained) {
auto result = Traits::Capacity(view_->storage());
if (result.is_error()) {
Fail("cannot determine storage capacity", std::move(result).error_value(), 0);
return;
}
payload_limit = std::move(result).value();
}
const uint32_t payload_offset =
ContainerTraits::PayloadOffset(*value_.header, next_item_offset);
const uint32_t payload_size = ContainerTraits::PayloadSize(*value_.header);
const uint32_t padded_payload_size =
(payload_size + ContainerTraits::kPayloadPaddingAlignment - 1) &
-ContainerTraits::kPayloadPaddingAlignment;
if (payload_offset > payload_limit ||
padded_payload_size < payload_size || // ensure aligned size didn't overflow
padded_payload_size > payload_limit - payload_offset) {
if constexpr (ContainerTraits::kPayloadsAreContained) {
Fail("container too short for next item payload");
} else {
Fail("storage too small for next item payload");
}
return;
}
if (auto payload = Traits::Payload(view_->storage(), payload_offset, payload_size);
payload.is_error()) {
Fail("cannot extract payload view", std::move(payload.error_value()), payload_offset);
return;
} else {
value_.payload = std::move(payload.value());
}
offset_ = next_item_offset;
}
void Fail(std::string_view sv, std::optional<storage_error_type> storage_error = std::nullopt,
std::optional<uint32_t> error_offset = std::nullopt) {
view_->Fail(ContainerTraits::template ToError<Traits>(
view_->storage(), sv, offset_, error_offset.value_or(offset_), &(*value_.header),
std::move(storage_error)));
*this = view_->end();
}
void Assert(const char* func) const {
ZX_ASSERT_MSG(view_, "%s on default-constructed %s::iterator", func,
ContainerTraits::kContainerType);
ZX_ASSERT_MSG(offset_ != kEnd_, "%s on %s::end() iterator", func,
ContainerTraits::kContainerType);
}
};
// This returns its own error state and does not affect the `take_error()`
// state of the Container.
fitx::result<Error, container_header_type> container_header() {
auto to_error = [this](
std::string_view reason, uint32_t error_offset = 0,
std::optional<storage_error_type> storage_error = std::nullopt) -> Error {
return ContainerTraits::template ToError<Traits>(storage(), reason, 0, error_offset, nullptr,
std::move(storage_error));
};
auto capacity_error = Traits::Capacity(storage());
if (capacity_error.is_error()) {
return fitx::error{to_error("cannot determine storage capacity", 0,
std::move(capacity_error).error_value())};
}
uint32_t capacity = capacity_error.value();
// Minimal bounds check before trying to read.
if (capacity < sizeof(container_header_type)) {
return fitx::error(to_error("container header doesn't fit. Truncated?", capacity));
}
// Read and validate the container header.
auto header_error = ContainerHeader();
if (header_error.is_error()) {
// Failed to read the container header.
return fitx::error{
to_error("cannot read container header", 0, std::move(header_error).error_value())};
}
container_header_type header = std::move(header_error).value();
auto check_error = ContainerTraits::CheckContainerHeader(header);
if (check_error.is_error()) {
return fitx::error{to_error(check_error.error_value())};
}
const uint32_t size = ContainerTraits::ContainerSize(header);
if (size < sizeof(header) || size > capacity) {
return fitx::error{to_error("container doesn't fit. Truncated?")};
}
return fitx::ok(header);
}
/// After calling begin(), it's mandatory to call take_error() before
/// destroying the Container object. An iteration that encounters an error
/// will simply end early, i.e. begin() or operator++() will yield an
/// iterator that equals end(). At the end of a loop, call take_error() to
/// check for errors. It's also acceptable to call take_error() during an
/// iteration that hasn't reached end() yet, but it cannot be called again
/// before the next begin() or operator++() call.
iterator begin() {
StartIteration();
auto header = container_header();
if (header.is_error()) {
Fail(header.error_value());
limit_ = 0; // Reset from past uses.
return end();
}
// The container's "payload" is all the items. Don't scan past it.
limit_ = ContainerTraits::ContainerSize(header.value());
return {this, false};
}
iterator end() { return {this, true}; }
size_t size_bytes() {
if (std::holds_alternative<Unused>(error_)) {
ZX_ASSERT(limit_ == 0);
// Taking the size before doing begin() takes extra work.
auto capacity_error = Traits::Capacity(storage());
if (capacity_error.is_ok()) {
uint32_t capacity = capacity_error.value();
if (capacity >= sizeof(container_header_type)) {
auto header_error = ContainerHeader();
if (header_error.is_ok()) {
container_header_type header = std::move(header_error).value();
const uint32_t size = ContainerTraits::ContainerSize(header);
if (sizeof(header) <= size && size <= capacity) {
return size;
}
}
}
}
}
return limit_;
}
protected:
// Fetches the container header.
fitx::result<storage_error_type,
typename Traits::template LocalizedReadResult<container_header_type>>
ContainerHeader() {
return Traits::template LocalizedRead<container_header_type>(storage(), 0);
}
// Fetches an item header at a given offset.
fitx::result<storage_error_type, typename Traits::template LocalizedReadResult<item_header_type>>
ItemHeader(uint32_t offset) {
return Traits::template LocalizedRead<item_header_type>(storage(), offset);
}
// Returns [offset, length] in the storage to cover the given item range.
auto RangeBounds(const iterator& first, const iterator& last) {
uint32_t offset = first.item_offset();
uint32_t limit = limit_;
if (last != end()) {
limit = last.item_offset();
}
return std::make_pair(offset, limit - offset);
}
private:
struct Unused {};
struct NoError {};
struct Taken {};
using ErrorState = std::variant<Unused, NoError, Error, Taken>;
void StartIteration() {
ZX_ASSERT_MSG(!std::holds_alternative<Error>(error_),
"%s iterators used without taking prior error", ContainerTraits::kContainerType);
error_ = NoError{};
}
void Fail(Error error) {
ZX_DEBUG_ASSERT_MSG(!std::holds_alternative<Error>(error_),
"Fail in error state: missing %s::StartIteration() call?",
ContainerTraits::kContainerType);
ZX_DEBUG_ASSERT_MSG(!std::holds_alternative<Unused>(error_),
"Fail in Unused: missing %s::StartIteration() call?",
ContainerTraits::kContainerType);
error_ = std::move(error);
}
storage_type storage_;
ErrorState error_;
uint32_t limit_ = 0;
};
} // namespace internal
} // namespace zbitl
#endif // LIB_ZBITL_INTERNAL_CONTAINER_H_