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}