blob: 9d3410341d2aaf6118bf31f184581347eaa8f001 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include "lib/linux-boot-config/linux-boot-config.h"
#include <ctype.h>
#include <lib/fit/defer.h>
#include <algorithm>
namespace linux_boot_config {
namespace {
// Represents a section of the BOOTDATA at a given offset. The offset allows providing some
// context about where an error might have happened within the file being parsed.
//
// Also the offset does not align
struct Chunk {
// Behaves like a string_view with a reference to were it was found in some other string.
constexpr const std::string_view& operator*() const { return data; }
constexpr const std::string_view* operator->() const { return &data; }
constexpr void remove_prefix(size_t n) {
data.remove_prefix(n);
chunk_offset += n;
}
fit::error<ParseError> error(std::string_view description) {
return fit::error(ParseError{.description = description, .offset = chunk_offset});
}
void remove_prefix_matching(std::string_view matching) {
size_t index = data.find_first_not_of(matching);
if (index == std::string_view::npos) {
data = std::string_view();
chunk_offset = data.size() + chunk_offset;
return;
}
data = data.substr(index);
chunk_offset = index + chunk_offset;
}
void remove_prefix_until(std::string_view matching) {
size_t index = data.find_first_of(matching);
if (index == std::string_view::npos) {
index = data.size() - 1;
}
remove_prefix(index + 1);
}
// Collection of characters representing a stream.
std::string_view data;
// Offset of `chunk` in the source string.
size_t chunk_offset = 0;
};
// Removes the trailing characters from `tail`, that
constexpr std::string_view TrimTail(std::string_view tail) {
size_t end_at = tail.find_last_not_of(' ');
if (end_at == std::string_view::npos) {
return std::string_view();
}
return tail.substr(0, end_at + 1);
}
bool IsKeyCharacter(const char c) { return isalnum(c) || c == '-' || c == '_' || c == '.'; }
bool IsUnquotedValueCharacter(const char c) {
// This cannot be embedded inside the value.
constexpr std::string_view kForbidden = " \n;,}#";
if (!isprint(c)) {
return false;
}
return kForbidden.find(c) == std::string_view::npos;
}
// Parses individual value entries, stopping at ',' , '\n' or ';'.
fit::result<ParseError, std::string_view> GetValue(Chunk& chunk) {
// Find first non-empty character.
chunk.remove_prefix_matching(" ");
if (chunk->empty()) {
return chunk.error("Operation with no value.");
}
if (chunk->front() == '"' || chunk->front() == '\'') {
// Great the value is delimited by quotes.
const char quote = chunk->front();
chunk.remove_prefix(1);
size_t end_quote = chunk->find(quote);
if (end_quote == std::string_view::npos) {
return chunk.error("Unclosed quotes");
}
std::string_view value_string = chunk->substr(0, end_quote);
if (std::ranges::find_if_not(value_string, isprint) != value_string.end()) {
return chunk.error("Invalid character in value");
}
chunk.remove_prefix(end_quote + 1);
return fit::ok(value_string);
}
auto value_end = std::ranges::find_if_not(*chunk, IsUnquotedValueCharacter);
std::string_view value_string = std::string_view(chunk->begin(), value_end);
chunk.remove_prefix(value_string.size());
return fit::ok(value_string);
}
fit::result<ParseError, std::string_view> GetKey(Chunk& chunk) {
// Eat any spaces and comment lines.
while (!chunk->empty()) {
chunk.remove_prefix_matching(" ");
if (chunk->empty()) {
return chunk.error("Empty key entry");
}
// Line breaks are only supported at the end of comments, key or values.
if (chunk->front() == '#') {
chunk.remove_prefix_until("\n");
continue;
}
if (chunk->front() == '}' || chunk->front() == ';') {
return fit::ok("");
}
// First non-space or comment, that isn't the end of a scope.
if (!IsKeyCharacter(chunk->front())) {
return chunk.error("Unsupported character for key");
}
size_t key_end =
std::distance(chunk->begin(), std::ranges::find_if_not(*chunk, &IsKeyCharacter));
std::string_view key = chunk->substr(0, key_end);
chunk.remove_prefix(key_end);
key = TrimTail(key);
return fit::ok(key);
}
return fit::ok("");
}
// Parses a value, and handles the possibility of array values. The assumption is,
// all values are "array-like", some of them are just one element arrays.
//
// Validates that comments do not precede a ','.
fit::result<ParseError> VisitValues(Key& key, auto& visitor, Chunk& chunk, Value::Action action) {
bool comment_before = false;
while (!chunk->empty()) {
chunk.remove_prefix_matching(" ");
if (chunk->empty()) {
if (action != Value::Action::kUnknown) {
return chunk.error("Operation without value");
}
return fit::ok();
}
switch (chunk->front()) {
case '#':
comment_before = true;
chunk.remove_prefix_until("\n");
// Comment following an array element.
// ```
// foo = 1, # Comment
// ```
if (action == Value::Action::kUnknown) {
// Continue so we check that we are not preceding a ','.
continue;
}
// Comment following empty value.
// ```
// foo# Comment
// ```
if (action == Value::Action::kDefine) {
visitor(key, Value{.action = action});
return fit::ok();
}
continue;
case ',':
if (comment_before) {
return fit::error(ParseError("Comment before ','", chunk.chunk_offset));
}
action = Value::Action::kAppend;
chunk.remove_prefix(1);
continue;
// Value has been fully visited.
case ';':
// Consume it so `VisitBody` does not treat this as the key having
// empty value.
chunk.remove_prefix(1);
return fit::ok();
case '\n':
if (action == Value::Action::kAppend) {
chunk.remove_prefix(1);
continue;
}
return fit::ok();
case '}':
return fit::ok();
// Consume a value.
default:
break;
};
// A comment that was at the end of the statement.
if (action == Value::Action::kUnknown) {
return fit::ok();
}
auto value_res = GetValue(chunk);
if (value_res.is_error()) {
return value_res.take_error();
}
visitor(key, Value{.action = action, .value = *value_res});
// Reset the value, so if no ',' is found and we run into a linebreak
// we exit.
action = Value::Action::kUnknown;
chunk.remove_prefix_matching(" ");
comment_before = false;
}
return fit::ok();
}
// Visits all leaf nodes with their respective values. If any key in this scope
// has children nodes, then those are recursively visited in pre-order fashion.
fit::result<ParseError> VisitBody(Key& key, auto& visitor, Chunk& chunk) {
auto is_assign_or_override = [&chunk]() -> fit::result<ParseError> {
if (chunk->size() < 2) {
return chunk.error("Not enough characters for operator.");
}
if (chunk->at(1) != '=') {
return chunk.error("Invalid operator.");
}
chunk.remove_prefix(2);
return fit::ok();
};
auto visit_empty = [&key, &visitor](KeyPart& node) {
if (node.name.empty()) {
return;
}
visitor(key, Value{.action = Value::Action::kDefine});
};
while (!chunk->empty()) {
auto key_res = GetKey(chunk);
if (key_res.is_error()) {
return key_res.take_error();
}
// Key may be empty.
KeyPart key_part{.name = *key_res};
// This is safe, every recursive call pushes one element to key at most, and
// if it does, it pops it as soon as it returns.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-pointer"
key.push_back(&key_part);
#pragma GCC diagnostic pop
auto deferred_pop = fit::defer([&key]() { key.pop_back(); });
chunk.remove_prefix_matching(" ");
if (chunk->empty()) {
// Do not visit empty keys.
if (key_part.name.empty()) {
break;
}
visitor(key, Value{.action = Value::Action::kDefine});
return fit::ok();
}
Value::Action action = Value::Action::kDefine;
switch (chunk->front()) {
case '\n':
case ';':
visit_empty(key_part);
chunk.remove_prefix(1);
continue;
case '}':
visit_empty(key_part);
return fit::ok();
case '{':
// Visit intermediate node.
chunk.remove_prefix(1);
chunk.remove_prefix_matching(" \n");
// Intermediate node, we may want to skip.
if (auto res = VisitBody(key, visitor, chunk); res.is_error()) {
return res.take_error();
}
chunk.remove_prefix_matching(" \n");
if (chunk->empty() || chunk->front() != '}') {
return chunk.error("Unterminated scope");
}
chunk.remove_prefix(1);
chunk.remove_prefix_matching(" \n");
continue;
case ':':
action = Value::Action::kOverride;
if (auto res = is_assign_or_override(); res.is_error()) {
return res.take_error();
}
break;
case '+':
action = Value::Action::kAppend;
if (auto res = is_assign_or_override(); res.is_error()) {
return res.take_error();
}
break;
case '=':
action = Value::Action::kDefine;
chunk.remove_prefix(1);
break;
default:
break;
};
if (auto res = VisitValues(key, visitor, chunk, action); res.is_error()) {
return res.take_error();
}
chunk.remove_prefix_matching(" \n");
}
return fit::ok();
}
} // namespace
Key::CompareResult Key::Compare(std::string_view key) const {
if (is_empty()) {
return key.empty() ? CompareResult::kMatch : CompareResult::kNoMatch;
}
// 'key' is used as the remainder of the compared key, we will be sliding
// forward and comparing the available sections, one at a time.
auto it = begin();
while (it != end() && key.size() > it->name.size()) {
std::string_view sections = it->name;
if (!key.starts_with(sections)) {
return CompareResult::kNoMatch;
}
// The section is a prefix of the key, but this chunk is not matching
// an entire section. E,g, 'foo.bar' with 'foo.b'
if (key[sections.size()] != '.') {
return CompareResult::kNoMatch;
}
it = std::next(it);
key.remove_prefix(sections.size() + 1);
}
// We consume all the sections, but not the key.
// E.g. foo.bar with `foo.bar.baz`
if (it == end()) {
return CompareResult::kParent;
}
// The opposite situation from above, `foo.bar.baz` with `foo.bar`.
if (key.empty()) {
return CompareResult::kChild;
}
std::string_view section = it->name;
if (!section.starts_with(key)) {
return CompareResult::kNoMatch;
}
if (section.size() == key.size()) {
if (std::next(it) == end()) {
return CompareResult::kMatch;
}
return CompareResult::kChild;
}
if (section[key.size()] == '.') {
return CompareResult::kChild;
}
return CompareResult::kNoMatch;
}
fit::result<ParseError, LinuxBootConfig> LinuxBootConfig::Create(
std::span<const std::byte> initrd) {
if (initrd.size_bytes() < sizeof(Trailer)) {
return fit::ok(LinuxBootConfig{});
}
Trailer trailer;
trailer.Read(initrd.subspan(initrd.size() - sizeof(Trailer)));
if (trailer.magic != Trailer::kMagic) {
return fit::ok(LinuxBootConfig{});
}
if (trailer.size % 4 != 0) {
return fit::error(ParseError("`bootconfig` file size is not properly aligned."));
}
if (trailer.size + sizeof(Trailer) > initrd.size_bytes()) {
return fit::error(ParseError("`bootconfig` file is bigger than `initrd`."));
}
auto byte_content =
initrd.subspan(initrd.size_bytes() - trailer.size - sizeof(trailer), trailer.size);
uint32_t checksum = Checksum(byte_content);
if (checksum != trailer.checksum) {
return fit::error(ParseError("`bootconfig` file checksum failed."));
}
LinuxBootConfig boot_config;
boot_config.contents_ = {reinterpret_cast<const char*>(byte_content.data()),
byte_content.size_bytes()};
return fit::ok(boot_config);
}
fit::result<ParseError> LinuxBootConfig::VisitInternal(LinuxBootConfig::NodeVisitor visitor) const {
Key key;
// No boot config.
if (contents_.empty()) {
return fit::ok();
}
Chunk boot_config{.data = contents_, .chunk_offset = 0};
boot_config.remove_prefix_matching(" \n");
// Empty boot config.
if (boot_config->empty()) {
return fit::ok();
}
// Non-empty boot-config, so we can enforce certain invariants. Also, because this is the root,
// we do not need to check for a closing brace.
if (auto visit_result = VisitBody(key, visitor, boot_config); visit_result.is_error()) {
return visit_result.take_error();
}
return fit::ok();
}
} // namespace linux_boot_config