blob: 42261d756d5ed38ca0cbb787b05ce9c128551b82 [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 SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_DIAGNOSTICS_H_
#define SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_DIAGNOSTICS_H_
#include <zircon/assert.h>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <utility>
namespace elfldltl {
// Various template APIs use a polymorphic "diagnostics object" argument.
//
// This object is responsible for reporting errors and for the policy on when
// to bail out of processing ELF data early. All processing using this object
// is implicitly related to a single ELF file, so error details and locations
// always refer to that file.
//
// A diagnostics object must implement a few simple methods:
//
// * `bool FormatError(std::string_view error, ...)`
//
// This is called to report a fatal error in the ELF data. The return value
// tells the caller whether to continue processing to the extent safely
// possible after the error.
//
// The first argument is a string constant with permanent extent which
// describes the error and can be referred to indefinitely. If there are
// additional arguments they have one of three types:
//
// * `size_type`, the address-sized unsigned integral type for the file.
// This argument is a value from the file that the error complains about.
//
// * `elfldltl::FileOffset<size_type>`
// This argument is the offset in the ELF file, where the bad data is.
//
// * `elfldltl::FileAddress<size_type>`
// This argument is the address relative to the load bias of the ELF file,
// where the bad data is.
//
// Essentially this is an input-dependent assertion failure. FormatError is
// called exclusively for anomalies that can be explained only by a corrupted
// ELF file or memory image or by a linker bug. Processing cannot succeed
// and no code or data from this file should be used. The diagnostics object
// should return true only for the purpose of logging additional errors from
// the same file before abandoning it. The processor may attempt additional
// work but will only do what it can do safely without assertion failures or
// other risks of crashing. The bad data it has already encountered could
// lead to a cascade of additional errors with entirely bogus details, but it
// might be possible to get coherent reports of multiple independent errors.
//
// * `bool FormatWarning(std::string_view error, ...)`
//
// This is like FormatError, but for issues that are less problematic. These
// are anomalies that probably constitute bugs in the ELF file, but plausibly
// could be the result of build-time errors or dubious practices by the
// programmer rather than a bug in the tools or corrupted data per se. It's
// probably safe enough to ignore these issues and use the file regardless.
//
// * `bool extra_checking()`
//
// If this returns true, the processor may do some extra work that is not
// necessary for its correct operation but just offers an opportunity to
// notice anomalies in the ELF data and report errors or warnings that might
// otherwise go unnoticed. Extra checking can be avoided if the use case is
// optimized for performance over maximal format strictness, or if the
// diagnostics object is ignoring warnings, etc.
//
// This wraps an unsigned integral type to represent an offset in the ELF file.
template <typename size_type>
struct FileOffset {
using value_type = size_type;
value_type operator*() const { return offset; }
static constexpr std::string_view kDescription = "file offset";
value_type offset;
};
// Helper to discover if T is a FileOffset type.
template <typename T>
inline constexpr bool kIsFileOffset = false;
template <typename size_type>
inline constexpr bool kIsFileOffset<FileOffset<size_type>> = true;
// This wraps an unsigned integral type to represent an address in the ELF
// file's load image, i.e. such that the p_vaddr of the first PT_LOAD segment
// corresponds to that segment's p_offset in the file.
template <typename size_type>
struct FileAddress {
using value_type = size_type;
value_type operator*() const { return address; }
static constexpr std::string_view kDescription = "file-relative address";
value_type address;
};
// Helper to discover if T is a FileAddress type.
template <typename T>
inline constexpr bool kIsFileAddress = false;
template <typename size_type>
inline constexpr bool kIsFileAddress<FileAddress<size_type>> = true;
// These flags are used by the elfldltl::Diagnostics template implementation.
// This is the default for its template parameter. Any other class can be used
// as long as it provides members that are contextually convertible to bool
// with these names.
struct DiagnosticsFlags {
// If true, keep going after errors so more errors can be diagnosed.
bool multiple_errors = false;
// If true, then warnings are treated like errors and obey the multiple_errors
// setting too. If false, then always keep going after a warning.
bool warnings_are_errors = true;
// If true, do extra work to diagnose more errors that could be ignored.
bool extra_checking = false;
};
// An alternative Flags type can be defined like this one to make one or more
// of the values fixed, or to change the default value of a mutable flag.
struct DiagnosticsPanicFlags {
[[no_unique_address]] std::false_type multiple_errors;
[[no_unique_address]] std::true_type warnings_are_errors;
[[no_unique_address]] std::false_type extra_checking;
};
// elfldltl::Diagnostics provides a canonical implementation of a diagnostics
// object. It wraps any callable object that takes the std::string_view and
// other arguments passed to FormatError.
//
// The Flags type can be DiagnosticsFlags or any type with those three member
// names having types convertible to bool. The Flags object passed to the
// constructor (or default-constructed) determines the behavior. The flags()
// method returns the Flags copy in the diagnostics object, which can then be
// changed in place. The diagnostics object tracks the numbers of errors and
// warnings reported, unless Flags::multiple_errors is std::false_type.
//
// Convenience functions below return some canonical specializations of this.
//
template <typename Report, class Flags = DiagnosticsFlags>
class Diagnostics {
public:
constexpr Diagnostics(const Diagnostics&) = default;
constexpr Diagnostics(Diagnostics&&) noexcept = default;
explicit constexpr Diagnostics(Report report) : report_(std::move(report)) {}
constexpr Diagnostics(Report report, Flags flags)
: report_(std::move(report)), flags_(std::move(flags)) {}
constexpr const Flags& flags() const { return flags_; }
constexpr Flags& flags() { return flags_; }
constexpr unsigned int errors() const { return errors_; }
constexpr unsigned int warnings() const { return warnings_; }
template <typename... Args>
constexpr bool FormatError(std::string_view error, Args&&... args) {
++errors_;
return report_(error, std::forward<Args>(args)...) && flags_.multiple_errors;
}
template <typename... Args>
constexpr bool FormatWarning(std::string_view error, Args&&... args) {
++warnings_;
return report_(error, std::forward<Args>(args)...) &&
(flags_.multiple_errors || !flags_.warnings_are_errors);
}
// Reset the counters.
// This doesn't do anything to the state of the Report object.
constexpr void reset() {
errors_ = {};
warnings_ = {};
}
private:
// This is either a wrapper around an integer, or is an empty object.
// The tag is unused but makes the two Count types always distinct so
// that adjacent empty members with [[no_unique_address]] can be elided.
template <bool Counting, auto Tag>
struct Count;
// When counting, a trivial wrapper around an integer.
template <auto Tag>
struct Count<true, Tag> {
constexpr Count& operator++() {
++value_;
return *this;
}
constexpr operator unsigned int() const noexcept { return value_; }
unsigned int value_ = 0;
};
// When not counting, increments are no-ops and the count is always one.
template <auto Tag>
struct Count<false, Tag> {
constexpr Count& operator++() { return *this; }
constexpr operator unsigned int() const noexcept { return 1; }
};
// If multiple_errors is actually std::false_type, then use the empty objects
// so we don't bother to keep counts at all.
static constexpr bool kCount =
!std::is_same_v<decltype(std::declval<Flags>().multiple_errors), std::false_type>;
[[no_unique_address]] Report report_;
[[no_unique_address]] Flags flags_;
[[no_unique_address]] Count<kCount, &Flags::multiple_errors> errors_;
[[no_unique_address]] Count<kCount, &Flags::warnings_are_errors> warnings_;
};
// This returns a Diagnostics object that crashes immediately for any error or
// warning. There are no library dependencies of any kind. This behavior is
// appropriate only for self-relocation and bootstrapping cases where if there
// is anything wrong in the ELF data then something went wrong in building this
// program itself and it shouldn't be running at all.
constexpr auto TrapDiagnostics() {
constexpr auto trap = [](auto&&... args) -> bool {
__builtin_trap();
return false;
};
return Diagnostics(trap, DiagnosticsPanicFlags());
}
// This is similar to TrapDiagnostics but it uses the <zircon/assert.h>
// ZX_PANIC call to write the message and crash, with an optional fixed prefix.
// So it has some library dependencies but might be able to generate some error
// output beofre crashing. The argument is stored in the diagnostics object;
// it can be any type convertible to std::string_view, such as std::string.
// It's forwarded perfectly, so if passed as an lvalue reference, the reference
// will be stored rather than its referent copied.
template <typename T = std::string_view>
constexpr auto PanicDiagnostics(T&& prefix = std::string_view{}) {
struct Report {
bool operator()(std::string_view prefix, std::string_view error) const {
ZX_PANIC("%.*s%.*s", static_cast<int>(prefix.size()), prefix.data(),
static_cast<int>(error.size()), error.data());
return false;
}
// TODO(mcgrathr): more overloads for value arguments.
};
auto panic = [prefix = std::forward<T>(prefix)](auto&&... args) {
return Report{}(prefix, std::forward<decltype(args)>(args)...);
};
return Diagnostics(panic, DiagnosticsPanicFlags());
}
// This returns a Diagnostics object that simply stores a single error or
// warning message string. It always request early bail-out for errors on the
// expectation that only one error will be reported. But if the same object is
// indeed called again for another failure, the new error message will replace
// the old one.
template <typename T, typename... Flags>
constexpr auto OneStringDiagnostics(T& holder, Flags&&... flags) {
auto set_error = [&holder](std::string_view error, auto&&... args) {
holder = error;
return false;
};
return Diagnostics(set_error, std::forward<Flags>(flags)...);
}
// This returns a Diagnostics object that collects a container of messages.
template <typename T, typename... Flags>
constexpr auto CollectStringsDiagnostics(T& container, Flags&&... flags) {
auto add_error = [&container](std::string_view error, auto&&... args) {
container.emplace_back(error);
return true;
};
return Diagnostics(add_error, std::forward<Flags>(flags)...);
}
// This returns a Diagnostics object that uses << on an ostream-style object.
// Any additional arguments are passed via << as a prefix on each message. The
// ostream should probably be in << std::hex state for the output to look good.
template <typename Ostream, class Flags = DiagnosticsFlags, typename... Args>
constexpr auto OstreamDiagnostics(Ostream& ostream, Flags&& flags = {}, Args&&... prefix) {
auto output = [prefix = std::make_tuple(std::forward<Args>(prefix)...), &ostream](
std::string_view error, auto&&... args) mutable -> bool {
std::apply([&](auto&&... prefix) { ((ostream << prefix), ...); }, prefix);
ostream << error;
((ostream << args), ...);
ostream << "\n";
return true;
};
return Diagnostics(std::move(output), flags);
}
// These overloads let the object returned by OstreamDiagnostics format the
// special argument types.
template <typename S, typename T>
constexpr decltype(auto) operator<<(S&& ostream, FileOffset<T> offset) {
return std::forward<S>(ostream) << " at file offset " << *offset;
}
template <typename S, typename T>
constexpr decltype(auto) operator<<(S&& ostream, FileAddress<T> address) {
return std::forward<S>(ostream) << " at relative address " << *address;
}
} // namespace elfldltl
#endif // SRC_LIB_ELFLDLTL_INCLUDE_LIB_ELFLDLTL_DIAGNOSTICS_H_