Skip to main content

zerodds_flatdata/
locator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! ShmLocator — Wire-Format fuer PID_SHM_LOCATOR (Spec §3.1).
4//!
5//! Layout (little-endian):
6//!
7//! ```text
8//! +--- ShmLocator value ---+
9//! | 0x00 | u32 | hostname_hash (FNV-1a)
10//! | 0x04 | u32 | uid (POSIX uid_t)
11//! | 0x08 | u32 | slot_count
12//! | 0x0c | u32 | slot_size
13//! | 0x10 |     | segment_path: u32 length + UTF-8 + 0 + pad to 4
14//! +------------------------+
15//! ```
16//!
17//! Caller-Layer (Discovery) sendet das via PID_SHM_LOCATOR=0x8001
18//! im SEDP-Sample. Reader auf demselben Host matcht (siehe
19//! `SameHostMatch`).
20
21extern crate alloc;
22use alloc::string::String;
23use alloc::vec::Vec;
24
25/// SHM-Locator: alle Daten die ein Same-Host-Reader braucht um zu
26/// einem Writer-SHM-Segment zu attachen.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ShmLocator {
29    /// FNV-1a Hash des Hostnamens. Same-Host-Match-Anker.
30    pub hostname_hash: u32,
31    /// POSIX-UID des Writer-Prozesses. Verhindert Cross-User-Attaches
32    /// auf shared Hosts.
33    pub uid: u32,
34    /// Anzahl Slots im Segment.
35    pub slot_count: u32,
36    /// Slot-Total-Size (Header + Daten + Padding).
37    pub slot_size: u32,
38    /// SHM-Segment-Pfad (z.B. `/zddspub_<entity_id>`).
39    pub segment_path: String,
40}
41
42/// Fehler beim Encode/Decode.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum LocatorError {
45    /// Buffer zu kurz fuer den fixen Header (16 byte).
46    TruncatedHeader,
47    /// String-Length-Prefix passt nicht zum Buffer.
48    TruncatedString,
49    /// Path enthaelt Non-UTF-8.
50    InvalidUtf8,
51    /// Path zu lang (> 256 byte als DoS-Cap).
52    PathTooLong,
53}
54
55impl core::fmt::Display for LocatorError {
56    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57        match self {
58            Self::TruncatedHeader => f.write_str("ShmLocator: truncated 16-byte header"),
59            Self::TruncatedString => f.write_str("ShmLocator: string length out of buffer"),
60            Self::InvalidUtf8 => f.write_str("ShmLocator: segment_path is not UTF-8"),
61            Self::PathTooLong => f.write_str("ShmLocator: segment_path > 256 bytes"),
62        }
63    }
64}
65
66#[cfg(feature = "std")]
67impl std::error::Error for LocatorError {}
68
69/// Maximale Pfad-Laenge (DoS-Cap).
70const MAX_PATH_LEN: usize = 256;
71
72impl ShmLocator {
73    /// Encoded little-endian. Layout siehe Modul-Doku.
74    ///
75    /// # Errors
76    /// `PathTooLong` wenn `segment_path.len() > 256`.
77    pub fn to_bytes_le(&self) -> Result<Vec<u8>, LocatorError> {
78        let path_bytes = self.segment_path.as_bytes();
79        if path_bytes.len() > MAX_PATH_LEN {
80            return Err(LocatorError::PathTooLong);
81        }
82        // CDR-String: u32 length (incl. null-terminator) + UTF-8 + null + padding to 4.
83        let str_len = u32::try_from(path_bytes.len() + 1).unwrap_or(u32::MAX);
84        let mut out = Vec::with_capacity(16 + 4 + path_bytes.len() + 4);
85        out.extend_from_slice(&self.hostname_hash.to_le_bytes());
86        out.extend_from_slice(&self.uid.to_le_bytes());
87        out.extend_from_slice(&self.slot_count.to_le_bytes());
88        out.extend_from_slice(&self.slot_size.to_le_bytes());
89        out.extend_from_slice(&str_len.to_le_bytes());
90        out.extend_from_slice(path_bytes);
91        out.push(0);
92        while out.len() % 4 != 0 {
93            out.push(0);
94        }
95        Ok(out)
96    }
97
98    /// Decoded aus little-endian-Bytes.
99    ///
100    /// # Errors
101    /// `TruncatedHeader`, `TruncatedString`, `InvalidUtf8`, `PathTooLong`.
102    pub fn from_bytes_le(bytes: &[u8]) -> Result<Self, LocatorError> {
103        if bytes.len() < 20 {
104            return Err(LocatorError::TruncatedHeader);
105        }
106        let hostname_hash = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
107        let uid = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
108        let slot_count = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
109        let slot_size = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
110        let str_len = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]) as usize;
111        if str_len > MAX_PATH_LEN + 1 {
112            return Err(LocatorError::PathTooLong);
113        }
114        if 20 + str_len > bytes.len() {
115            return Err(LocatorError::TruncatedString);
116        }
117        // String inkl. null-terminator.
118        let raw = &bytes[20..20 + str_len];
119        let str_no_null = if raw.last() == Some(&0) {
120            &raw[..raw.len() - 1]
121        } else {
122            raw
123        };
124        let segment_path =
125            core::str::from_utf8(str_no_null).map_err(|_| LocatorError::InvalidUtf8)?;
126        Ok(Self {
127            hostname_hash,
128            uid,
129            slot_count,
130            slot_size,
131            segment_path: segment_path.into(),
132        })
133    }
134}
135
136/// FNV-1a Hash des Hostnamens (32-bit). Wird vom Writer beim Discovery
137/// gesetzt; Reader prueft gegen den eigenen.
138#[must_use]
139pub fn fnv1a_32(bytes: &[u8]) -> u32 {
140    const OFFSET: u32 = 0x811c_9dc5;
141    const PRIME: u32 = 0x0100_0193;
142    let mut h = OFFSET;
143    for b in bytes {
144        h ^= u32::from(*b);
145        h = h.wrapping_mul(PRIME);
146    }
147    h
148}
149
150/// Same-Host-Match-Helper. Caller passes (lokaler hostname, lokaler uid)
151/// und einen empfangenen `ShmLocator`; liefert `true` wenn beides
152/// uebereinstimmt — d.h. wir koennen mmap auf das Segment.
153#[must_use]
154pub fn is_same_host(local_hostname: &str, local_uid: u32, locator: &ShmLocator) -> bool {
155    let local_hash = fnv1a_32(local_hostname.as_bytes());
156    local_hash == locator.hostname_hash && local_uid == locator.uid
157}
158
159#[cfg(test)]
160#[allow(clippy::expect_used, clippy::unwrap_used)]
161mod tests {
162    use super::*;
163
164    fn sample() -> ShmLocator {
165        ShmLocator {
166            hostname_hash: fnv1a_32(b"node1.local"),
167            uid: 1000,
168            slot_count: 16,
169            slot_size: 128,
170            segment_path: "/zddspub_AB12CD".into(),
171        }
172    }
173
174    #[test]
175    fn roundtrip_le() {
176        let l = sample();
177        let bytes = l.to_bytes_le().expect("encode");
178        let l2 = ShmLocator::from_bytes_le(&bytes).expect("decode");
179        assert_eq!(l, l2);
180    }
181
182    #[test]
183    fn truncated_header_errors() {
184        assert_eq!(
185            ShmLocator::from_bytes_le(&[0u8; 19]),
186            Err(LocatorError::TruncatedHeader)
187        );
188    }
189
190    #[test]
191    fn path_too_long_errors() {
192        let mut l = sample();
193        l.segment_path = "x".repeat(MAX_PATH_LEN + 1);
194        assert_eq!(l.to_bytes_le(), Err(LocatorError::PathTooLong));
195    }
196
197    #[test]
198    fn fnv1a_known_value() {
199        // Offizieller FNV-1a-Test-Vektor: hash("hello") = 0x4f9f2cab.
200        assert_eq!(fnv1a_32(b"hello"), 0x4f9f_2cab);
201    }
202
203    #[test]
204    fn same_host_match_positive() {
205        let l = sample();
206        assert!(is_same_host("node1.local", 1000, &l));
207    }
208
209    #[test]
210    fn same_host_mismatch_uid() {
211        let l = sample();
212        assert!(!is_same_host("node1.local", 999, &l));
213    }
214
215    #[test]
216    fn same_host_mismatch_hostname() {
217        let l = sample();
218        assert!(!is_same_host("node2.local", 1000, &l));
219    }
220}