Skip to main content

native_ipc_core/
layout.rs

1//! Checked construction and validation of independent directional regions.
2
3use alloc::vec::Vec;
4use core::fmt;
5use core::ops::Range;
6
7use crate::codec::{VERSION_MAJOR, VERSION_MINOR};
8use crate::slot::{
9    ACKNOWLEDGEMENT_CELL_SIZE, AcknowledgementReaderBinding, AcknowledgementWriterBinding,
10    ReaderSlotBinding, SLOT_HEADER_SIZE, WriterSlotBinding,
11};
12
13/// Cache-line granularity used for concurrently accessed records.
14pub const CACHE_LINE: u64 = 64;
15/// Region signature stored in every mapping header.
16pub const REGION_MAGIC: [u8; 8] = *b"NIPCREG\0";
17/// Manually encoded region header size.
18pub const REGION_HEADER_SIZE: u64 = 128;
19
20/// A validated, nonzero numeric region role.
21#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
22pub struct RoleId(u32);
23
24impl RoleId {
25    /// Creates a role, rejecting the reserved zero value.
26    pub const fn new(value: u32) -> Option<Self> {
27        if value == 0 { None } else { Some(Self(value)) }
28    }
29
30    /// Returns the fixed-width wire value.
31    pub const fn get(self) -> u32 {
32        self.0
33    }
34}
35
36/// One of the two authenticated endpoints of a connection.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38#[repr(u32)]
39pub enum Endpoint {
40    /// Endpoint that initiated the connection.
41    Initiator = 1,
42    /// Endpoint accepted or inherited the connection.
43    Responder = 2,
44}
45
46impl Endpoint {
47    const fn from_raw(raw: u32) -> Option<Self> {
48        match raw {
49            1 => Some(Self::Initiator),
50            2 => Some(Self::Responder),
51            _ => None,
52        }
53    }
54}
55
56/// Capacity of one independently permissioned region.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct RegionSpec {
59    /// Numeric role unique within the connection.
60    pub role: RoleId,
61    /// Sole endpoint allowed to write this region.
62    pub writer: Endpoint,
63    /// Number of fixed-capacity ring slots.
64    pub slot_count: u32,
65    /// Maximum opaque payload bytes in each slot.
66    pub payload_bytes: u32,
67    /// Number of independently routed acknowledgement cells.
68    pub acknowledgement_count: u32,
69}
70
71/// Proposed per-slot acknowledgement route validated during region composition.
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub struct AcknowledgementRouteSpec {
74    /// Role whose mapping owns the acknowledgement cell.
75    pub owner: RoleId,
76    /// Producer role whose slot is acknowledged.
77    pub target: RoleId,
78    /// Slot index in the target region.
79    pub slot_index: u32,
80    /// Cell index in the owner region.
81    pub cell_index: u32,
82}
83
84/// A composition-validated, exact per-slot acknowledgement route.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct AcknowledgementRoute {
87    owner: RoleId,
88    target: RoleId,
89    slot_index: u32,
90    cell_index: u32,
91}
92
93impl AcknowledgementRoute {
94    pub(crate) const fn validated(
95        owner: RoleId,
96        target: RoleId,
97        slot_index: u32,
98        cell_index: u32,
99    ) -> Self {
100        Self {
101            owner,
102            target,
103            slot_index,
104            cell_index,
105        }
106    }
107    /// Returns the role that owns the acknowledgement cell.
108    pub const fn owner(self) -> RoleId {
109        self.owner
110    }
111    /// Returns the producer role being acknowledged.
112    pub const fn target(self) -> RoleId {
113        self.target
114    }
115    /// Returns the exact target slot index.
116    pub const fn slot_index(self) -> u32 {
117        self.slot_index
118    }
119    /// Returns the exact owner cell index.
120    pub const fn cell_index(self) -> u32 {
121        self.cell_index
122    }
123}
124
125/// Bounds applied while calculating or validating a region.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct LayoutLimits {
128    /// Maximum complete mapping size.
129    pub maximum_mapping_size: u64,
130    /// Maximum slots in one region.
131    pub maximum_slot_count: u32,
132    /// Maximum acknowledgement cells in one region.
133    pub maximum_acknowledgement_count: u32,
134    /// Maximum opaque bytes in one slot.
135    pub maximum_payload_bytes: u32,
136}
137
138/// Checked layouts for caller-configured independent directional regions.
139#[derive(Clone, Debug)]
140pub struct RegionSetLayout {
141    regions: Vec<RegionLayout>,
142    routes: Vec<AcknowledgementRoute>,
143}
144
145impl RegionSetLayout {
146    /// Calculates layouts and rejects empty or duplicate role sets.
147    pub fn calculate(
148        schema_id: [u8; 32],
149        generation: u64,
150        specs: &[RegionSpec],
151        route_specs: &[AcknowledgementRouteSpec],
152        limits: LayoutLimits,
153    ) -> Result<Self, LayoutError> {
154        if generation == 0 {
155            return Err(LayoutError::ZeroGeneration);
156        }
157        if specs.is_empty() {
158            return Err(LayoutError::EmptyRegionSet);
159        }
160        let mut regions = Vec::new();
161        regions
162            .try_reserve_exact(specs.len())
163            .map_err(|_| LayoutError::AllocationFailed)?;
164        for (index, spec) in specs.iter().copied().enumerate() {
165            if specs[..index].iter().any(|prior| prior.role == spec.role) {
166                return Err(LayoutError::DuplicateRole(spec.role));
167            }
168            regions.push(RegionLayout::calculate(
169                schema_id, generation, spec, limits,
170            )?);
171        }
172        let routes = validate_routes(&regions, route_specs)?;
173        Ok(Self { regions, routes })
174    }
175
176    /// Returns all independent layouts.
177    pub fn regions(&self) -> &[RegionLayout] {
178        &self.regions
179    }
180
181    /// Finds a layout by validated numeric role.
182    pub fn region(&self, role: RoleId) -> Option<&RegionLayout> {
183        self.regions.iter().find(|region| region.role() == role)
184    }
185
186    /// Returns the validated route for one producer slot.
187    pub fn acknowledgement_route(
188        &self,
189        target: RoleId,
190        slot_index: u32,
191    ) -> Option<AcknowledgementRoute> {
192        self.routes
193            .iter()
194            .copied()
195            .find(|route| route.target == target && route.slot_index == slot_index)
196    }
197
198    /// Returns all exact acknowledgement routes.
199    pub fn acknowledgement_routes(&self) -> &[AcknowledgementRoute] {
200        &self.routes
201    }
202}
203
204/// Layout of one independent, single-writer mapping.
205#[derive(Clone, Debug)]
206pub struct RegionLayout {
207    header: RegionHeader,
208}
209
210impl RegionLayout {
211    fn calculate(
212        schema_id: [u8; 32],
213        generation: u64,
214        spec: RegionSpec,
215        limits: LayoutLimits,
216    ) -> Result<Self, LayoutError> {
217        validate_counts(spec, limits)?;
218        let acknowledgement_offset = REGION_HEADER_SIZE;
219        let acknowledgement_len = u64::from(spec.acknowledgement_count)
220            .checked_mul(ACKNOWLEDGEMENT_CELL_SIZE)
221            .ok_or(LayoutError::Overflow)?;
222        let slots_offset = align_up(
223            acknowledgement_offset
224                .checked_add(acknowledgement_len)
225                .ok_or(LayoutError::Overflow)?,
226            CACHE_LINE,
227        )?;
228        let slot_stride = align_up(
229            SLOT_HEADER_SIZE
230                .checked_add(u64::from(spec.payload_bytes))
231                .ok_or(LayoutError::Overflow)?,
232            CACHE_LINE,
233        )?;
234        let slots_len = slot_stride
235            .checked_mul(u64::from(spec.slot_count))
236            .ok_or(LayoutError::Overflow)?;
237        let total_size = slots_offset
238            .checked_add(slots_len)
239            .ok_or(LayoutError::Overflow)?;
240        if slot_stride > u64::from(u32::MAX)
241            || total_size > limits.maximum_mapping_size
242            || total_size > usize::MAX as u64
243        {
244            return Err(LayoutError::LimitExceeded);
245        }
246        Ok(Self {
247            header: RegionHeader {
248                total_size,
249                schema_id,
250                generation,
251                role: spec.role.get(),
252                writer: spec.writer as u32,
253                acknowledgement_offset,
254                acknowledgement_count: spec.acknowledgement_count,
255                acknowledgement_stride: ACKNOWLEDGEMENT_CELL_SIZE as u32,
256                slots_offset,
257                slot_count: spec.slot_count,
258                slot_stride: slot_stride as u32,
259                payload_capacity: spec.payload_bytes,
260            },
261        })
262    }
263
264    /// Returns the region role.
265    pub const fn role(&self) -> RoleId {
266        RoleId(self.header.role)
267    }
268
269    /// Returns the sole writer endpoint.
270    pub const fn writer(&self) -> Endpoint {
271        match Endpoint::from_raw(self.header.writer) {
272            Some(writer) => writer,
273            None => unreachable!(),
274        }
275    }
276
277    /// Returns the exact mapping size.
278    pub const fn total_size(&self) -> u64 {
279        self.header.total_size
280    }
281
282    /// Returns the fixed slot count.
283    pub const fn slot_count(&self) -> u32 {
284        self.header.slot_count
285    }
286
287    /// Returns the per-slot payload capacity.
288    pub const fn payload_capacity(&self) -> u32 {
289        self.header.payload_capacity
290    }
291
292    /// Initializes a quiescent mapping with a manually encoded header and zero metadata.
293    pub fn encode_into(&self, destination: &mut [u8]) -> Result<(), LayoutError> {
294        let total = usize::try_from(self.header.total_size).map_err(|_| LayoutError::Overflow)?;
295        if destination.len() < total {
296            return Err(LayoutError::MappingTooSmall {
297                required: self.header.total_size,
298                actual: destination.len() as u64,
299            });
300        }
301        destination[..total].fill(0);
302        encode_header(
303            &self.header,
304            &mut destination[..REGION_HEADER_SIZE as usize],
305        );
306        for slot in 0..self.header.slot_count {
307            let start = self.header.slots_offset as usize
308                + slot as usize * self.header.slot_stride as usize;
309            put_u64(destination, start, self.header.generation);
310        }
311        Ok(())
312    }
313}
314
315/// Expected identity for quiescent mapping validation.
316#[derive(Clone, Copy, Debug, Eq, PartialEq)]
317pub struct ValidationExpectations {
318    /// Exact protocol schema.
319    pub schema_id: [u8; 32],
320    /// Exact nonzero connection generation.
321    pub generation: u64,
322    /// Expected numeric role.
323    pub role: RoleId,
324    /// Expected sole writer endpoint.
325    pub writer: Endpoint,
326    /// Maximum accepted complete mapping size.
327    pub maximum_mapping_size: u64,
328}
329
330/// Owned metadata and checked ranges copied from one validated mapping.
331///
332/// This type never retains or returns a slice into cross-process storage.
333#[derive(Clone, Debug)]
334pub struct ValidatedRegionLayout {
335    header: RegionHeader,
336    acknowledgements: Range<usize>,
337    slots: Range<usize>,
338    mapping_size: usize,
339    routes: Vec<AcknowledgementRoute>,
340}
341
342impl ValidatedRegionLayout {
343    /// Returns whether this validation belongs to an exact composed topology.
344    pub fn matches_topology(&self, topology: &RegionSetLayout) -> bool {
345        topology
346            .region(self.role())
347            .is_some_and(|region| region.header == self.header)
348            && self.routes.len() == topology.routes.len()
349            && self
350                .routes
351                .iter()
352                .all(|route| topology.routes.contains(route))
353    }
354    /// Validates a mapping while it is quiescent and before peer mutation begins.
355    ///
356    /// # Safety
357    ///
358    /// No process may mutate `bytes` for the duration of this call. The caller
359    /// mapping must be the exact future capability range.
360    pub unsafe fn validate(
361        bytes: &[u8],
362        expected: ValidationExpectations,
363        topology: &RegionSetLayout,
364    ) -> Result<Self, LayoutError> {
365        if expected.generation == 0 {
366            return Err(LayoutError::ZeroGeneration);
367        }
368        if bytes.len() < REGION_HEADER_SIZE as usize {
369            return Err(LayoutError::MappingTooSmall {
370                required: REGION_HEADER_SIZE,
371                actual: bytes.len() as u64,
372            });
373        }
374        validate_header_encoding(&bytes[..REGION_HEADER_SIZE as usize])?;
375        let header = decode_header(&bytes[..REGION_HEADER_SIZE as usize]);
376        validate_header(&header, bytes.len(), expected)?;
377        match topology.region(expected.role) {
378            Some(region) if region.header == header => {}
379            _ => return Err(LayoutError::TopologyMismatch),
380        }
381        let acknowledgements_end = header
382            .acknowledgement_offset
383            .checked_add(
384                u64::from(header.acknowledgement_count)
385                    .checked_mul(u64::from(header.acknowledgement_stride))
386                    .ok_or(LayoutError::Overflow)?,
387            )
388            .ok_or(LayoutError::Overflow)?;
389        let acknowledgements = checked_range(
390            header.acknowledgement_offset,
391            acknowledgements_end,
392            header.total_size,
393            bytes.len(),
394        )?;
395        if bytes[acknowledgements.clone()]
396            .iter()
397            .any(|byte| *byte != 0)
398        {
399            return Err(LayoutError::AcknowledgementNotZero);
400        }
401        if bytes[header.total_size as usize..]
402            .iter()
403            .any(|byte| *byte != 0)
404        {
405            return Err(LayoutError::CapabilityPaddingNotZero);
406        }
407        let minimum_stride = align_up(
408            SLOT_HEADER_SIZE
409                .checked_add(u64::from(header.payload_capacity))
410                .ok_or(LayoutError::Overflow)?,
411            CACHE_LINE,
412        )?;
413        if u64::from(header.slot_stride) != minimum_stride {
414            return Err(LayoutError::BadSlotStride);
415        }
416        let slots_end = header
417            .slots_offset
418            .checked_add(
419                u64::from(header.slot_stride)
420                    .checked_mul(u64::from(header.slot_count))
421                    .ok_or(LayoutError::Overflow)?,
422            )
423            .ok_or(LayoutError::Overflow)?;
424        if slots_end != header.total_size {
425            return Err(LayoutError::BadTotalSize);
426        }
427        let slots = checked_range(
428            header.slots_offset,
429            slots_end,
430            header.total_size,
431            bytes.len(),
432        )?;
433        for slot in 0..header.slot_count {
434            let start = slots.start + slot as usize * header.slot_stride as usize;
435            if get_u64(bytes, start) != header.generation
436                || bytes[start + 8..start + SLOT_HEADER_SIZE as usize]
437                    .iter()
438                    .any(|byte| *byte != 0)
439            {
440                return Err(LayoutError::SlotMetadataNotInitialized);
441            }
442        }
443        Ok(Self {
444            header,
445            acknowledgements,
446            slots,
447            mapping_size: bytes.len(),
448            routes: topology.routes.clone(),
449        })
450    }
451
452    /// Returns the validated role.
453    pub const fn role(&self) -> RoleId {
454        RoleId(self.header.role)
455    }
456
457    /// Returns the validated generation.
458    pub const fn generation(&self) -> u64 {
459        self.header.generation
460    }
461
462    /// Returns the exact validated native capability size, including zero padding.
463    pub const fn mapping_size(&self) -> usize {
464        self.mapping_size
465    }
466
467    /// Returns a checked complete slot range without granting memory access.
468    pub fn slot_range(&self, slot: u32) -> Result<Range<usize>, LayoutError> {
469        if slot >= self.header.slot_count {
470            return Err(LayoutError::SlotOutOfBounds {
471                slot,
472                count: self.header.slot_count,
473            });
474        }
475        let start = self
476            .slots
477            .start
478            .checked_add(slot as usize * self.header.slot_stride as usize)
479            .ok_or(LayoutError::Overflow)?;
480        let end = start
481            .checked_add(self.header.slot_stride as usize)
482            .ok_or(LayoutError::Overflow)?;
483        Ok(start..end)
484    }
485
486    pub(crate) fn slot_payload_range(
487        &self,
488        slot: u32,
489        payload_len: u32,
490    ) -> Result<Range<usize>, LayoutError> {
491        if payload_len > self.header.payload_capacity {
492            return Err(LayoutError::PayloadOutOfBounds {
493                length: payload_len,
494                capacity: self.header.payload_capacity,
495            });
496        }
497        let slot = self.slot_range(slot)?;
498        let start = slot
499            .start
500            .checked_add(SLOT_HEADER_SIZE as usize)
501            .ok_or(LayoutError::Overflow)?;
502        let end = start
503            .checked_add(payload_len as usize)
504            .ok_or(LayoutError::Overflow)?;
505        if end > slot.end {
506            return Err(LayoutError::RangeOutOfBounds);
507        }
508        Ok(start..end)
509    }
510
511    /// Binds metadata for a sole writer. Read-only mappings cannot call this successfully.
512    pub(crate) fn writer_slot_binding(
513        &self,
514        route: AcknowledgementRoute,
515    ) -> Result<WriterSlotBinding, LayoutError> {
516        if route.target != self.role() {
517            return Err(LayoutError::RouteRegionMismatch);
518        }
519        self.slot_range(route.slot_index)?;
520        Ok(WriterSlotBinding::validated(
521            self.role(),
522            self.generation(),
523            self.header.payload_capacity,
524            route.slot_index,
525            self.header.slot_count,
526            route.owner,
527            route.cell_index,
528        ))
529    }
530
531    /// Binds metadata for an acquire-only reader. Writable mappings are not treated as readers.
532    pub(crate) fn reader_slot_binding(&self, slot: u32) -> Result<ReaderSlotBinding, LayoutError> {
533        self.slot_range(slot)?;
534        Ok(ReaderSlotBinding::validated(
535            self.role(),
536            self.generation(),
537            self.header.payload_capacity,
538            slot,
539            self.header.slot_count,
540        ))
541    }
542
543    /// Returns a checked acknowledgement cell range without granting access.
544    pub fn acknowledgement_range(&self, index: u32) -> Result<Range<usize>, LayoutError> {
545        if index >= self.header.acknowledgement_count {
546            return Err(LayoutError::AcknowledgementOutOfBounds {
547                index,
548                count: self.header.acknowledgement_count,
549            });
550        }
551        let start = self
552            .acknowledgements
553            .start
554            .checked_add(index as usize * self.header.acknowledgement_stride as usize)
555            .ok_or(LayoutError::Overflow)?;
556        let end = start
557            .checked_add(self.header.acknowledgement_stride as usize)
558            .ok_or(LayoutError::Overflow)?;
559        Ok(start..end)
560    }
561
562    /// Binds a store-capable acknowledgement route only for a writable mapping.
563    pub(crate) fn acknowledgement_writer_binding(
564        &self,
565        route: AcknowledgementRoute,
566    ) -> Result<AcknowledgementWriterBinding, LayoutError> {
567        if route.owner != self.role() {
568            return Err(LayoutError::RouteRegionMismatch);
569        }
570        self.acknowledgement_range(route.cell_index)?;
571        Ok(AcknowledgementWriterBinding::validated(
572            route,
573            self.generation(),
574        ))
575    }
576
577    /// Binds an acquire-only acknowledgement route only for a read-only mapping.
578    pub(crate) fn acknowledgement_reader_binding(
579        &self,
580        route: AcknowledgementRoute,
581    ) -> Result<AcknowledgementReaderBinding, LayoutError> {
582        if route.owner != self.role() {
583            return Err(LayoutError::RouteRegionMismatch);
584        }
585        self.acknowledgement_range(route.cell_index)?;
586        Ok(AcknowledgementReaderBinding::validated(
587            route,
588            self.generation(),
589        ))
590    }
591}
592
593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
594struct RegionHeader {
595    total_size: u64,
596    schema_id: [u8; 32],
597    generation: u64,
598    role: u32,
599    writer: u32,
600    acknowledgement_offset: u64,
601    acknowledgement_count: u32,
602    acknowledgement_stride: u32,
603    slots_offset: u64,
604    slot_count: u32,
605    slot_stride: u32,
606    payload_capacity: u32,
607}
608
609/// Bounded region layout validation failures.
610#[allow(missing_docs)]
611#[derive(Clone, Copy, Debug, Eq, PartialEq)]
612pub enum LayoutError {
613    /// Checked arithmetic overflowed.
614    Overflow,
615    /// Allocation of owned layout metadata failed.
616    AllocationFailed,
617    /// Generation zero is reserved.
618    ZeroGeneration,
619    /// No independent regions were configured.
620    EmptyRegionSet,
621    /// A numeric role appeared more than once.
622    DuplicateRole(RoleId),
623    /// Slot, payload, acknowledgement, or total-size limit was exceeded.
624    LimitExceeded,
625    /// Region has no slots or zero-capacity slots.
626    EmptySlots,
627    /// Mapping cannot contain the declared region.
628    MappingTooSmall { required: u64, actual: u64 },
629    /// Region signature is invalid.
630    BadMagic,
631    /// Region wire revision is unsupported.
632    BadVersion { major: u16, minor: u16 },
633    /// Encoded header size is noncanonical.
634    BadHeaderSize(u32),
635    /// Schema identity differs.
636    SchemaMismatch,
637    /// Generation differs.
638    StaleGeneration { expected: u64, actual: u64 },
639    /// Role differs.
640    UnexpectedRole { expected: RoleId, actual: u32 },
641    /// Writer endpoint differs or is invalid.
642    UnexpectedWriter,
643    /// Reserved bytes or flags are nonzero.
644    ReservedFieldSet,
645    /// Total size is invalid.
646    BadTotalSize,
647    /// Acknowledgement layout is invalid.
648    BadAcknowledgementLayout,
649    /// Acknowledgement storage was not zero before transfer.
650    AcknowledgementNotZero,
651    /// Slot layout is invalid.
652    BadSlotLayout,
653    /// Quiescent slot generation or unpublished metadata is invalid.
654    SlotMetadataNotInitialized,
655    /// Slot stride is invalid.
656    BadSlotStride,
657    /// A checked range escapes the mapping.
658    RangeOutOfBounds,
659    /// Slot index is outside the negotiated count.
660    SlotOutOfBounds { slot: u32, count: u32 },
661    /// Acknowledgement index is outside the negotiated count.
662    AcknowledgementOutOfBounds { index: u32, count: u32 },
663    /// Payload length exceeds its validated fixed-capacity slot.
664    PayloadOutOfBounds { length: u32, capacity: u32 },
665    /// Page-rounded capability padding was not zero before transfer.
666    CapabilityPaddingNotZero,
667    /// A route names a missing role, same-direction owner, or out-of-range index.
668    InvalidAcknowledgementRoute,
669    /// More than one route names a target slot or owner cell.
670    DuplicateAcknowledgementRoute,
671    /// A producer slot or acknowledgement cell has no exact route.
672    IncompleteAcknowledgementRoutes,
673    /// A validated route was applied to a different region.
674    RouteRegionMismatch,
675    /// Validated bytes do not belong to the supplied composed topology.
676    TopologyMismatch,
677}
678
679impl fmt::Display for LayoutError {
680    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
681        write!(formatter, "region layout failed validation: {self:?}")
682    }
683}
684
685#[cfg(feature = "std")]
686impl std::error::Error for LayoutError {}
687
688fn validate_counts(spec: RegionSpec, limits: LayoutLimits) -> Result<(), LayoutError> {
689    if spec.slot_count == 0 || spec.payload_bytes == 0 {
690        return Err(LayoutError::EmptySlots);
691    }
692    if spec.slot_count > limits.maximum_slot_count
693        || spec.payload_bytes > limits.maximum_payload_bytes
694        || spec.acknowledgement_count > limits.maximum_acknowledgement_count
695    {
696        return Err(LayoutError::LimitExceeded);
697    }
698    Ok(())
699}
700
701fn validate_routes(
702    regions: &[RegionLayout],
703    specs: &[AcknowledgementRouteSpec],
704) -> Result<Vec<AcknowledgementRoute>, LayoutError> {
705    let total_slots = regions.iter().try_fold(0_usize, |total, region| {
706        total
707            .checked_add(region.header.slot_count as usize)
708            .ok_or(LayoutError::Overflow)
709    })?;
710    let total_cells = regions.iter().try_fold(0_usize, |total, region| {
711        total
712            .checked_add(region.header.acknowledgement_count as usize)
713            .ok_or(LayoutError::Overflow)
714    })?;
715    if specs.len() != total_slots || specs.len() != total_cells {
716        return Err(LayoutError::IncompleteAcknowledgementRoutes);
717    }
718    let mut routes = Vec::new();
719    routes
720        .try_reserve_exact(specs.len())
721        .map_err(|_| LayoutError::AllocationFailed)?;
722    for spec in specs {
723        let owner = regions
724            .iter()
725            .find(|region| region.role() == spec.owner)
726            .ok_or(LayoutError::InvalidAcknowledgementRoute)?;
727        let target = regions
728            .iter()
729            .find(|region| region.role() == spec.target)
730            .ok_or(LayoutError::InvalidAcknowledgementRoute)?;
731        if owner.role() == target.role()
732            || owner.writer() == target.writer()
733            || spec.slot_index >= target.header.slot_count
734            || spec.cell_index >= owner.header.acknowledgement_count
735        {
736            return Err(LayoutError::InvalidAcknowledgementRoute);
737        }
738        if routes.iter().any(|route: &AcknowledgementRoute| {
739            (route.target == spec.target && route.slot_index == spec.slot_index)
740                || (route.owner == spec.owner && route.cell_index == spec.cell_index)
741        }) {
742            return Err(LayoutError::DuplicateAcknowledgementRoute);
743        }
744        routes.push(AcknowledgementRoute::validated(
745            spec.owner,
746            spec.target,
747            spec.slot_index,
748            spec.cell_index,
749        ));
750    }
751    Ok(routes)
752}
753
754fn validate_header(
755    header: &RegionHeader,
756    mapped_len: usize,
757    expected: ValidationExpectations,
758) -> Result<(), LayoutError> {
759    if header.schema_id != expected.schema_id {
760        return Err(LayoutError::SchemaMismatch);
761    }
762    if header.generation != expected.generation {
763        return Err(LayoutError::StaleGeneration {
764            expected: expected.generation,
765            actual: header.generation,
766        });
767    }
768    if header.role != expected.role.get() {
769        return Err(LayoutError::UnexpectedRole {
770            expected: expected.role,
771            actual: header.role,
772        });
773    }
774    if Endpoint::from_raw(header.writer) != Some(expected.writer) {
775        return Err(LayoutError::UnexpectedWriter);
776    }
777    if mapped_len as u64 > expected.maximum_mapping_size
778        || header.total_size > mapped_len as u64
779        || header.total_size > expected.maximum_mapping_size
780        || header.total_size > usize::MAX as u64
781    {
782        return Err(LayoutError::BadTotalSize);
783    }
784    if header.acknowledgement_offset != REGION_HEADER_SIZE
785        || header.acknowledgement_stride != ACKNOWLEDGEMENT_CELL_SIZE as u32
786    {
787        return Err(LayoutError::BadAcknowledgementLayout);
788    }
789    let acknowledgement_len = u64::from(header.acknowledgement_count)
790        .checked_mul(u64::from(header.acknowledgement_stride))
791        .ok_or(LayoutError::Overflow)?;
792    let expected_slots_offset = align_up(
793        header
794            .acknowledgement_offset
795            .checked_add(acknowledgement_len)
796            .ok_or(LayoutError::Overflow)?,
797        CACHE_LINE,
798    )?;
799    if header.slots_offset != expected_slots_offset
800        || !header.slots_offset.is_multiple_of(CACHE_LINE)
801    {
802        return Err(LayoutError::BadSlotLayout);
803    }
804    if header.slot_count == 0 || header.payload_capacity == 0 {
805        return Err(LayoutError::EmptySlots);
806    }
807    Ok(())
808}
809
810fn validate_header_encoding(bytes: &[u8]) -> Result<(), LayoutError> {
811    if bytes[0..8] != REGION_MAGIC {
812        return Err(LayoutError::BadMagic);
813    }
814    let major = get_u16(bytes, 8);
815    let minor = get_u16(bytes, 10);
816    if major != VERSION_MAJOR || minor != VERSION_MINOR {
817        return Err(LayoutError::BadVersion { major, minor });
818    }
819    let header_size = get_u32(bytes, 12);
820    if header_size != REGION_HEADER_SIZE as u32 {
821        return Err(LayoutError::BadHeaderSize(header_size));
822    }
823    if get_u32(bytes, 108) != 0 || bytes[112..128].iter().any(|byte| *byte != 0) {
824        return Err(LayoutError::ReservedFieldSet);
825    }
826    Ok(())
827}
828
829fn encode_header(header: &RegionHeader, bytes: &mut [u8]) {
830    bytes[0..8].copy_from_slice(&REGION_MAGIC);
831    put_u16(bytes, 8, VERSION_MAJOR);
832    put_u16(bytes, 10, VERSION_MINOR);
833    put_u32(bytes, 12, REGION_HEADER_SIZE as u32);
834    put_u64(bytes, 16, header.total_size);
835    bytes[24..56].copy_from_slice(&header.schema_id);
836    put_u64(bytes, 56, header.generation);
837    put_u32(bytes, 64, header.role);
838    put_u32(bytes, 68, header.writer);
839    put_u64(bytes, 72, header.acknowledgement_offset);
840    put_u32(bytes, 80, header.acknowledgement_count);
841    put_u32(bytes, 84, header.acknowledgement_stride);
842    put_u64(bytes, 88, header.slots_offset);
843    put_u32(bytes, 96, header.slot_count);
844    put_u32(bytes, 100, header.slot_stride);
845    put_u32(bytes, 104, header.payload_capacity);
846    put_u32(bytes, 108, 0);
847    bytes[112..128].fill(0);
848}
849
850fn decode_header(bytes: &[u8]) -> RegionHeader {
851    let mut schema_id = [0; 32];
852    schema_id.copy_from_slice(&bytes[24..56]);
853    RegionHeader {
854        total_size: get_u64(bytes, 16),
855        schema_id,
856        generation: get_u64(bytes, 56),
857        role: get_u32(bytes, 64),
858        writer: get_u32(bytes, 68),
859        acknowledgement_offset: get_u64(bytes, 72),
860        acknowledgement_count: get_u32(bytes, 80),
861        acknowledgement_stride: get_u32(bytes, 84),
862        slots_offset: get_u64(bytes, 88),
863        slot_count: get_u32(bytes, 96),
864        slot_stride: get_u32(bytes, 100),
865        payload_capacity: get_u32(bytes, 104),
866    }
867}
868
869fn checked_range(
870    start: u64,
871    end: u64,
872    total: u64,
873    mapped_len: usize,
874) -> Result<Range<usize>, LayoutError> {
875    if start > end || end > total || end > mapped_len as u64 || end > usize::MAX as u64 {
876        return Err(LayoutError::RangeOutOfBounds);
877    }
878    Ok(start as usize..end as usize)
879}
880
881fn align_up(value: u64, alignment: u64) -> Result<u64, LayoutError> {
882    value
883        .checked_add(alignment - 1)
884        .map(|sum| sum & !(alignment - 1))
885        .ok_or(LayoutError::Overflow)
886}
887
888fn put_u16(bytes: &mut [u8], offset: usize, value: u16) {
889    bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
890}
891fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
892    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
893}
894fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
895    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
896}
897fn get_u32(bytes: &[u8], offset: usize) -> u32 {
898    u32::from_le_bytes(
899        bytes[offset..offset + 4]
900            .try_into()
901            .expect("fixed checked range"),
902    )
903}
904fn get_u16(bytes: &[u8], offset: usize) -> u16 {
905    u16::from_le_bytes(
906        bytes[offset..offset + 2]
907            .try_into()
908            .expect("fixed checked range"),
909    )
910}
911fn get_u64(bytes: &[u8], offset: usize) -> u64 {
912    u64::from_le_bytes(
913        bytes[offset..offset + 8]
914            .try_into()
915            .expect("fixed checked range"),
916    )
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    const SCHEMA: [u8; 32] = [0x5a; 32];
924    const GENERATION: u64 = 11;
925    const ROLE_A: RoleId = RoleId::new(1).unwrap();
926    const ROLE_B: RoleId = RoleId::new(2).unwrap();
927
928    fn limits() -> LayoutLimits {
929        LayoutLimits {
930            maximum_mapping_size: 1 << 20,
931            maximum_slot_count: 32,
932            maximum_acknowledgement_count: 8,
933            maximum_payload_bytes: 4096,
934        }
935    }
936
937    fn specs() -> [RegionSpec; 2] {
938        [
939            RegionSpec {
940                role: ROLE_A,
941                writer: Endpoint::Initiator,
942                slot_count: 2,
943                payload_bytes: 128,
944                acknowledgement_count: 4,
945            },
946            RegionSpec {
947                role: ROLE_B,
948                writer: Endpoint::Responder,
949                slot_count: 4,
950                payload_bytes: 64,
951                acknowledgement_count: 2,
952            },
953        ]
954    }
955
956    fn routes() -> [AcknowledgementRouteSpec; 6] {
957        [
958            AcknowledgementRouteSpec {
959                owner: ROLE_B,
960                target: ROLE_A,
961                slot_index: 0,
962                cell_index: 0,
963            },
964            AcknowledgementRouteSpec {
965                owner: ROLE_B,
966                target: ROLE_A,
967                slot_index: 1,
968                cell_index: 1,
969            },
970            AcknowledgementRouteSpec {
971                owner: ROLE_A,
972                target: ROLE_B,
973                slot_index: 0,
974                cell_index: 0,
975            },
976            AcknowledgementRouteSpec {
977                owner: ROLE_A,
978                target: ROLE_B,
979                slot_index: 1,
980                cell_index: 1,
981            },
982            AcknowledgementRouteSpec {
983                owner: ROLE_A,
984                target: ROLE_B,
985                slot_index: 2,
986                cell_index: 2,
987            },
988            AcknowledgementRouteSpec {
989                owner: ROLE_A,
990                target: ROLE_B,
991                slot_index: 3,
992                cell_index: 3,
993            },
994        ]
995    }
996
997    fn encoded(role: RoleId) -> (RegionLayout, Vec<u8>) {
998        let set = topology();
999        let layout = set.region(role).unwrap().clone();
1000        let mut bytes = vec![0; layout.total_size() as usize];
1001        layout.encode_into(&mut bytes).unwrap();
1002        (layout, bytes)
1003    }
1004
1005    fn topology() -> RegionSetLayout {
1006        RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &routes(), limits()).unwrap()
1007    }
1008
1009    fn expected(role: RoleId, writer: Endpoint, size: u64) -> ValidationExpectations {
1010        ValidationExpectations {
1011            schema_id: SCHEMA,
1012            generation: GENERATION,
1013            role,
1014            writer,
1015            maximum_mapping_size: size,
1016        }
1017    }
1018
1019    #[test]
1020    fn configurable_regions_have_checked_independent_layouts() {
1021        let set =
1022            RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &routes(), limits()).unwrap();
1023        assert_eq!(set.regions().len(), 2);
1024        assert_eq!(set.acknowledgement_routes().len(), 6);
1025        let (layout, bytes) = encoded(ROLE_B);
1026        // SAFETY: owned vector is quiescent and its simulated permission is exact.
1027        let validated = unsafe {
1028            ValidatedRegionLayout::validate(
1029                &bytes,
1030                expected(ROLE_B, Endpoint::Responder, layout.total_size()),
1031                &set,
1032            )
1033        }
1034        .unwrap();
1035        assert_eq!(validated.role(), ROLE_B);
1036        assert_eq!(validated.slot_range(3).unwrap().len(), 128);
1037        assert_eq!(validated.acknowledgement_range(1).unwrap().len(), 64);
1038        assert!(validated.reader_slot_binding(0).is_ok());
1039        assert!(
1040            validated
1041                .writer_slot_binding(set.acknowledgement_route(ROLE_B, 0).unwrap())
1042                .is_ok()
1043        );
1044        assert!(
1045            validated
1046                .acknowledgement_reader_binding(set.acknowledgement_route(ROLE_A, 0).unwrap())
1047                .is_ok()
1048        );
1049    }
1050
1051    #[test]
1052    fn writable_layout_cannot_mint_reader_or_ack_reader_capabilities() {
1053        let set = topology();
1054        let (layout, bytes) = encoded(ROLE_B);
1055        // SAFETY: owned vector is quiescent and permission is simulated exactly.
1056        let validated = unsafe {
1057            ValidatedRegionLayout::validate(
1058                &bytes,
1059                expected(ROLE_B, Endpoint::Responder, layout.total_size()),
1060                &set,
1061            )
1062        }
1063        .unwrap();
1064        assert!(
1065            validated
1066                .writer_slot_binding(set.acknowledgement_route(ROLE_B, 0).unwrap())
1067                .is_ok()
1068        );
1069        assert!(validated.reader_slot_binding(0).is_ok());
1070        assert!(
1071            validated
1072                .acknowledgement_writer_binding(set.acknowledgement_route(ROLE_A, 0).unwrap())
1073                .is_ok()
1074        );
1075        assert!(
1076            validated
1077                .acknowledgement_reader_binding(set.acknowledgement_route(ROLE_A, 0).unwrap())
1078                .is_ok()
1079        );
1080    }
1081
1082    #[test]
1083    fn malformed_headers_offsets_slots_and_limits_fail_closed() {
1084        let topology = topology();
1085        let (layout, original) = encoded(ROLE_B);
1086        let expectation = expected(ROLE_B, Endpoint::Responder, layout.total_size());
1087        for offset in [
1088            0_usize, 8, 12, 16, 24, 56, 64, 68, 72, 84, 88, 100, 108, 112,
1089        ] {
1090            let mut bytes = original.clone();
1091            bytes[offset] ^= 1;
1092            // SAFETY: mutation occurs before validation and no peer exists.
1093            assert!(
1094                unsafe { ValidatedRegionLayout::validate(&bytes, expectation, &topology) }.is_err()
1095            );
1096        }
1097        let slot_start = get_u64(&original, 88) as usize;
1098        let mut bytes = original;
1099        bytes[slot_start + 16] = 1;
1100        // SAFETY: mutation occurs before validation and no peer exists.
1101        assert_eq!(
1102            unsafe { ValidatedRegionLayout::validate(&bytes, expectation, &topology) }.unwrap_err(),
1103            LayoutError::SlotMetadataNotInitialized
1104        );
1105    }
1106
1107    #[test]
1108    fn rejects_zero_generation_duplicate_roles_and_excess_capacity() {
1109        let topology = topology();
1110        assert_eq!(
1111            RegionSetLayout::calculate(SCHEMA, 0, &specs(), &routes(), limits()).unwrap_err(),
1112            LayoutError::ZeroGeneration
1113        );
1114        let duplicate = [specs()[0], specs()[0]];
1115        assert_eq!(
1116            RegionSetLayout::calculate(SCHEMA, 1, &duplicate, &routes(), limits()).unwrap_err(),
1117            LayoutError::DuplicateRole(ROLE_A)
1118        );
1119        let mut too_large = specs();
1120        too_large[0].slot_count = 33;
1121        assert_eq!(
1122            RegionSetLayout::calculate(SCHEMA, 1, &too_large, &routes(), limits()).unwrap_err(),
1123            LayoutError::LimitExceeded
1124        );
1125
1126        let (layout, bytes) = encoded(ROLE_A);
1127        let zero_generation = ValidationExpectations {
1128            generation: 0,
1129            ..expected(ROLE_A, Endpoint::Initiator, layout.total_size())
1130        };
1131        // SAFETY: owned vector is quiescent; the hostile expectation is deliberate.
1132        assert_eq!(
1133            unsafe { ValidatedRegionLayout::validate(&bytes, zero_generation, &topology) }
1134                .unwrap_err(),
1135            LayoutError::ZeroGeneration
1136        );
1137    }
1138
1139    #[test]
1140    fn composition_rejects_ambiguous_and_directionally_invalid_routes() {
1141        let mut shared_cell = routes();
1142        shared_cell[1].cell_index = shared_cell[0].cell_index;
1143        assert_eq!(
1144            RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &shared_cell, limits())
1145                .unwrap_err(),
1146            LayoutError::DuplicateAcknowledgementRoute
1147        );
1148
1149        let mut duplicate_slot = routes();
1150        duplicate_slot[1].slot_index = 0;
1151        assert_eq!(
1152            RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &duplicate_slot, limits())
1153                .unwrap_err(),
1154            LayoutError::DuplicateAcknowledgementRoute
1155        );
1156
1157        for hostile in [
1158            AcknowledgementRouteSpec {
1159                owner: RoleId::new(99).unwrap(),
1160                ..routes()[0]
1161            },
1162            AcknowledgementRouteSpec {
1163                target: RoleId::new(99).unwrap(),
1164                ..routes()[0]
1165            },
1166            AcknowledgementRouteSpec {
1167                slot_index: 99,
1168                ..routes()[0]
1169            },
1170            AcknowledgementRouteSpec {
1171                cell_index: 99,
1172                ..routes()[0]
1173            },
1174        ] {
1175            let mut hostile_routes = routes();
1176            hostile_routes[0] = hostile;
1177            assert_eq!(
1178                RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &hostile_routes, limits())
1179                    .unwrap_err(),
1180                LayoutError::InvalidAcknowledgementRoute
1181            );
1182        }
1183
1184        let mut wrong_direction = routes();
1185        wrong_direction[0].owner = ROLE_A;
1186        assert_eq!(
1187            RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &wrong_direction, limits())
1188                .unwrap_err(),
1189            LayoutError::InvalidAcknowledgementRoute
1190        );
1191
1192        let mut same_endpoint = specs();
1193        same_endpoint[1].writer = Endpoint::Initiator;
1194        assert_eq!(
1195            RegionSetLayout::calculate(SCHEMA, GENERATION, &same_endpoint, &routes(), limits())
1196                .unwrap_err(),
1197            LayoutError::InvalidAcknowledgementRoute
1198        );
1199
1200        assert_eq!(
1201            RegionSetLayout::calculate(SCHEMA, GENERATION, &specs(), &routes()[..5], limits())
1202                .unwrap_err(),
1203            LayoutError::IncompleteAcknowledgementRoutes
1204        );
1205    }
1206}