Skip to main content

hurray_core/descriptor/
composite_member.rs

1//! Composite Member section — binary encode/decode.
2//!
3//! A composite member descriptor identifies this tensor's role (`base` or
4//! `correction`) within an **overlay** composite (`composition_rule = 0x02`; see
5//! `docs/spec/layouts/composite.md`). It is present in the wire format when the
6//! `HAS_COMPOSITE_MEMBER` flag is set, and appears after the Extension Type section.
7//!
8//! Wire layout (spec § Composite Member Section), a fixed 16-byte block:
9//! ```text
10//! member_role  uint8     (1 byte)
11//! _reserved    uint8[15] (15 bytes) — MUST be 0x00
12//! ```
13
14use crate::descriptor::cursor::{ByteCursor, ByteWriter};
15use crate::{Error, Result};
16
17/// Total byte length of the encoded Composite Member section.
18pub(crate) const COMPOSITE_MEMBER_BYTE_LEN: usize = 16;
19
20/// The role a member plays within an overlay composite.
21///
22/// v1.0 defines only `Correction` and `Base`; wire values `0x02`–`0xFF` are
23/// RESERVED for a future tombstone kind (see `docs/spec/layouts/composite.md`
24/// § Deferred) and are rejected by [`MemberRole::from_wire`].
25///
26/// # Examples
27///
28/// ```
29/// use hurray_core::descriptor::MemberRole;
30///
31/// assert_eq!(MemberRole::Correction.wire_byte(), 0x00);
32/// assert_eq!(MemberRole::Base.wire_byte(), 0x01);
33/// assert_eq!(MemberRole::from_wire(0x01).unwrap(), MemberRole::Base);
34/// assert!(MemberRole::from_wire(0x02).is_err());
35/// ```
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum MemberRole {
39    /// A scattered correction applied within its box. Wire byte `0x00`.
40    Correction,
41    /// The base member spanning the whole index space. Wire byte `0x01`.
42    Base,
43}
44
45impl MemberRole {
46    /// Returns the wire byte for this role.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use hurray_core::descriptor::MemberRole;
52    ///
53    /// assert_eq!(MemberRole::Base.wire_byte(), 0x01);
54    /// ```
55    #[inline]
56    pub fn wire_byte(self) -> u8 {
57        match self {
58            Self::Correction => 0x00,
59            Self::Base => 0x01,
60        }
61    }
62
63    /// Constructs a [`MemberRole`] from its wire byte.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`Error::InvalidMemberRole`] for any byte other than `0x00` or `0x01`.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use hurray_core::{Error, descriptor::MemberRole};
73    ///
74    /// assert_eq!(MemberRole::from_wire(0x00).unwrap(), MemberRole::Correction);
75    /// assert!(matches!(MemberRole::from_wire(0xFF), Err(Error::InvalidMemberRole(0xFF))));
76    /// ```
77    #[inline]
78    pub fn from_wire(byte: u8) -> Result<Self> {
79        match byte {
80            0x00 => Ok(Self::Correction),
81            0x01 => Ok(Self::Base),
82            _ => Err(Error::InvalidMemberRole(byte)),
83        }
84    }
85}
86
87/// The Composite Member section: a member's role within an overlay composite.
88///
89/// Carried by [`crate::descriptor::TensorDescriptor::composite_member`], gated by
90/// the `HAS_COMPOSITE_MEMBER` descriptor flag (bit 4). See
91/// `docs/spec/layouts/composite.md` § Composite Member Section and
92/// [`crate::composite::CompositeValidator`] for the cross-member rules that apply
93/// this role (first member MUST be base and span the index space; subsequent
94/// members MUST be corrections).
95///
96/// # Examples
97///
98/// ```
99/// use hurray_core::descriptor::{CompositeMemberDescriptor, MemberRole};
100///
101/// let base = CompositeMemberDescriptor::new(MemberRole::Base);
102/// assert_eq!(base.member_role, MemberRole::Base);
103/// ```
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct CompositeMemberDescriptor {
106    /// This member's role within the enclosing overlay composite.
107    pub member_role: MemberRole,
108}
109
110impl CompositeMemberDescriptor {
111    /// Creates a new [`CompositeMemberDescriptor`].
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use hurray_core::descriptor::{CompositeMemberDescriptor, MemberRole};
117    ///
118    /// let cm = CompositeMemberDescriptor::new(MemberRole::Correction);
119    /// assert_eq!(cm.member_role, MemberRole::Correction);
120    /// ```
121    pub fn new(member_role: MemberRole) -> Self {
122        Self { member_role }
123    }
124
125    /// Encodes this section into `w` as an exact [`COMPOSITE_MEMBER_BYTE_LEN`]-byte block.
126    pub(crate) fn encode_into(&self, w: &mut ByteWriter) {
127        let start = w.len();
128        w.write_u8(self.member_role.wire_byte());
129        w.write_zeros(15); // _reserved — MUST be 0x00
130        debug_assert_eq!(
131            w.len() - start,
132            COMPOSITE_MEMBER_BYTE_LEN,
133            "composite member encoded size invariant violated"
134        );
135    }
136
137    /// Decodes a [`COMPOSITE_MEMBER_BYTE_LEN`]-byte Composite Member section from `cursor`.
138    ///
139    /// # Errors
140    ///
141    /// - [`Error::InvalidMemberRole`] if `member_role` is not `0x00` or `0x01`.
142    /// - [`Error::ReservedBytesNonZero`] if any `_reserved` byte is non-zero.
143    /// - [`Error::DescriptorTruncated`] if fewer than [`COMPOSITE_MEMBER_BYTE_LEN`] bytes remain.
144    pub(crate) fn decode_from(cursor: &mut ByteCursor<'_>) -> Result<Self> {
145        let role_byte = cursor.read_u8()?;
146        let member_role = MemberRole::from_wire(role_byte)?;
147
148        let reserved = cursor.read_bytes(15)?;
149        if reserved.iter().any(|&b| b != 0) {
150            return Err(Error::ReservedBytesNonZero {
151                field: "composite_member._reserved",
152            });
153        }
154
155        Ok(Self { member_role })
156    }
157}
158
159// ── Tests ─────────────────────────────────────────────────────────────────────
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn round_trip(desc: &CompositeMemberDescriptor) -> CompositeMemberDescriptor {
166        let mut w = ByteWriter::new();
167        desc.encode_into(&mut w);
168        let bytes = w.into_vec();
169        assert_eq!(bytes.len(), COMPOSITE_MEMBER_BYTE_LEN);
170        let mut c = ByteCursor::new(&bytes, bytes.len());
171        CompositeMemberDescriptor::decode_from(&mut c).unwrap()
172    }
173
174    // ── MemberRole wire round trip ──────────────────────────────────────────
175
176    #[test]
177    fn member_role_wire_round_trip() {
178        assert_eq!(MemberRole::Correction.wire_byte(), 0x00);
179        assert_eq!(MemberRole::Base.wire_byte(), 0x01);
180        assert_eq!(MemberRole::from_wire(0x00).unwrap(), MemberRole::Correction);
181        assert_eq!(MemberRole::from_wire(0x01).unwrap(), MemberRole::Base);
182    }
183
184    #[test]
185    fn member_role_rejects_reserved_bytes() {
186        for byte in [0x02_u8, 0x50, 0xFE, 0xFF] {
187            assert!(
188                matches!(MemberRole::from_wire(byte), Err(Error::InvalidMemberRole(b)) if b == byte),
189                "0x{byte:02X} should be rejected as InvalidMemberRole"
190            );
191        }
192    }
193
194    // ── CompositeMemberDescriptor encode/decode round trip ──────────────────
195
196    #[test]
197    fn composite_member_round_trip_correction() {
198        let desc = CompositeMemberDescriptor::new(MemberRole::Correction);
199        assert_eq!(round_trip(&desc), desc);
200    }
201
202    #[test]
203    fn composite_member_round_trip_base() {
204        let desc = CompositeMemberDescriptor::new(MemberRole::Base);
205        assert_eq!(round_trip(&desc), desc);
206    }
207
208    #[test]
209    fn composite_member_encode_reserved_bytes_are_zero() {
210        let desc = CompositeMemberDescriptor::new(MemberRole::Base);
211        let mut w = ByteWriter::new();
212        desc.encode_into(&mut w);
213        let bytes = w.into_vec();
214        assert_eq!(bytes[0], 0x01); // member_role
215        assert!(
216            bytes[1..16].iter().all(|&b| b == 0),
217            "_reserved must be all-zero"
218        );
219    }
220
221    // ── Rejection when reserved bytes are non-zero ──────────────────────────
222
223    #[test]
224    fn composite_member_decode_rejects_nonzero_reserved_bytes() {
225        let mut w = ByteWriter::new();
226        w.write_u8(MemberRole::Base.wire_byte());
227        w.write_zeros(14);
228        w.write_u8(0xFF); // last reserved byte non-zero
229        let bytes = w.into_vec();
230        let mut c = ByteCursor::new(&bytes, bytes.len());
231        let err = CompositeMemberDescriptor::decode_from(&mut c).unwrap_err();
232        assert!(matches!(err, Error::ReservedBytesNonZero { .. }));
233    }
234
235    #[test]
236    fn composite_member_decode_rejects_invalid_role() {
237        let mut w = ByteWriter::new();
238        w.write_u8(0xFF); // invalid member_role
239        w.write_zeros(15);
240        let bytes = w.into_vec();
241        let mut c = ByteCursor::new(&bytes, bytes.len());
242        let err = CompositeMemberDescriptor::decode_from(&mut c).unwrap_err();
243        assert!(matches!(err, Error::InvalidMemberRole(0xFF)));
244    }
245
246    #[test]
247    fn composite_member_decode_truncated() {
248        // Only 5 bytes available, need 16.
249        let bytes = [0x00u8, 0x00, 0x00, 0x00, 0x00];
250        let mut c = ByteCursor::new(&bytes, bytes.len());
251        let err = CompositeMemberDescriptor::decode_from(&mut c).unwrap_err();
252        assert!(matches!(err, Error::DescriptorTruncated { .. }));
253    }
254}