Skip to main content

nord_format/
layout.rs

1//! Body layouts as data.
2//!
3//! `#[bitbody]` generates an implementation of [`BodyLayout`] alongside the codec,
4//! so a body's bit map exists once in the source and is readable at runtime — for
5//! generated documentation, for `nord inspect`, for anything that wants to answer
6//! "which bits does this field own" without re-stating the layout. Nested bodies
7//! chain to their own layouts, so the whole map is one recursive walk.
8
9/// One field's placement: an inclusive bit range, MSB-first from byte 0 of the
10/// body that declares it. For the file offset a hex dump shows, add the enclosing
11/// placements and the container's body start — `0x2c` on a type-1 file, `0x18` on
12/// a type-0.
13#[derive(Clone)]
14pub struct LayoutField {
15    /// The field's registry path within its body — the field's own name. A walker
16    /// prefixes nested children with this path and a dot.
17    pub path: &'static str,
18    /// The field's Rust type, as written.
19    pub ty: &'static str,
20    pub lo: u32,
21    pub hi: u32,
22    /// The nested body's own layout, for an `#[at]` field; `None` for a leaf.
23    pub nested: Option<fn() -> &'static [LayoutField]>,
24}
25
26/// A structure whose bit map is declared once, by `#[bitbody]`.
27pub trait BodyLayout {
28    /// Every placed field, in declaration order. Bits no field claims are
29    /// preserved by the codec but have no entry here — there is no name to
30    /// report them under.
31    fn layout() -> &'static [LayoutField];
32}
33
34impl std::fmt::Debug for LayoutField {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        if self.nested.is_some() {
37            write!(
38                f,
39                "{} bytes {:#04x}..{:#04x} ({})",
40                self.path,
41                self.lo / 8,
42                (self.hi + 1) / 8,
43                self.ty,
44            )
45        } else {
46            write!(
47                f,
48                "{} bits {}..={} ({})",
49                self.path, self.lo, self.hi, self.ty
50            )
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::cbin::{self, Cbin, Header};
59    use crate::fields::{ControlKind, Unit};
60    use nord_bits_derive::bitbody;
61    use std::io::Cursor;
62
63    /// A nested body: one flag, the rest of its two bytes unclaimed.
64    #[bitbody(2)]
65    #[derive(Default)]
66    struct Inner {
67        #[bits(0..=0)]
68        pub flag: bool,
69        #[bits(4..=11)]
70        pub level: u8,
71    }
72
73    /// A body exercising both placements: a private leaf word, a nested body,
74    /// and a public leaf, with unclaimed bits in between.
75    #[bitbody(6)]
76    struct Outer {
77        #[bits(0..=15)]
78        word: u16,
79
80        #[at(0x02..0x04)]
81        pub inner: Inner,
82
83        #[bits(40..=47)]
84        pub level: u8,
85    }
86
87    fn body() -> Outer {
88        let mut b = Outer::try_from([0xab, 0xcd, 0x0f, 0xf0, 0xff, 0x00]).unwrap();
89        b.word = 0x0102;
90        b.inner.level = 0x55;
91        b.level = 7;
92        b
93    }
94
95    /// Both placement kinds serve both directions, and unclaimed bits ride along
96    /// at every level.
97    #[test]
98    fn the_codec_is_the_declaration() {
99        let raw = <[u8; 6]>::from(&body());
100        // Claimed fields change; inner and outer unclaimed bits remain verbatim.
101        assert_eq!(raw, [0x01, 0x02, 0x05, 0x50, 0xff, 0x07]);
102        let back = Outer::try_from(raw).unwrap();
103        assert_eq!(back.word, 0x0102);
104        assert_eq!(back.inner.level, 0x55);
105        assert_eq!(back.level, 7);
106    }
107
108    /// The generated `Body` impl carries a bitbody through the container whole,
109    /// both generations.
110    #[test]
111    fn a_bitbody_rides_the_container() {
112        for generation in [cbin::Generation::V1, cbin::Generation::V0] {
113            let mut header = Header::new("tstb", (2, 5), 7);
114            header.generation = generation;
115            let file = Cbin {
116                header,
117                body: body(),
118            };
119            let mut bytes = Cursor::new(Vec::new());
120            file.write_to(&mut bytes).unwrap();
121            let mut bytes = Cursor::new(bytes.into_inner());
122            let back: Cbin<Outer> = cbin::read(&mut bytes, "tstb").unwrap();
123            assert_eq!(back.header.slot(), (2, 5));
124            assert_eq!(<[u8; 6]>::from(&back.body), <[u8; 6]>::from(&body()));
125        }
126    }
127
128    /// Paths: a nested field prefixes its children with its own name, a leaf
129    /// registers under its bare name, and private fields stay unregistered.
130    #[test]
131    fn paths_recurse_through_nested_bodies() {
132        let b = body();
133        let paths: Vec<String> = b.fields().into_iter().map(|f| f.path).collect();
134        assert_eq!(paths, ["inner.flag", "inner.level", "level"]);
135
136        let mut b = body();
137        b.set_field("inner.level", "3").unwrap();
138        assert_eq!(b.inner.level, 3);
139        // ⚠️ `level` and `inner.level` are different fields: the bare name is the
140        // outer leaf, and nothing about a nested body's child reaches it.
141        b.set_field("level", "9").unwrap();
142        assert_eq!(b.level, 9);
143        assert_eq!(b.inner.level, 3);
144        assert!(b.set_field("word", "1").is_err(), "private is not a path");
145    }
146
147    /// A body whose names carry the two relations the derive binds: a morph slot beside
148    /// its parameter, a drawbar with a rank, and an orphan of each.
149    #[bitbody(6)]
150    struct Named {
151        #[bits(0..=6)]
152        pub volume: crate::components::Level,
153        #[bits(7..=14)]
154        pub volume_wheel: crate::components::MorphTarget,
155        #[bits(15..=22)]
156        pub absent_wheel: crate::components::MorphTarget,
157        #[bits(23..=26)]
158        pub drawbar_4: crate::components::Drawbar,
159        #[bits(27..=30)]
160        pub bar: crate::components::Drawbar,
161        #[bits(31..=38)]
162        #[morphs(volume)]
163        pub misnamed_wheel: crate::components::MorphTarget,
164        #[bits(39..=42)]
165        #[rank(7)]
166        pub seventh: crate::components::Drawbar,
167    }
168
169    /// The parameter is bound by name, and only where the body registers one; the rank
170    /// likewise. Neither reaches a field whose type has no use for it, and a declared
171    /// binding stands in where the name says nothing.
172    #[test]
173    fn a_name_binds_a_morph_slot_and_places_a_drawbar() {
174        let specs = Named::field_specs();
175        let of = |name: &str| specs.iter().find(|s| s.name == name).expect(name).control;
176
177        assert_eq!(
178            of("volume_wheel"),
179            ControlKind::Morph { of: Some("volume") }
180        );
181        // Nothing named `absent` in this body, so the slot stands alone.
182        assert_eq!(of("absent_wheel"), ControlKind::Morph { of: None });
183        assert_eq!(
184            of("misnamed_wheel"),
185            ControlKind::Morph { of: Some("volume") }
186        );
187        let drawbar = <crate::components::Drawbar as crate::bits::Packed>::CONTROL;
188        assert_eq!(of("drawbar_4"), drawbar.ranked(4));
189        assert_eq!(of("bar"), drawbar);
190        assert_eq!(of("seventh"), drawbar.ranked(7));
191        // The knob a morph slot is named after is untouched by the binding.
192        assert_eq!(of("volume"), ControlKind::Knob(Unit::Panel10));
193    }
194
195    /// The layout publishes every placement — including the unregistered word —
196    /// and a nested entry chains to the nested body's own layout.
197    #[test]
198    fn the_layout_is_readable_as_data() {
199        let fields = Outer::layout();
200        let rendered: Vec<String> = fields.iter().map(|f| format!("{f:?}")).collect();
201        assert_eq!(
202            rendered,
203            [
204                "word bits 0..=15 (u16)",
205                "inner bytes 0x02..0x04 (Inner)",
206                "level bits 40..=47 (u8)",
207            ],
208        );
209
210        let nested = fields[1].nested.expect("inner is nested");
211        let rendered: Vec<String> = nested().iter().map(|f| format!("{f:?}")).collect();
212        assert_eq!(
213            rendered,
214            ["flag bits 0..=0 (bool)", "level bits 4..=11 (u8)"]
215        );
216    }
217}