LibDeviceTree: Add a simple DeviceTree class

This makes it easier to work with device tree nodes and properties, then
writing simple state machines to parse the device tree.
This also makes the old slow traversal methods use the
DeviceTreeProperty helper class, and adds a simple test.
This commit is contained in:
Hediadyoin1 2024-02-14 14:57:17 +01:00 committed by Andrew Kaster
parent 0fa718ead8
commit 21a21c6a11
Notes: sideshowbarker 2024-07-17 02:55:44 +09:00
10 changed files with 316 additions and 31 deletions

View File

@ -5,6 +5,7 @@ add_subdirectory(LibC)
add_subdirectory(LibCompress)
add_subdirectory(LibCore)
add_subdirectory(LibCpp)
add_subdirectory(LibDeviceTree)
add_subdirectory(LibDiff)
add_subdirectory(LibEDID)
add_subdirectory(LibELF)

View File

@ -0,0 +1,9 @@
set(TEST_SOURCES
TestLookup.cpp
)
foreach(source IN LISTS TEST_SOURCES)
serenity_test("${source}" LibDeviceTree LIBS LibDeviceTree LibFileSystem)
endforeach()
install(FILES dtb.dtb DESTINATION usr/Tests/LibDeviceTree)

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024, Leon Albrecht <leon.a@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibTest/TestCase.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibDeviceTree/DeviceTree.h>
#include <LibDeviceTree/FlattenedDeviceTree.h>
TEST_CASE(basic_functionality)
{
auto fdt_file = TRY_OR_FAIL(Core::File::open("/usr/Tests/LibDeviceTree/dtb.dtb"sv, Core::File::OpenMode::Read));
auto fdt = TRY_OR_FAIL(fdt_file->read_until_eof());
auto device_tree = TRY_OR_FAIL(DeviceTree::DeviceTree::parse(fdt));
auto boot_args = device_tree->resolve_property("/chosen/bootargs"sv);
EXPECT(boot_args.has_value());
EXPECT_EQ(boot_args->as_string(), "hello root=nvme0:1:0 serial_debug"sv);
EXPECT(device_tree->phandle(1));
auto device_type = device_tree->phandle(1)->get_property("device_type"sv);
EXPECT(device_type.has_value());
EXPECT_EQ(device_type->as_string(), "cpu"sv);
}

BIN
Tests/LibDeviceTree/dtb.dtb Normal file

Binary file not shown.

View File

