blob: 78d28df5c30a2c734c85f0012127c868f4d883a3 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef LIB_ZBITL_VIEW_H_
#define LIB_ZBITL_VIEW_H_
#include <lib/fitx/result.h>
#include <zircon/assert.h>
#include <zircon/boot/image.h>
#include <functional>
#include <type_traits>
#include <variant>
#include "checking.h"
#include "storage_traits.h"
namespace zbitl {
/// The zbitl::View template class provides an error-checking container view of
/// a ZBI. It satisfies the C++20 std::forward_range concept; it satisfies the
/// std::view concept if the Storage and Storage::error_type types support
/// constant-time copy/move/assignment.
///
/// The "error-checking view" pattern means that the container/range/view API
/// of begin() and end() iterators is supported, but 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 View is destroyed, so no error goes
/// undetected. Since all use of iterators updates the error state, use of any
/// zbitl::View object must be serialized and after begin() or operator++()
/// yields end(), take_error() must be checked before using begin() again.
///
/// Each time begin() is called the underlying storage is examined afresh, so
/// it's safe to reuse a zbitl::View object after changing the data. Reducing
/// the size of the underlying storage invalidates any iterators that pointed
/// past the new end of the image. It's simplest just to assume that changing
/// the underlying storage always invalidates all iterators.
///
/// The Storage type is some type that can be abstractly considered to have
/// non-owning "view" semantics: it doesn't hold the storage of the ZBI, it
/// just refers to it somehow. The zbitl::View:Error type describes errors
/// encountered while iterating. It uses the Storage::error_type type to
/// propagate errors caused by access to the underlying storage.
///
/// Usually Storage and Storage:error_type types are small and can be copied.
/// zbitl::View is move-only if Storage is move-only or if Storage::error_type
/// is move-only. Note that copying zbitl::View copies its error-checking
/// state exactly, so if the original View needed to be checked for errors
/// before destruction then both the original and the copy need to be checked
/// before their respective destructions. A moved-from zbitl::View can always
/// be destroyed without checking.
template <typename Storage, Checking Check = Checking::kStrict>
class View {
public:
using storage_type = Storage;
View() = default;
View(const View&) = default;
View& operator=(const View&) = 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 View can be destroyed without checking it.
View(View&& other)
: error_(std::move(other.error_)), storage_(std::move(other.storage_)), limit_(other.limit_) {
other.error_ = Unused{};
other.limit_ = 0;
}
View& operator=(View&& other) {
error_ = std::move(other.error_);
other.error_ = Unused{};
storage_ = std::move(other.storage_);
limit_ = other.limit_;
other.limit_ = 0;
return *this;
}
explicit View(storage_type storage) : storage_(std::move(storage)) {}
~View() {
ZX_ASSERT_MSG(!std::holds_alternative<Error>(error_),
"zbitl::View destroyed after error without check");
ZX_ASSERT_MSG(!std::holds_alternative<NoError>(error_),
"zbtil::View destroyed after successful iteration without check");
}
/// The API details of what Storage means are delegated to the StorageTraits
/// template; see <lib/zbitl/storage_traits.h>.
using Traits = StorageTraits<Storage>;
static_assert(std::is_default_constructible_v<typename Traits::error_type>);
static_assert(std::is_copy_constructible_v<typename Traits::error_type>);
static_assert(std::is_copy_assignable_v<typename Traits::error_type>);
static_assert(std::is_default_constructible_v<typename Traits::payload_type>);
static_assert(std::is_copy_constructible_v<typename Traits::payload_type>);
static_assert(std::is_copy_assignable_v<typename Traits::payload_type>);
/// The header is represented by an opaque type that can be dereferenced as
/// if it were `const zbi_header_t*`, i.e. `*header` or `header->member`.
/// Either it stores the `zbi_header_t` directly or it holds a pointer into
/// someplace owned or viewed by the Storage object. In the latter case,
/// i.e. when Storage represents something already in memory, `header_type`
/// should be no larger than a plain pointer.
class header_type {
public:
header_type() = default;
header_type(const header_type&) = default;
header_type(header_type&&) = default;
header_type& operator=(const header_type&) = default;
header_type& operator=(header_type&&) = default;
/// `*header` always copies, so lifetime of `this` doesn't matter.
zbi_header_t operator*() const {
if constexpr (kCopy) {
return stored_;
} else {
return *stored_;
}
}
/// `header->member` refers to the header in place, so never do
/// `&header->member` but always dereference a member directly.
const zbi_header_t* operator->() const {
if constexpr (kCopy) {
return &stored_;
} else {
return stored_;
}
}
private:
using TraitsHeader = decltype(Traits::Header(std::declval<Storage&>(), 0));
static constexpr bool kCopy =
std::is_same_v<TraitsHeader, fitx::result<typename Traits::error_type, zbi_header_t>>;
static constexpr bool kReference =
std::is_same_v<TraitsHeader, fitx::result<typename Traits::error_type,
std::reference_wrapper<const zbi_header_t>>>;
static_assert(kCopy || kReference,
"zbitl::StorageTraits specialization's Header function returns wrong type");
friend View;
using HeaderStorage = std::conditional_t<kCopy, zbi_header_t, const zbi_header_t*>;
HeaderStorage stored_;
// This can only be used by begin(), below.
template <typename T>
explicit header_type(const T& header)
: stored_([&header]() {
if constexpr (kCopy) {
static_assert(std::is_same_v<zbi_header_t, T>);
return header;
} else {
static_assert(std::is_same_v<std::reference_wrapper<const zbi_header_t>, T>);
return &(header.get());
}
}()) {}
};
/// The payload type is provided by the StorageTraits specialization. It's
/// opaque to View, 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<header_type, 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 {
header_type header;
payload_type payload;
};
/// The Error type is returned by take_error() after begin() or an iterator
/// operator encountered an error. There is always a string description of
/// the error. Errors arising from Storage access also provide an error
/// value defined via StorageTraits; see <lib/zbitl/storage_traits.h>.
struct Error {
/// A string constant describing the error.
std::string_view zbi_error{};
/// This is the offset into the ZBI of the item (header) at fault. This is
/// zero for problems with the overall container, which begin() detects.
/// In iterator operations, it refers to the offset into the image where
/// the item was (or should have been).
uint32_t item_offset = 0;
/// This reflects the underlying error from accessing the Storage object,
/// if any. If storage_error.is_error() is false, then the error is in the
/// format of the contents of the ZBI, not in accessing the contents.
fitx::result<typename Traits::error_type> storage_error{};
};
/// 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 View 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() {
decltype(error_) 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),
"zbitl::View::take_error() was already called");
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 accessor for the underlying Storage (view) object.
storage_type& storage() { 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();
ZX_DEBUG_ASSERT(offset_ >= sizeof(zbi_header_t));
ZX_DEBUG_ASSERT(offset_ <= view_->limit_);
ZX_DEBUG_ASSERT(offset_ % ZBI_ALIGNMENT == 0);
if (view_->limit_ - offset_ < sizeof(zbi_header_t)) {
// Reached the end of the container.
if constexpr (Check != Checking::kPermissive) {
if (offset_ != view_->limit_) {
Fail("container too short for next item header");
}
}
*this = view_->end();
} else if (auto header = Traits::Header(view_->storage(), offset_); header.is_error()) {
// Failed to read the next header.
Fail("cannot read item header", fitx::error{std::move(header.error_value())});
} else if (auto header_error = CheckHeader<Check>(header.value(), view_->limit_ - offset_);
header_error.is_error()) {
Fail(header_error.error_value());
} else {
header_ = header_type(header.value());
offset_ += static_cast<uint32_t>(sizeof(zbi_header_t));
if (auto payload = Traits::Payload(view_->storage(), offset_, header_->length);
payload.is_error()) {
Fail("cannot extract payload view", fitx::error{std::move(payload.error_value())});
} else {
offset_ += ZBI_ALIGN(header_->length);
payload_ = std::move(payload.value());
if constexpr (Check == Checking::kCrc) {
if (auto crc = Traits::Crc32(view_->storage(), offset_, header_->length);
crc.is_error()) {
Fail("cannot compute payload CRC32", fitx::error{std::move(crc.error_value())});
}
}
}
}
return *this;
}
iterator operator++(int) { // postfix
iterator old = *this;
++*this;
return old;
}
value_type operator*() const {
Assert(__func__);
return {header_, payload_};
}
uint32_t item_offset() const {
return payload_offset() - static_cast<uint32_t>(sizeof(zbi_header_t));
}
uint32_t payload_offset() const {
Assert(__func__);
return offset_ - ZBI_ALIGN(header_->length);
}
// Replace this item's header with a new one. This never changes the
// existing item's length (nor its payload), and always writes a header
// that passes Checking::kStrict. So the header can be `{.type = XYZ}`
// alone or whatever fields and flags matter. Note this returns only the
// storage error type, not an Error since no ZBI format errors are possible
// here, only a storage failure to update.
//
// This method is not available if zbitl::StorageTraits<storage_type>
// doesn't support mutation.
template <typename T = Traits, typename = std::void_t<decltype(T::Write(
std::declval<storage_type&>(), 0, AsBytes(nullptr, 0)))>>
fitx::result<typename Traits::error_type> Replace(zbi_header_t header) {
Assert(__func__);
header = SanitizeHeader(header);
header.length = header_->length;
auto result = Traits::Write(view_->storage(), item_offset(), AsBytes(header));
// Make the next operator*() consistent with the new header if it worked.
// For kReference storage types, the change is reflected intrinsically.
if constexpr (header_type::kCopy) {
if (result.is_ok()) {
header_.stored_ = header;
}
}
return result;
}
private:
// The default-constructed state is almost the same as the end() state:
// nothing but operator==() should ever be called if view_ is nullptr.
View* view_ = nullptr;
// The offset into the ZBI of the next 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, and its offset_ always looks past to the horizon. If offset_ as
// 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();
// These are left uninitialized until a successful increment sets them.
// They are only examined by a dereference, which is invalid without
// a successful increment.
header_type header_{};
payload_type payload_;
// This is called only by begin() and end().
friend class View;
iterator(View* view, bool is_end)
: view_(view), offset_(is_end ? kEnd_ : sizeof(zbi_header_t)) {
ZX_DEBUG_ASSERT(view_);
if (!is_end) {
// The initial offset_ points past the container header, to the first
// item. The first increment reaches end() or makes the iterator valid.
++*this;
}
}
void Fail(std::string_view sv,
fitx::result<typename Traits::error_type> storage_error = fitx::ok()) {
view_->Fail({sv, offset_, std::move(storage_error)});
*this = view_->end();
}
void Assert(const char* func) const {
ZX_ASSERT_MSG(view_, "%s on default-constructed zbitl::View::iterator", func);
ZX_ASSERT_MSG(offset_ != kEnd_, "%s on zbitl::View::end() iterator", func);
}
};
// This returns its own error state and does not affect the `take_error()`
// state of the View.
fitx::result<Error, zbi_header_t> container_header() {
auto capacity_error = Traits::Capacity(storage());
if (capacity_error.is_error()) {
return fitx::error(Error{"cannot determine storage capacity", 0,
fitx::error{std::move(capacity_error.error_value())}});
}
uint32_t capacity = capacity_error.value();
// Minimal bounds check before trying to read.
if (capacity < sizeof(zbi_header_t)) {
return fitx::error(
Error{"storage capacity too small for ZBI container header", capacity, fitx::ok()});
}
// Read and validate the container header.
auto header_error = Traits::Header(storage(), 0);
if (header_error.is_error()) {
// Failed to read the container header.
return fitx::error(Error{"cannot read container header", 0,
fitx::error{std::move(header_error.error_value())}});
}
const header_type header(std::move(header_error.value()));
auto check_error = CheckHeader<Check>(*header, capacity);
if (check_error.is_error()) {
return fitx::error(Error{check_error.error_value(), 0, fitx::ok()});
}
if constexpr (Check != Checking::kPermissive) {
if (header->flags & ZBI_FLAG_CRC32) {
return fitx::error(Error{"container header has CRC32 flag", 0, fitx::ok()});
}
}
if (header->length % ZBI_ALIGNMENT != 0) {
return fitx::error(Error{"container header has misaligned length", 0, fitx::ok()});
}
return fitx::ok(*header);
}
/// After calling begin(), it's mandatory to call take_error() before
/// destroying the View 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_ = static_cast<uint32_t>(sizeof(zbi_header_t) + header->length);
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(zbi_header_t)) {
auto header_error = Traits::Header(storage(), 0);
if (header_error.is_ok()) {
const header_type header(header_error.value());
if (header->length <= capacity - sizeof(zbi_header_t)) {
return sizeof(zbi_header_t) + header->length;
}
}
}
}
}
return limit_;
}
private:
struct Unused {};
struct NoError {};
struct Taken {};
enum ErrorState {
kUnused,
kOk,
kTaken,
};
std::variant<Unused, NoError, Error, Taken> error_;
storage_type storage_;
uint32_t limit_ = 0;
void StartIteration() {
ZX_ASSERT_MSG(!std::holds_alternative<Error>(error_),
"zbitl:View iterators used without taking prior error");
error_ = NoError{};
}
void Fail(Error error) {
ZX_DEBUG_ASSERT_MSG(!std::holds_alternative<Error>(error_),
"Fail in error state: missing zbitl::View::StartIteration() call?");
ZX_DEBUG_ASSERT_MSG(!std::holds_alternative<Unused>(error_),
"Fail in Unused: missing zbitl::View::StartIteration() call?");
error_ = std::move(error);
}
};
// Deduction guide: View v(T{}) instantiates View<T>.
template <typename Storage>
explicit View(Storage) -> View<Storage>;
// A shorthand for permissive checking.
template <typename Storage>
using PermissiveView = View<Storage, Checking::kPermissive>;
// A shorthand for CRC checking.
template <typename Storage>
using CrcCheckingView = View<Storage, Checking::kCrc>;
} // namespace zbitl
#endif // LIB_ZBITL_VIEW_H_