firmware: add FDT parser

This commit is contained in:
Loïc Grégoire
2026-10-12 14:46:20 -04:00
parent a9ecd069a3
commit 2b1b7a7c84
3 changed files with 593 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
//! Flattened device tree (FDT) parsing.
const std = @import("std");
const assert = std.debug.assert;
const rtl = @import("rtl");
bytes: []const u8,
header: Header,
const fdt_magic = 0xd00dfeed;
const Fdt = @This();
const Header = extern struct {
magic: u32,
total_size: u32,
off_dt_struct: u32,
off_dt_strings: u32,
off_mem_rsvmap: u32,
version: u32,
last_comp_version: u32,
boot_cpuid_phys: u32,
size_dt_strings: u32,
size_dt_struct: u32,
};
const TokenId = enum(u32) {
BeginNode = 1,
EndNode = 2,
Property = 3,
Nop = 4,
End = 9,
};
pub const Error = error{
InvalidMagic,
MalformedHeader,
InvalidSize,
InvalidToken,
};
pub const IterError = error{
InvalidString,
Truncated,
};
pub const IterMode = enum {
Nodes,
Properties,
Values,
};
pub const EvalError = IterError || error{
NotANode,
NotFound,
};
pub const Node = struct {
name: []const u8,
offset: usize,
pub fn iter(
node: *const Node,
fdt: *const Fdt,
comptime mode: IterMode,
) IteratorType(mode) {
return .{
.core = .{
.fdt = fdt,
.offset = node.offset,
},
};
}
};
pub const Property = struct {
name: []const u8,
value: []const u8,
pub fn as_type(self: *const Property, comptime T: type) T {
return std.mem.readInt(T, self.value[0..@sizeOf(T)], .big);
}
};
pub const Value = union(enum) {
Node: Node,
Property: Property,
};
const IteratorCore = struct {
fdt: *const Fdt,
offset: usize,
fn read_u32_at_offset(self: *IteratorCore) IterError!u32 {
const value = std.mem.readInt(
u32,
self.fdt.bytes[self.offset..][0..4],
.big,
);
if (self.offset + 4 >= self.fdt.bytes.len) {
return error.Truncated;
}
self.offset += @sizeOf(u32);
return value;
}
/// Skip until the offset is at a 4-byte boundary.
fn align_offset(self: *IteratorCore) void {
self.offset = (self.offset + 3) & ~@as(usize, 3);
}
};
fn IterType(comptime mode: IterMode) type {
return switch (mode) {
.Nodes => Node,
.Properties => Property,
.Values => Value,
};
}
fn IteratorType(comptime mode: IterMode) type {
return struct {
core: IteratorCore,
const Iterator = @This();
/// Returns the next token in the node, if any.
pub fn next(self: *Iterator) IterError!?IterType(mode) {
var depth: usize = 0;
var node: ?Node = null;
while (true) {
const raw_token_id = try self.core.read_u32_at_offset();
// Ensure the offset gets advanced.
const token_id: TokenId = @fromBackingInt(raw_token_id);
const bytes = self.core.fdt.bytes;
switch (token_id) {
.BeginNode => {
const offset = self.core.offset;
const null_byte = std.mem.indexOfScalar(
u8,
bytes[offset..],
0,
) orelse return error.InvalidString;
const name = bytes[offset .. offset + null_byte];
// Skip past the null byte.
self.core.offset += null_byte + 1;
self.core.align_offset();
if (depth == 0) {
node = .{
.name = name,
.offset = self.core.offset,
};
}
depth += 1;
},
.Property => {
const length = try self.core.read_u32_at_offset();
const name_off = try self.core.read_u32_at_offset();
const off_strings = self.core.fdt.header.off_dt_strings;
const name_raw = bytes[off_strings..][name_off..];
const name_null = std.mem.indexOfScalar(
u8,
name_raw,
0,
) orelse
return error.InvalidString;
const name = name_raw[0..name_null];
const value = bytes[self.core.offset..][0..length];
self.core.offset += length;
self.core.align_offset();
if (mode != .Nodes) {
const prop: Property = .{
.name = name,
.value = value,
};
if (mode == .Values) {
if (depth == 0) return .{ .Property = prop };
} else {
if (depth == 0) return prop;
}
}
},
.EndNode => {
if (depth == 0) return null;
depth -= 1;
if (mode != .Properties) {
if (mode == .Values) {
if (depth == 0) return .{ .Node = node.? };
} else {
if (depth == 0) return node.?;
}
}
},
.End => return null,
.Nop => continue,
}
}
}
};
}
pub const ReservedMemoryEntry = struct {
address: u64,
size: u64,
};
const ReservedMemoryIterator = struct {
fdt: *const Fdt,
offset: usize,
/// Return the next reserved memory entry in the parsing context, if any.
pub fn next(self: *ReservedMemoryIterator) IterError!?ReservedMemoryEntry {
const address = try self.read_u64_at_offset();
const size = try self.read_u64_at_offset();
if (address == 0 and size == 0) return null;
return .{ .address = address, .size = size };
}
fn read_u64_at_offset(self: *ReservedMemoryIterator) IterError!u64 {
const value = std.mem.readInt(
u64,
self.fdt.bytes[self.offset..][0..8],
.big,
);
if (self.offset + 8 >= self.fdt.bytes.len) {
return error.Truncated;
}
self.offset += @sizeOf(u64);
return value;
}
};
/// Initialize a flattened device tree from a byte slice.
pub fn init(bytes: []const u8) Error!Fdt {
if (bytes.len < @sizeOf(Header)) {
return Error.InvalidSize;
}
const header = rtl.mem.read_struct(Header, bytes, .big);
if (header.magic != fdt_magic) {
return Error.InvalidMagic;
}
if (bytes.len != header.total_size) {
return Error.InvalidSize;
}
// Only version 17 is supported.
if (header.version != 17) {
return Error.MalformedHeader;
}
return .{
.bytes = bytes,
.header = header,
};
}
/// Return the tree's root node
pub fn root(self: *const Fdt) Node {
// Skip begin_node tag and name.
const offset = self.header.off_dt_struct + 8;
return .{
.name = "\x00",
.offset = offset,
};
}
/// Return the index of the boot CPU id.
pub fn boot_cpu_id(self: *const Fdt) u32 {
return self.header.boot_cpuid_phys;
}
/// Return an iterator over the reserved memory map entries.
pub fn reserved_memory_iter(self: *const Fdt) ReservedMemoryIterator {
return .{
.fdt = self,
.offset = self.header.off_mem_rsvmap,
};
}
/// Evaluate a device tree path and return the value, if found.
/// Trailing slashes are not handled and will return `NotFound`.
pub fn eval_path(self: *const Fdt, path: []const u8) EvalError!Value {
var path_iter = std.mem.splitScalar(u8, path, '/');
var cur_node = self.root();
// Skip '/'
_ = path_iter.next();
while (path_iter.next()) |component| {
var it = cur_node.iter(self, .Values);
var found = false;
inner: while (try it.next()) |val| {
switch (val) {
.Property => |prop| {
if (std.mem.eql(u8, prop.name, component)) {
if (path_iter.peek() != null) {
// Path got cut short by a property.
return error.NotANode;
}
return .{ .Property = prop };
}
},
.Node => |node| {
if (std.mem.eql(u8, node.name, component)) {
if (path_iter.peek() == null) {
// Node path was passed.
return .{ .Node = node };
}
// Go deeper.
cur_node = node;
found = true;
break :inner;
}
},
}
}
if (!found) {
return error.NotFound;
}
}
unreachable;
}
// /dts-v1/;
// /memreserve/ 0x80000000 0x100000;
// /memreserve/ 0xDEADBEEF 0xC001C0DE;
// / {
// compatible = "zag";
// model = "dummy";
// blabla = <0x69420>;
// test-empty;
//
// beatles {
// names = "john,paul,george,ringo";
// };
// empty_node {
// };
// nest {
// grandchild {};
// };
// };
test "parses the reserved memory" {
const bytes align(4) = @embedFile("test.dtb").*;
var fdt = (try Fdt.init(&bytes)).reserved_memory_iter();
var val = try fdt.next() orelse unreachable;
try std.testing.expectEqual(0x80000000, val.address);
try std.testing.expectEqual(0x100000, val.size);
val = try fdt.next() orelse unreachable;
try std.testing.expectEqual(0xDEADBEEF, val.address);
try std.testing.expectEqual(0xC001C0DE, val.size);
// Ensure there are no more nodes.
try std.testing.expectEqual(null, try fdt.next());
}
test "parses root node properties" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
var it = fdt.root().iter(&fdt, .Properties);
// Check 'compatible'
var prop = try it.next() orelse
return error.TestUnexpectedResult;
try std.testing.expectEqualStrings("compatible", prop.name);
try std.testing.expectEqualSlices(
u8,
"zag\x00",
prop.value,
);
prop = try it.next() orelse
return error.TestUnexpectedResult;
// Check 'model'
try std.testing.expectEqualStrings("model", prop.name);
try std.testing.expectEqualSlices(
u8,
"dummy\x00",
prop.value,
);
prop = try it.next() orelse
return error.TestUnexpectedResult;
// Check 'blabla'
try std.testing.expectEqualStrings("blabla", prop.name);
try std.testing.expectEqual(
0x69420,
prop.as_type(u32),
);
}
test "parses empty property" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
var it = fdt.root().iter(&fdt, .Properties);
// Skip other properties.
_ = try it.next();
_ = try it.next();
_ = try it.next();
const prop = try it.next() orelse
return error.TestUnexpectedResult;
// Check 'test-empty'
try std.testing.expectEqualStrings("test-empty", prop.name);
try std.testing.expectEqualSlices(
u8,
&.{},
prop.value,
);
}
test "parses child node" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
var it = fdt.root().iter(&fdt, .Nodes);
// Check 'beatles'.
const node = try it.next() orelse
return error.TestUnexpectedResult;
try std.testing.expectEqualStrings("beatles", node.name);
var node_iter = node.iter(&fdt, .Properties);
// Check the names of the beatles.
const prop = try node_iter.next() orelse
return error.TestUnexpectedResult;
try std.testing.expectEqualStrings("names", prop.name);
try std.testing.expectEqualSlices(
u8,
"john,paul,george,ringo\x00",
prop.value,
);
// Ensure the node ends here.
try std.testing.expectEqual(null, try node_iter.next());
}
test "parses empty node" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
var it = fdt.root().iter(&fdt, .Nodes);
// Skip beatles node.
_ = try it.next();
// Check 'empty_node'.
const node = try it.next() orelse
return error.TestUnexpectedResult;
try std.testing.expectEqualStrings("empty_node", node.name);
var node_iter = node.iter(&fdt, .Properties);
// Ensure it ends immediately.
try std.testing.expectEqual(null, try node_iter.next());
}
test "parses nested node" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
var it = fdt.root().iter(&fdt, .Nodes);
// Skip beatles node.
_ = try it.next();
// Skip empty node.
_ = try it.next();
// Check 'nest'.
const node = try it.next() orelse
return error.TestUnexpectedResult;
var node_iter = node.iter(&fdt, .Nodes);
// Ensure its child is 'grandchild'
const child_node = try node_iter.next() orelse
return error.TestUnexpectedResult;
try std.testing.expectEqualStrings("grandchild", child_node.name);
// Ensure this has nothing else.
try std.testing.expectEqual(null, try node_iter.next());
// Ensure we're at the end of root.
try std.testing.expectEqual(null, try it.next());
}
test "eval_path root property" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
const model_val = try fdt.eval_path("/model");
switch (model_val) {
.Property => |prop| {
try std.testing.expectEqualStrings("dummy\x00", prop.value);
},
else => return error.TestUnexpectedResult,
}
}
test "eval_path NotANode" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
try std.testing.expectError(
EvalError.NotANode,
fdt.eval_path("/model/something"),
);
}
test "eval_path nested property" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
const names_val = try fdt.eval_path("/beatles/names");
switch (names_val) {
.Property => |prop| {
try std.testing.expectEqualStrings(
"john,paul,george,ringo\x00",
prop.value,
);
},
else => return error.TestUnexpectedResult,
}
}
test "eval_path root node" {
const bytes align(4) = @embedFile("test.dtb").*;
const fdt = try Fdt.init(&bytes);
const node_val = try fdt.eval_path("/nest");
switch (node_val) {
.Node => |node| {
try std.testing.expectEqualStrings("nest", node.name);
},
else => return error.TestUnexpectedResult,
}
}
+8
View File
@@ -0,0 +1,8 @@
//! Library abstracting over firmware interactions.
const std = @import("std");
pub const fdt = @import("fdt.zig");
comptime {
_ = std.testing.refAllDecls(@This());
}
Binary file not shown.