@ -1,5 +1,6 @@
set(SOURCES
DeviceTree.cpp
FlattenedDeviceTree.cpp
Validation.cpp
)

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2024, Leon Albrecht <leon.a@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "DeviceTree.h"
#include "FlattenedDeviceTree.h"
#include <AK/NonnullOwnPtr.h>
#include <AK/Types.h>
namespace DeviceTree {
ErrorOr<NonnullOwnPtr<DeviceTree>> DeviceTree::parse(ReadonlyBytes flattened_device_tree)
{
// Device tree must be 8-byte aligned
if ((bit_cast<FlatPtr>(flattened_device_tree.data()) & 0b111) != 0)
return Error::from_errno(EINVAL);
auto device_tree = TRY(adopt_nonnull_own_or_enomem(new (nothrow) DeviceTree { flattened_device_tree }));
DeviceTreeNodeView* current_node = device_tree.ptr();
auto const& header = *reinterpret_cast<FlattenedDeviceTreeHeader const*>(flattened_device_tree.data());
TRY(walk_device_tree(header, flattened_device_tree,
{
.on_node_begin = [&current_node, &device_tree](StringView name) -> ErrorOr<IterationDecision> {
// Skip the root node, which has an empty name
if (current_node == device_tree.ptr() && name.is_empty())
return IterationDecision::Continue;
// FIXME: Use something like children.emplace
TRY(current_node->children().try_set(name, DeviceTreeNodeView { current_node }));
auto& new_node = current_node->children().get(name).value();
current_node = &new_node;
return IterationDecision::Continue;
},
.on_node_end = [&current_node](StringView) -> ErrorOr<IterationDecision> {
current_node = current_node->parent();
return IterationDecision::Continue;
},
.on_property = [&device_tree, &current_node](StringView name, ReadonlyBytes value) -> ErrorOr<IterationDecision> {
DeviceTreeProperty property { value };
if (name == "phandle"sv) {
auto phandle = property.as<u32>();
TRY(device_tree->set_phandle(phandle, current_node));
}
TRY(current_node->properties().try_set(name, DeviceTreeProperty { value }));
return IterationDecision::Continue;
},
.on_noop = []() -> ErrorOr<IterationDecision> {
return IterationDecision::Continue;
},
.on_end = [&]() -> ErrorOr<void> {
return {};
},
}));
return device_tree;
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (c) 2024, Leon Albrecht <leon.a@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Concepts.h>
#include <AK/Endian.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/IterationDecision.h>
#include <AK/MemoryStream.h>
#include <AK/Optional.h>
#include <AK/Span.h>
namespace DeviceTree {
struct DeviceTreeProperty {
ReadonlyBytes raw_data;
size_t size() const { return raw_data.size(); }
StringView as_string() const { return StringView(raw_data.data(), raw_data.size() - 1); }
Vector<StringView> as_strings() const { return as_string().split_view('\0'); }
template<typename T>
auto for_each_string(T callback) const { return as_string().for_each_split_view('\0', SplitBehavior::Nothing, callback); }
// Note: as<T> does not convert endianness, so all structures passed in
// should use BigEndian<T>s for their members and keep ordering in mind
// Note: The Integral variant does convert endianness, so no need to pass in BigEndian<T>s
template<typename T>
T as() const
{
VERIFY(raw_data.size() == sizeof(T));
T value;
__builtin_memcpy(&value, raw_data.data(), sizeof(T));
return value;
}
template<typename T>
requires(alignof(T) <= 4 && !IsIntegral<T>)
T const& as() const
{
return *reinterpret_cast<T const*>(raw_data.data());
}
template<Integral I>
I as() const
{
VERIFY(raw_data.size() == sizeof(I));
BigEndian<I> value;
__builtin_memcpy(&value, raw_data.data(), sizeof(I));
return value;
}
template<typename T>
ErrorOr<void> for_each_in_array_of(CallableAs<ErrorOr<IterationDecision>, T const&> auto callback) const
{
VERIFY(raw_data.size() % sizeof(T) == 0);
size_t count = raw_data.size() / sizeof(T);
size_t offset = 0;
for (size_t i = 0; i < count; ++i, offset += sizeof(T)) {
auto sub_property = DeviceTreeProperty { raw_data.slice(offset, sizeof(T)) };
auto result = callback(sub_property.as<T>());
if (result.is_error())
return result;
if (result.value() == IterationDecision::Break)
break;
}
return {};
}
FixedMemoryStream as_stream() const { return FixedMemoryStream { raw_data }; }
};
class DeviceTreeNodeView {
public:
bool has_property(StringView prop) const { return m_properties.contains(prop); }
bool has_child(StringView child) const { return m_children.contains(child); }
bool child(StringView name) const { return has_property(name) || has_child(name); }
Optional<DeviceTreeProperty> get_property(StringView prop) const { return m_properties.get(prop); }
// FIXME: The spec says that @address parts of the name should be ignored when looking up nodes
// when they do not appear in the queried name, and all nodes with the same name should be returned
Optional<DeviceTreeNodeView const&> get_child(StringView child) const { return m_children.get(child); }
HashMap<StringView, DeviceTreeNodeView> const& children() const { return m_children; }
HashMap<StringView, DeviceTreeProperty> const& properties() const { return m_properties; }
DeviceTreeNodeView const* parent() const { return m_parent; }
// FIXME: Add convenience functions for common properties like "reg" and "compatible"
// Note: The "reg" property is a list of address and size pairs, but the address is not always a u32 or u64
// In pci devices the #address-size is 3 cells: (phys.lo phys.mid phys.hi)
// with the following format:
// phys.lo, phys.mid: 64-bit Address - BigEndian
// phys.hi: relocatable(1), prefetchable(1), aliased(1), 000(3), space type(2), bus number(8), device number(5), function number(3), register number(8) - BigEndian
// FIXME: Stringify?
// FIXME: Flatten?
// Note: That we dont have a oder of children and properties in this view
protected:
friend class DeviceTree;
DeviceTreeNodeView(DeviceTreeNodeView* parent)
: m_parent(parent)
{
}
HashMap<StringView, DeviceTreeNodeView>& children() { return m_children; }
HashMap<StringView, DeviceTreeProperty>& properties() { return m_properties; }
DeviceTreeNodeView* parent() { return m_parent; }
private:
DeviceTreeNodeView* m_parent;
HashMap<StringView, DeviceTreeNodeView> m_children;
HashMap<StringView, DeviceTreeProperty> m_properties;
};
class DeviceTree : public DeviceTreeNodeView {
public:
static ErrorOr<NonnullOwnPtr<DeviceTree>> parse(ReadonlyBytes);
DeviceTreeNodeView const* resolve_node(StringView path) const
{
// FIXME: May children of aliases be referenced?
// Note: Aliases may not contain a '/' in their name
// And as all paths other than aliases should start with '/', we can just check for the first '/'
if (!path.starts_with('/')) {
if (auto alias_list = get_child("aliases"sv); alias_list.has_value()) {
if (auto alias = alias_list->get_property(path); alias.has_value()) {
path = alias.value().as_string();
} else {
dbgln("DeviceTree: '{}' not found in /aliases, treating as absolute path", path);
}
} else {
dbgln("DeviceTree: No /aliases node found, treating '{}' as absolute path", path);
}
}
DeviceTreeNodeView const* node = this;
path.for_each_split_view('/', SplitBehavior::Nothing, [&](auto const& part) {
if (auto child = node->get_child(part); child.has_value()) {
node = &child.value();
} else {
node = nullptr;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return node;
}
Optional<DeviceTreeProperty> resolve_property(StringView path) const
{
auto property_name = path.find_last_split_view('/');
auto node_path = path.substring_view(0, path.length() - property_name.length() - 1);
auto const* node = resolve_node(node_path);
if (!node)
return {};
return node->get_property(property_name);
}
// FIXME: Add a helper to iterate over each descendant fulfilling some properties
// Like each node with a "compatible" property containing "pci" or "usb",
// bonus points if it could automatically recurse in the tree under some conditions,
// like "simple-bus" or "pci-bridge" nodes
DeviceTreeNodeView const* phandle(u32 phandle) const
{
if (phandle >= m_phandles.size())
return nullptr;
return m_phandles[phandle];
}
ReadonlyBytes flattened_device_tree() const { return m_flattened_device_tree; }
private:
DeviceTree(ReadonlyBytes flattened_device_tree)
: DeviceTreeNodeView(nullptr)
, m_flattened_device_tree(flattened_device_tree)
{
}
ErrorOr<void> set_phandle(u32 phandle, DeviceTreeNodeView* node)
{
if (m_phandles.size() > phandle && m_phandles[phandle] != nullptr)
return Error::from_string_view_or_print_error_and_return_errno("Duplicate phandle entry in DeviceTree"sv, EINVAL);
if (m_phandles.size() <= phandle)
TRY(m_phandles.try_resize(phandle + 1));
m_phandles[phandle] = node;
return {};
}
ReadonlyBytes m_flattened_device_tree;
Vector<DeviceTreeNodeView*> m_phandles;
};
}

View File

@ -9,6 +9,7 @@
#include <AK/IterationDecision.h>
#include <AK/MemoryStream.h>
#include <AK/StringView.h>
#include <LibDeviceTree/DeviceTree.h>
#include <LibDeviceTree/FlattenedDeviceTree.h>
#ifdef KERNEL
@ -137,6 +138,7 @@ static ErrorOr<ReadonlyBytes> slow_get_property_raw(StringView name, FlattenedDe
++current_path_idx; // Root node
return IterationDecision::Continue;
}
// FIXME: This might need to ignore address details in the path
if (token_name == path[current_path_idx]) {
++current_path_idx;
if (current_path_idx == static_cast<ssize_t>(path.size() - 1)) {
@ -169,26 +171,9 @@ static ErrorOr<ReadonlyBytes> slow_get_property_raw(StringView name, FlattenedDe
return found_property_value;
}
template<typename T>
ErrorOr<T> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree)
ErrorOr<DeviceTreeProperty> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree)
{
[[maybe_unused]] auto bytes = TRY(slow_get_property_raw(name, header, raw_device_tree));
if constexpr (IsVoid<T>) {
return {};
} else if constexpr (IsArithmetic<T>) {
if (bytes.size() != sizeof(T)) {
return Error::from_errno(EINVAL);
}
return *bit_cast<T*>(bytes.data());
} else {
static_assert(IsSame<T, StringView>);
return T(bytes);
}
return DeviceTreeProperty { TRY(slow_get_property_raw(name, header, raw_device_tree)) };
}
template ErrorOr<void> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
template ErrorOr<u32> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
template ErrorOr<u64> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
template ErrorOr<StringView> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
} // namespace DeviceTree

View File

@ -13,6 +13,8 @@
#include <AK/StringView.h>
#include <AK/Types.h>
#include "DeviceTree.h"
namespace DeviceTree {
// https://devicetree-specification.readthedocs.io/en/v0.3/flattened-format.html
@ -60,12 +62,6 @@ struct DeviceTreeCallbacks {
ErrorOr<void> walk_device_tree(FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree, DeviceTreeCallbacks);
template<typename T>
ErrorOr<T> slow_get_property(StringView name, FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree);
extern template ErrorOr<void> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
extern template ErrorOr<u32> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
extern template ErrorOr<u64> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
extern template ErrorOr<StringView> slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree);
ErrorOr<DeviceTreeProperty> slow_get_property(StringView name, FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree);
} // namespace DeviceTree

View File

@ -35,14 +35,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
TRY(DeviceTree::dump(*fdt_header, bytes));
auto compatible = TRY(DeviceTree::slow_get_property<StringView>("/compatible"sv, *fdt_header, bytes));
auto compatible_strings = compatible.split_view('\0');
dbgln("compatible with: {}", compatible_strings);
auto compatible = TRY(DeviceTree::slow_get_property("/compatible"sv, *fdt_header, bytes)).as_strings();
dbgln("compatible with: {}", compatible);
auto bootargs = TRY(DeviceTree::slow_get_property<StringView>("/chosen/bootargs"sv, *fdt_header, bytes));
auto bootargs = TRY(DeviceTree::slow_get_property("/chosen/bootargs"sv, *fdt_header, bytes)).as_string();
dbgln("bootargs: {}", bootargs);
auto cpu_compatible = TRY(DeviceTree::slow_get_property<StringView>("/cpus/cpu@0/compatible"sv, *fdt_header, bytes));
auto cpu_compatible = TRY(DeviceTree::slow_get_property("/cpus/cpu@0/compatible"sv, *fdt_header, bytes)).as_string();
dbgln("cpu0 compatible: {}", cpu_compatible);
return 0;