// Encoding a node tree into the flattened blob format.
//
// The writer is the inverse of `header.mbt` and `struct_block.mbt`: it lays out
// the four blocks in the order the format requires and fills in the size
// fields afterwards, since every offset depends on sizes that are only known
// once the blocks are built.
//
// Property names are deduplicated through a string table. dtc emits each name
// once, at the position of its first use, and later properties point back at
// it; matching that order is what makes the output byte-identical rather than
// merely equivalent.
///|
/// A name table for the strings block.
///
/// Insertion order matters: dtc writes a name the first time it appears and
/// never moves it, so a tree encoded in source order reproduces the same block
/// layout. A hash map alone would not, which is why the offsets are also kept
/// in a list.
pub(all) struct StringTable {
/// Bytes accumulated so far, NUL-terminated entries.
bytes : Buffer
/// Offsets keyed by name.
offsets : Map[String, Int32]
}
///|
/// Creates an empty table.
pub fn StringTable::new() -> StringTable {
{ bytes: Buffer(), offsets: Map([]), }
}
///|
/// Returns the offset of `name`, appending it if it is not present yet.
fn StringTable::intern(self : StringTable, name : String) -> Int32 {
match self.offsets.get(name) {
Some(at) => at
None => {
let at = self.bytes.length()
for i in 0.. Bytes {
self.bytes.to_bytes()
}
///|
/// Writes a big-endian 32-bit word.
fn put_u32(out : Buffer, v : UInt32) -> Unit {
let raw = v.reinterpret_as_int()
out.write_byte(((raw >> 24) & 0xFF).to_byte())
out.write_byte(((raw >> 16) & 0xFF).to_byte())
out.write_byte(((raw >> 8) & 0xFF).to_byte())
out.write_byte((raw & 0xFF).to_byte())
}
///|
/// Writes a big-endian 32-bit word from a cell value.
fn put_cell(out : Buffer, v : UInt64) -> Unit {
let value = (v & 0xFFFF_FFFFUL).to_int()
out.write_byte(((value >> 24) & 0xFF).to_byte())
out.write_byte(((value >> 16) & 0xFF).to_byte())
out.write_byte(((value >> 8) & 0xFF).to_byte())
out.write_byte((value & 0xFF).to_byte())
}
///|
/// Pads the buffer to a 4-byte boundary with NUL.
fn align_buffer(out : Buffer) -> Unit {
while out.length() % 4 != 0 {
out.write_byte((0 : Int32).to_byte())
}
}
///|
/// Encodes one property value into the byte form the blob stores.
///
/// Values are concatenated in source order with no separator, which is what
/// makes `compatible = "a", "b"` come out as one string table entry rather
/// than two cells. Strings keep their trailing NUL; cells widen to 32 or 64
/// bits depending on the part; a reference has no encoding yet and is refused
/// here so that a caller cannot silently get a wrong blob.
fn encode_value(parts : Array[ValuePart], out : Buffer) -> Unit raise DtbError {
for part in parts {
match part {
Text(s) => {
for i in 0..
for c in cells {
put_cell(out, c)
}
TypedCells(width, cells) =>
for c in cells {
match width {
8UL => out.write_byte((c.to_int() & 0xFF).to_byte())
16UL => {
let v = c.to_int() & 0xFFFF
out.write_byte(((v >> 8) & 0xFF).to_byte())
out.write_byte((v & 0xFF).to_byte())
}
32UL => put_cell(out, c)
_ => {
let raw = c.to_int()
for shift in [56, 48, 40, 32, 24, 16, 8, 0] {
out.write_byte(((raw >> shift) & 0xFF).to_byte())
}
}
}
}
Bytes(bs) =>
for b in bs {
out.write_byte(b)
}
Ref(label) =>
raise Unsupported(
what="reference &\{label}, call resolve_references first",
)
PathRef(path) =>
raise Unsupported(
what="path reference &\{path}, call resolve_references first",
)
Refs(_) => raise Unsupported(what="cell list containing a reference")
}
}
}
///|
/// Emits a node and its descendants into the struct block.
///
/// Order is properties first, then children, which is what the format requires
/// and what dtc produces. A child's `BEGIN_NODE` comes after its parent's
/// properties so a reader can attach properties as it walks without buffering.
fn write_node(
node : SourceNode,
strings : StringTable,
out : Buffer,
) -> Unit raise DtbError {
put_u32(out, 0x1)
// The root node is named by the empty string, matching `/` in the source.
for i in 0.. Bytes raise DtbError {
let strings = StringTable::new()
let struct_block = Buffer()
write_node(file.root, strings, struct_block)
put_u32(struct_block, 0x9)
let strings_bytes = strings.to_bytes()
let memreserve = Buffer()
for (address, size) in file.memreserves {
put_u64(memreserve, address)
put_u64(memreserve, size)
}
// The map ends with an all-zero entry, which is also what an empty map is.
put_u64(memreserve, 0UL)
put_u64(memreserve, 0UL)
let struct_bytes = struct_block.to_bytes()
let memreserve_bytes = memreserve.to_bytes()
let blocks = memreserve_bytes.length() + struct_bytes.length()
let total = 40 + blocks + strings_bytes.length()
let out = Buffer()
put_u32(out, 0xD00D_FEED)
put_u32(out, total.reinterpret_as_uint())
let off_struct = 40 + memreserve_bytes.length()
put_u32(out, off_struct.reinterpret_as_uint())
let off_strings = off_struct + struct_bytes.length()
put_u32(out, off_strings.reinterpret_as_uint())
put_u32(out, 40)
put_u32(out, 17)
put_u32(out, 16)
put_u32(out, 0)
put_u32(out, strings_bytes.length().reinterpret_as_uint())
put_u32(out, struct_bytes.length().reinterpret_as_uint())
for i in 0.. Unit {
let raw = v.to_int()
for shift in [56, 48, 40, 32, 24, 16, 8, 0] {
out.write_byte(((raw >> shift) & 0xFF).to_byte())
}
}