blob: 5bb072b29aaa7d40e63dee8cc13aa01602d17993 [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_STORAGE_TRAITS_H_
#define LIB_ZBITL_STORAGE_TRAITS_H_
#include <lib/fitx/result.h>
#include <zircon/assert.h>
#include <zircon/boot/image.h>
#include <cstdint>
#include <cstring>
#include <functional>
#include <limits>
#include <optional>
#include <string_view>
#include <type_traits>
#include <version>
#if __cpp_lib_span
#include <span>
#endif
namespace zbitl {
using ByteView = std::basic_string_view<std::byte>;
inline ByteView AsBytes(const void* ptr, size_t len) {
return {reinterpret_cast<const std::byte*>(ptr), len};
}
template <typename T>
inline ByteView AsBytes(const T& payload) {
static_assert(std::has_unique_object_representations_v<T>);
return AsBytes(&payload, sizeof(payload));
}
/// The zbitl::StorageTraits template must be specialized for each type used as
/// the Storage type parameter to zbitl::View (see <lib/zbitl/view.h). The
/// generic template can only be instantiated with `std::tuple<>` as the
/// Storage type. This is a stub implementation that always fails with an
/// empty error_type. It also serves to document the API for StorageTraits
/// specializations.
///
template <typename Storage>
struct StorageTraits {
static_assert(std::is_same_v<std::tuple<>, Storage>, "missing StorageTraits specialization");
/// This represents an error accessing the storage, either to read a header
/// or to access a payload.
struct error_type {};
/// This represents an item payload (does not include the header). The
/// corresponding zbi_header_t.length gives its size. This type is wholly
/// opaque to zbitl::View but must be copyable. It might be something as
/// simple as the offset into the whole ZBI, or for in-memory Storage types a
/// std::span pointing to the contents.
struct payload_type {};
/// This method is expected to return a type convertible to std::string_view
/// (e.g., std::string or const char*) representing the message associated to
/// a given error value. The returned object is "owning" and so it is
/// expected that the caller keep the returned object alive for as long as
/// they use any string_view converted from it.
static std::string_view error_string(error_type error) { return {}; }
/// This returns the upper bound on available space where the ZBI is stored.
/// The container must fit within this maximum. Storage past the container's
/// self-encoded size need not be accessible and will never be accessed.
/// If the actual upper bound is unknown, this can safely return UINT32_MAX.
static fitx::result<error_type, uint32_t> Capacity(Storage& zbi) {
return fitx::error<error_type>{};
}
/// A specialization must define this if it also defines Write. This method
/// ensures that the capacity is at least that of the provided value
/// (possibly larger), for specializations where such an operation is
/// sensible.
static fitx::result<error_type> EnsureCapacity(Storage& zbi, uint32_t capacity) {
return fitx::error<error_type>{};
}
/// This fetches the item (or container) header at the given offset. The
/// return type can use either plain `zbi_header_t` or it can use
/// `std::reference_wrapper<const zbi_header_t>`. The former case is for
/// remote access to storage, where fetching the header has to copy it. The
/// latter case is for in-memory storage, where the header can just be
/// accessed in place via a direct pointer.
static fitx::result<error_type, zbi_header_t> Header(Storage& zbi, uint32_t offset) {
return fitx::error<error_type>{};
}
/// This fetches the item payload view object, whatever that means for this
/// Storage type. This is not expected to read the contents, just transfer a
/// pointer or offset around so they can be explicitly read later.
static fitx::result<error_type, payload_type> Payload(Storage& zbi, uint32_t offset,
uint32_t length) {
return fitx::error<error_type>{};
}
/// Referred to as the "buffered read".
///
/// This reads the payload indicated by a payload_type as returned by Payload
/// and feeds it to the callback in chunks sized for the convenience of the
/// storage backend. The length is guaranteed to match that passed to
/// Payload to fetch this payload_type value.
///
/// The callback returns some type fitx::result<E>. Read returns
/// fitx::result<error_type, fitx::result<E>>>, yielding a storage error or
/// the result of the callback. If a callback returns an error, its return
/// value is used immediately. If a callback returns success, another
/// callback may be made for another chunk of the payload. If the payload is
/// empty (`length` == 0), there will always be a single callback made with
/// an empty data argument.
template <typename Callback>
static auto Read(Storage& zbi, payload_type payload, uint32_t length, Callback&& callback)
-> fitx::result<error_type, decltype(callback(ByteView{}))> {
return fitx::error<error_type>{};
}
// Referred to as the "unbuffered read".
//
// A specialization provides this overload if the payload can be read
// directly into a provided buffer for zero-copy operation.
static fitx::result<error_type> Read(Storage& zbi, payload_type payload, void* buffer,
uint32_t length) {
return fitx::error<error_type>{};
}
// Referred to as the "one-shot read".
//
// A specialization only provides this overload if the payload can be
// accessed directly in memory. If this overload is provided, then the other
// overloads need not be provided. The returned view is only guaranteed
// valid until the next use of the same Storage object. So it could
// e.g. point into a cache that's repurposed by this or other calls made
// later using the same object.
static fitx::result<error_type, ByteView> Read(Storage& zbi, payload_type payload,
uint32_t length) {
return fitx::error<error_type>{};
}
// Referred to as the "buffered write".
//
// A specialization defines this only if it supports mutation. It might be
// called to write whole or partial headers and/or payloads, but it will
// never be called with an offset and size that would exceed the capacity
// previously reported by Capacity (above). It returns success only if it
// wrote the whole chunk specified. If it returns an error, any subset of
// the chunk that failed to write might be corrupted in the image and the
// container will always revalidate everything.
static fitx::result<error_type> Write(Storage& zbi, uint32_t offset, ByteView data) {
return fitx::error<error_type>{};
}
// Referred to as the "unbuffered write".
//
// A specialization may define this if it also defines Write. It returns a
// pointer where the data can be mutated directly in memory. That pointer is
// only guaranteed valid until the next use of the same Storage object. So
// it could e.g. point into a cache that's repurposed by this or other calls
// made later using the same object.
static fitx::result<error_type, void*> Write(Storage& zbi, uint32_t offset, uint32_t length) {
return fitx::error<error_type>{};
}
// A specialization defines this only if it supports mutation and if creating
// new storage from whole cloth makes sense for the storage type somehow.
// Its successful return value is whatever makes sense for returning a new,
// owning object of a type akin to Storage (possibly Storage itself, possibly
// another type). The new object refers to new storage of at least the given
// capacity (in bytes) with a provided zero-fill header size. The old
// storage object might be used as a prototype in some sense, but the new
// object is distinct storage.
static fitx::result<error_type, Storage> Create(Storage& zbi, uint32_t capacity,
uint32_t initial_zero_size) {
return fitx::error<error_type>{};
}
// A specialization defines this only if it defines Create, and if Clone adds
// any value. The new object is new storage that doesn't mutate the original
// storage, whose capacity is at least `to_offset + length`, and whose
// contents are the subrange of the original storage starting at `offset`,
// with zero-fill from the beginning of the storage up to `to_offset` bytes.
// The successful return value is `std::optional<std::pair<T, uint32_t>>`
// where T is what a successful Create call returns and the uint32_t is the
// actual offset into the new storage, aka the "slop" (see below). If this
// doesn't have something more efficient to do than just allocating storage
// space for and copying all `length` bytes of data (using Create and Write),
// then it can just return std::nullopt. If the method would *always* return
// std::nullopt then it can just be omitted entirely. The "slop" refers to
// some number of bytes at the beginning of the storage that will read as
// zero before the requested range of the original storage begins. The
// storage backend will endeavor to make this match `to_offset`, but might
// deliver a different result due to factors like page-rounding. The
// `slopcheck` parameter is a `(uint32_t) -> bool` predicate function object
// that says whether a given byte count is acceptable as slop for this clone.
// If `slopcheck(slop)` returns false, Clone *must* return std::nullopt
// rather than yielding storage with a rejected slop byte count.
template <typename SlopCheck>
static fitx::result<error_type, std::optional<std::pair<Storage, uint32_t>>> Clone(
Storage& zbi, uint32_t offset, uint32_t length, uint32_t to_offset, SlopCheck&& slopcheck) {
static_assert(std::is_invocable_r_v<bool, SlopCheck, uint32_t>);
return fitx::error<error_type>{};
}
};
// The first chunk StorageTraits<...>::Read passes to its callback must be at
// least as long as the minimum of kReadMinimum and header.length.
inline constexpr uint32_t kReadMinimum = 32;
// Specialization for std::basic_string_view<byte-size type> as Storage. Its
// payload_type is the same type as Storage, just yielding the substring of the
// original whole-ZBI string_view.
template <typename T>
struct StorageTraits<std::basic_string_view<T>> {
using Storage = std::basic_string_view<T>;
static_assert(sizeof(T) == sizeof(uint8_t));
struct error_type {};
using payload_type = Storage;
static std::string_view error_string(error_type error) { return {}; }
static fitx::result<error_type, uint32_t> Capacity(Storage& zbi) {
return fitx::ok(static_cast<uint32_t>(
std::min(zbi.size(),
static_cast<typename Storage::size_type>(std::numeric_limits<uint32_t>::max()))));
}
static fitx::result<error_type, std::reference_wrapper<const zbi_header_t>> Header(
Storage& zbi, uint32_t offset) {
return fitx::ok(std::ref(
*reinterpret_cast<const zbi_header_t*>(zbi.substr(offset, sizeof(zbi_header_t)).data())));
}
static fitx::result<error_type, payload_type> Payload(Storage& zbi, uint32_t offset,
uint32_t length) {
auto payload = zbi.substr(offset, length);
ZX_DEBUG_ASSERT(payload.size() == length);
return fitx::ok(std::move(payload));
}
static fitx::result<error_type, ByteView> Read(Storage& zbi, payload_type payload,
uint32_t length) {
ZX_DEBUG_ASSERT(payload.size() == length);
return fitx::ok(AsBytes(payload.data(), payload.size()));
}
};
// Specialization for std::span<any type> as Storage. Its payload_type is the
// same type as Storage, just yielding the subspan of the original whole-ZBI
// span.
#if __cpp_lib_span
template <typename T, size_t Extent>
inline ByteView AsBytes(std::span<T, Extent> payload) {
auto bytes = std::as_bytes(payload);
return {const_cast<const std::byte*>(bytes.data()), bytes.size()};
}
inline std::span<std::byte> AsWritableBytes(void* ptr, size_t len) {
return {reinterpret_cast<std::byte*>(ptr), len};
}
template <typename T, size_t Extent>
struct StorageTraits<std::span<T, Extent>> {
using Storage = std::span<T, Extent>;
struct error_type {};
using payload_type = Storage;
static std::string_view error_string(error_type error) { return {}; }
static fitx::result<error_type, uint32_t> Capacity(Storage& zbi) {
return fitx::ok(static_cast<uint32_t>(
std::min(zbi.size_bytes(), static_cast<size_t>(std::numeric_limits<uint32_t>::max()))));
}
template <typename S = T, typename = std::enable_if_t<!std::is_const_v<S>>>
static fitx::result<error_type> EnsureCapacity(Storage& zbi, uint32_t capacity_bytes) {
if (capacity_bytes > zbi.size()) {
return fitx::error{error_type{}};
}
return fitx::ok();
}
static fitx::result<error_type, std::reference_wrapper<const zbi_header_t>> Header(
Storage& zbi, uint32_t offset) {
return fitx::ok(std::ref(
*reinterpret_cast<const zbi_header_t*>(zbi.subspan(offset, sizeof(zbi_header_t)).data())));
}
static fitx::result<error_type, payload_type> Payload(Storage& zbi, uint32_t offset,
uint32_t length) {
auto payload = [&]() {
if constexpr (std::is_const_v<T>) {
return std::as_bytes(zbi).subspan(offset, length);
} else {
return std::as_writable_bytes(zbi).subspan(offset, length);
}
}();
ZX_DEBUG_ASSERT(payload.size() == length);
ZX_ASSERT_MSG(payload.size() % sizeof(T) == 0,
"payload size not a multiple of storage span element_type size");
return fitx::ok(payload_type{reinterpret_cast<T*>(payload.data()), payload.size() / sizeof(T)});
}
static fitx::result<error_type, ByteView> Read(Storage& zbi, payload_type payload,
uint32_t length) {
auto bytes = AsBytes(std::as_bytes(payload));
ZX_DEBUG_ASSERT(bytes.size() == length);
return fitx::ok(bytes);
}
template <typename S = T, typename = std::enable_if_t<!std::is_const_v<S>>>
static fitx::result<error_type> Write(Storage& zbi, uint32_t offset, ByteView data) {
memcpy(Write(zbi, offset, static_cast<uint32_t>(data.size())).value(), data.data(),
data.size());
return fitx::ok();
}
template <typename S = T, typename = std::enable_if_t<!std::is_const_v<S>>>
static fitx::result<error_type, void*> Write(Storage& zbi, uint32_t offset, uint32_t length) {
// The caller is supposed to maintain these invariants.
ZX_DEBUG_ASSERT(offset <= zbi.size_bytes());
ZX_DEBUG_ASSERT(length <= zbi.size_bytes() - offset);
return fitx::ok(&std::as_writable_bytes(zbi)[offset]);
}
};
#endif // __cpp_lib_span
} // namespace zbitl
#endif // LIB_ZBITL_STORAGE_TRAITS_H_