diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index eff2a1c6610..de523f1280f 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -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) diff --git a/Tests/LibDeviceTree/CMakeLists.txt b/Tests/LibDeviceTree/CMakeLists.txt new file mode 100644 index 00000000000..86ed04371cf --- /dev/null +++ b/Tests/LibDeviceTree/CMakeLists.txt @@ -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) diff --git a/Tests/LibDeviceTree/TestLookup.cpp b/Tests/LibDeviceTree/TestLookup.cpp new file mode 100644 index 00000000000..bc0d8084f18 --- /dev/null +++ b/Tests/LibDeviceTree/TestLookup.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024, Leon Albrecht + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include +#include +#include + +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); +} diff --git a/Tests/LibDeviceTree/dtb.dtb b/Tests/LibDeviceTree/dtb.dtb new file mode 100644 index 00000000000..def7fcb6e48 Binary files /dev/null and b/Tests/LibDeviceTree/dtb.dtb differ diff --git a/Userland/Libraries/LibDeviceTree/CMakeLists.txt b/Userland/Libraries/LibDeviceTree/CMakeLists.txt index 6ab6fc73805..78b2e0241cb 100644 --- a/Userland/Libraries/LibDeviceTree/CMakeLists.txt +++ b/Userland/Libraries/LibDeviceTree/CMakeLists.txt @@ -1,5 +1,6 @@ set(SOURCES + DeviceTree.cpp FlattenedDeviceTree.cpp Validation.cpp ) diff --git a/Userland/Libraries/LibDeviceTree/DeviceTree.cpp b/Userland/Libraries/LibDeviceTree/DeviceTree.cpp new file mode 100644 index 00000000000..51cfef64572 --- /dev/null +++ b/Userland/Libraries/LibDeviceTree/DeviceTree.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, Leon Albrecht + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "DeviceTree.h" +#include "FlattenedDeviceTree.h" +#include +#include + +namespace DeviceTree { + +ErrorOr> DeviceTree::parse(ReadonlyBytes flattened_device_tree) +{ + // Device tree must be 8-byte aligned + if ((bit_cast(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(flattened_device_tree.data()); + + TRY(walk_device_tree(header, flattened_device_tree, + { + .on_node_begin = [¤t_node, &device_tree](StringView name) -> ErrorOr { + // 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 = [¤t_node](StringView) -> ErrorOr { + current_node = current_node->parent(); + return IterationDecision::Continue; + }, + .on_property = [&device_tree, ¤t_node](StringView name, ReadonlyBytes value) -> ErrorOr { + DeviceTreeProperty property { value }; + + if (name == "phandle"sv) { + auto phandle = property.as(); + TRY(device_tree->set_phandle(phandle, current_node)); + } + + TRY(current_node->properties().try_set(name, DeviceTreeProperty { value })); + return IterationDecision::Continue; + }, + .on_noop = []() -> ErrorOr { + return IterationDecision::Continue; + }, + .on_end = [&]() -> ErrorOr { + return {}; + }, + })); + + return device_tree; +} + +} diff --git a/Userland/Libraries/LibDeviceTree/DeviceTree.h b/Userland/Libraries/LibDeviceTree/DeviceTree.h new file mode 100644 index 00000000000..b50cba50029 --- /dev/null +++ b/Userland/Libraries/LibDeviceTree/DeviceTree.h @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2024, Leon Albrecht + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +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 as_strings() const { return as_string().split_view('\0'); } + template + auto for_each_string(T callback) const { return as_string().for_each_split_view('\0', SplitBehavior::Nothing, callback); } + + // Note: as does not convert endianness, so all structures passed in + // should use BigEndians for their members and keep ordering in mind + // Note: The Integral variant does convert endianness, so no need to pass in BigEndians + template + T as() const + { + VERIFY(raw_data.size() == sizeof(T)); + T value; + __builtin_memcpy(&value, raw_data.data(), sizeof(T)); + return value; + } + + template + requires(alignof(T) <= 4 && !IsIntegral) + T const& as() const + { + return *reinterpret_cast(raw_data.data()); + } + + template + I as() const + { + VERIFY(raw_data.size() == sizeof(I)); + BigEndian value; + __builtin_memcpy(&value, raw_data.data(), sizeof(I)); + return value; + } + + template + ErrorOr for_each_in_array_of(CallableAs, 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()); + 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 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 get_child(StringView child) const { return m_children.get(child); } + + HashMap const& children() const { return m_children; } + HashMap 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& children() { return m_children; } + HashMap& properties() { return m_properties; } + DeviceTreeNodeView* parent() { return m_parent; } + +private: + DeviceTreeNodeView* m_parent; + HashMap m_children; + HashMap m_properties; +}; + +class DeviceTree : public DeviceTreeNodeView { +public: + static ErrorOr> 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 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 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 m_phandles; +}; + +} diff --git a/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.cpp b/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.cpp index 9112619e81b..af523beed70 100644 --- a/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.cpp +++ b/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #ifdef KERNEL @@ -137,6 +138,7 @@ static ErrorOr 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(path.size() - 1)) { @@ -169,26 +171,9 @@ static ErrorOr slow_get_property_raw(StringView name, FlattenedDe return found_property_value; } -template -ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree) +ErrorOr 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) { - return {}; - } else if constexpr (IsArithmetic) { - if (bytes.size() != sizeof(T)) { - return Error::from_errno(EINVAL); - } - return *bit_cast(bytes.data()); - } else { - static_assert(IsSame); - return T(bytes); - } + return DeviceTreeProperty { TRY(slow_get_property_raw(name, header, raw_device_tree)) }; } -template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); - } // namespace DeviceTree diff --git a/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.h b/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.h index 36c85ef6c39..7b2d263725e 100644 --- a/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.h +++ b/Userland/Libraries/LibDeviceTree/FlattenedDeviceTree.h @@ -13,6 +13,8 @@ #include #include +#include "DeviceTree.h" + namespace DeviceTree { // https://devicetree-specification.readthedocs.io/en/v0.3/flattened-format.html @@ -60,12 +62,6 @@ struct DeviceTreeCallbacks { ErrorOr walk_device_tree(FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree, DeviceTreeCallbacks); -template -ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree); - -extern template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -extern template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -extern template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); -extern template ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const& header, ReadonlyBytes raw_device_tree); +ErrorOr slow_get_property(StringView name, FlattenedDeviceTreeHeader const&, ReadonlyBytes raw_device_tree); } // namespace DeviceTree diff --git a/Userland/Utilities/fdtdump.cpp b/Userland/Utilities/fdtdump.cpp index 3e484535126..689a285e78e 100644 --- a/Userland/Utilities/fdtdump.cpp +++ b/Userland/Utilities/fdtdump.cpp @@ -35,14 +35,13 @@ ErrorOr serenity_main(Main::Arguments arguments) TRY(DeviceTree::dump(*fdt_header, bytes)); - auto compatible = TRY(DeviceTree::slow_get_property("/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("/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("/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;