Skip to main content

zeppelin_embed/lifecycle/namespace_batch/
portable.rs

1//! Path-independent namespace metadata and coordinator-only preparation authority.
2use super::*;
3
4pub(super) const ROOT_MAGIC: &[u8] = b"ZENS0003";
5const REFERENCE_MAGIC: &[u8] = b"ZENR0002";
6
7/// Random, nonzero identity of a complete namespace root, independent of its path.
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct NamespaceRootId(u128);
10
11impl NamespaceRootId {
12    /// Validates a persisted identity; zero is reserved and always refused.
13    pub fn new(value: u128) -> std::io::Result<Self> {
14        if value == 0 {
15            return Err(std::io::Error::new(
16                std::io::ErrorKind::InvalidData,
17                "zero namespace root identity",
18            ));
19        }
20        Ok(Self(value))
21    }
22
23    /// Allocates one identity from OS randomness. Entropy errors and zero propagate.
24    pub fn generate() -> std::io::Result<Self> {
25        Self::from_entropy(fill_entropy)
26    }
27
28    /// Returns the persisted numeric identity.
29    pub const fn get(self) -> u128 {
30        self.0
31    }
32
33    pub(super) fn from_entropy(
34        fill: impl FnOnce(&mut [u8; 16]) -> std::io::Result<()>,
35    ) -> std::io::Result<Self> {
36        let mut bytes = [0; 16];
37        fill(&mut bytes)?;
38        Self::new(u128::from_le_bytes(bytes))
39    }
40}
41
42fn fill_entropy(output: &mut [u8; 16]) -> std::io::Result<()> {
43    #[cfg(any(target_os = "macos", target_os = "linux"))]
44    {
45        // SAFETY: output owns sixteen writable bytes, below getentropy's 256-byte limit.
46        if unsafe { libc::getentropy(output.as_mut_ptr().cast(), output.len()) } == 0 {
47            Ok(())
48        } else {
49            Err(std::io::Error::last_os_error())
50        }
51    }
52    #[cfg(windows)]
53    {
54        crate::sys::windows::fill_entropy(output)
55    }
56    #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
57    {
58        let _ = output;
59        Err(std::io::Error::new(
60            std::io::ErrorKind::Unsupported,
61            "namespace OS entropy is unsupported on this platform",
62        ))
63    }
64}
65
66fn checked_body<'a>(path: &Path, bytes: &'a [u8], magic: &[u8]) -> Result<&'a [u8], StoreError> {
67    let end = bytes
68        .len()
69        .checked_sub(8)
70        .ok_or_else(|| invalid(path, "short portable record"))?;
71    let prefix = bytes
72        .get(..end)
73        .ok_or_else(|| invalid(path, "portable record bounds"))?;
74    if bytes.len() > MAX_RECORD
75        || !prefix.starts_with(magic)
76        || bytes.get(end..) != Some(xxh3_64(prefix).to_le_bytes().as_slice())
77    {
78        return Err(invalid(path, "portable record checksum/version"));
79    }
80    prefix
81        .get(magic.len()..)
82        .ok_or_else(|| invalid(path, "portable record header"))
83}
84
85fn take<'a>(path: &Path, bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], StoreError> {
86    let value = bytes
87        .get(..n)
88        .ok_or_else(|| invalid(path, "truncated portable record"))?;
89    *bytes = bytes
90        .get(n..)
91        .ok_or_else(|| invalid(path, "portable record bounds"))?;
92    Ok(value)
93}
94
95fn integer<const N: usize>(path: &Path, bytes: &mut &[u8]) -> Result<[u8; N], StoreError> {
96    take(path, bytes, N)?
97        .try_into()
98        .map_err(|_| invalid(path, "portable integer"))
99}
100
101fn finish(mut bytes: Vec<u8>) -> Vec<u8> {
102    bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
103    bytes
104}
105
106pub(super) fn participant_id(id: NamespaceRootId, name: &str) -> Result<u128, StoreError> {
107    let path = Path::new(REFERENCE);
108    if !name_valid(name) {
109        return Err(invalid(path, "portable namespace name"));
110    }
111    let length = u16::try_from(name.len()).map_err(|_| invalid(path, "portable name length"))?;
112    let mut bytes = b"ZENSPID1".to_vec();
113    bytes.extend_from_slice(&id.get().to_le_bytes());
114    bytes.extend_from_slice(&length.to_le_bytes());
115    bytes.extend_from_slice(name.as_bytes());
116    let identity = xxhash_rust::xxh3::xxh3_128(&bytes);
117    if identity == 0 {
118        return Err(invalid(path, "zero portable participant identity"));
119    }
120    Ok(identity)
121}
122
123pub(super) fn encode_root(
124    id: NamespaceRootId,
125    descriptor: &RootDescriptor,
126) -> Result<Vec<u8>, StoreError> {
127    let path = Path::new(RECORD);
128    let descriptor = match descriptor {
129        RootDescriptor::Legacy(routes) => {
130            let bytes = encode(routes);
131            decode_routes(path, &bytes)?;
132            bytes
133        }
134        RootDescriptor::Staged(staged) => {
135            for (name, selection) in &staged.0 {
136                if selection.binding.participant != participant_id(id, name)? {
137                    return Err(invalid(path, "portable staged participant identity"));
138                }
139            }
140            encode_staged(staged)?
141        }
142    };
143    let length =
144        u32::try_from(descriptor.len()).map_err(|_| invalid(path, "portable descriptor length"))?;
145    let mut bytes = ROOT_MAGIC.to_vec();
146    bytes.extend_from_slice(&id.get().to_le_bytes());
147    bytes.extend_from_slice(&length.to_le_bytes());
148    bytes.extend_from_slice(&descriptor);
149    let bytes = finish(bytes);
150    if bytes.len() > MAX_RECORD {
151        return Err(invalid(path, "portable root too large"));
152    }
153    Ok(bytes)
154}
155
156pub(super) fn decode_root(
157    path: &Path,
158    bytes: &[u8],
159) -> Result<(NamespaceRootId, RootDescriptor), StoreError> {
160    let mut remaining = checked_body(path, bytes, ROOT_MAGIC)?;
161    let id = NamespaceRootId::new(u128::from_le_bytes(integer(path, &mut remaining)?))
162        .map_err(|e| io(path, e))?;
163    let length = usize::try_from(u32::from_le_bytes(integer(path, &mut remaining)?))
164        .map_err(|_| invalid(path, "portable descriptor length"))?;
165    // Only canonical v1/v2 descriptors are accepted: a nested v3 is refused here.
166    let descriptor = decode_descriptor(path, take(path, &mut remaining, length)?)?;
167    if !remaining.is_empty() || encode_root(id, &descriptor)? != bytes {
168        return Err(invalid(path, "noncanonical portable root"));
169    }
170    Ok((id, descriptor))
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub(super) struct Reference {
175    pub root_id: NamespaceRootId,
176    pub parent_depth: u8,
177    pub name: String,
178}
179
180pub(super) fn encode_reference(reference: &Reference) -> Result<Vec<u8>, StoreError> {
181    let path = Path::new(REFERENCE);
182    if !matches!(reference.parent_depth, 1 | 2) || !name_valid(&reference.name) {
183        return Err(invalid(path, "portable reference name/depth"));
184    }
185    let length =
186        u16::try_from(reference.name.len()).map_err(|_| invalid(path, "portable name length"))?;
187    let mut bytes = REFERENCE_MAGIC.to_vec();
188    bytes.extend_from_slice(&reference.root_id.get().to_le_bytes());
189    bytes.extend_from_slice(&[reference.parent_depth, 0]);
190    bytes.extend_from_slice(&length.to_le_bytes());
191    bytes.extend_from_slice(reference.name.as_bytes());
192    Ok(finish(bytes))
193}
194
195pub(super) fn decode_reference(path: &Path, bytes: &[u8]) -> Result<Reference, StoreError> {
196    let mut remaining = checked_body(path, bytes, REFERENCE_MAGIC)?;
197    let root_id = NamespaceRootId::new(u128::from_le_bytes(integer(path, &mut remaining)?))
198        .map_err(|e| io(path, e))?;
199    let [parent_depth, reserved] = integer(path, &mut remaining)?;
200    let length = usize::from(u16::from_le_bytes(integer(path, &mut remaining)?));
201    let name = std::str::from_utf8(take(path, &mut remaining, length)?)
202        .map_err(|_| invalid(path, "portable name ASCII"))?
203        .to_owned();
204    let reference = Reference {
205        root_id,
206        parent_depth,
207        name,
208    };
209    if reserved != 0 || !remaining.is_empty() || encode_reference(&reference)? != bytes {
210        return Err(invalid(path, "noncanonical portable reference"));
211    }
212    Ok(reference)
213}
214
215/// A portable reference prescribes the relative root; no path hash is tried on failure.
216pub(super) fn authority<'a>(
217    vfs: &dyn Vfs,
218    directory: &'a Path,
219) -> Result<Option<(&'a Path, &'a str, NamespaceRootId)>, StoreError> {
220    authority_with_preparation(vfs, directory, None)
221}
222
223pub(super) fn authority_with_preparation<'a>(
224    vfs: &dyn Vfs,
225    directory: &'a Path,
226    preparation: Option<&PrivatePreparation>,
227) -> Result<Option<(&'a Path, &'a str, NamespaceRootId)>, StoreError> {
228    let reference_path = directory.join(REFERENCE);
229    let Some(reference) = read_optional_vfs(vfs, &reference_path)? else {
230        // Only a reference claims portable membership. Absence is a plain store.
231        return Ok(None);
232    };
233    let Some(parent) = directory.parent() else {
234        return Ok(None);
235    };
236    if !reference.starts_with(REFERENCE_MAGIC) {
237        // A legacy reference cannot authorize membership in a portable root.
238        let portable_parent = root_record_vfs(vfs, parent)?.0.is_some();
239        let portable_ancestor = if parent
240            .file_name()
241            .and_then(|name| name.to_str())
242            .is_some_and(|name| name.starts_with(".ze-batch-"))
243        {
244            match parent.parent() {
245                Some(root) => root_record_vfs(vfs, root)?.0.is_some(),
246                None => false,
247            }
248        } else {
249            false
250        };
251        if portable_parent || portable_ancestor {
252            return Err(invalid(
253                &reference_path,
254                "portable namespace requires its portable reference",
255            ));
256        }
257        return Ok(None);
258    }
259    let reference = decode_reference(&reference_path, &reference)?;
260    let name = directory
261        .file_name()
262        .and_then(|name| name.to_str())
263        .ok_or_else(|| invalid(directory, "portable participant name"))?;
264    if name != reference.name {
265        return Err(invalid(directory, "portable namespace name mismatch"));
266    }
267    let root = if reference.parent_depth == 1 {
268        parent
269    } else {
270        parent
271            .parent()
272            .ok_or_else(|| invalid(directory, "portable relative root"))?
273    };
274    let (id, descriptor) = root_record_vfs(vfs, root)?;
275    if id != Some(reference.root_id) {
276        return Err(invalid(
277            directory,
278            "portable namespace requires its matching root",
279        ));
280    }
281    if let Some(preparation) = preparation
282        && (reference.parent_depth != 2
283            || preparation.root_id != reference.root_id
284            || preparation.name != name
285            || preparation.directory != directory
286            || parent.file_name().and_then(|s| s.to_str()).is_none_or(|s| {
287                !s.starts_with(".ze-batch-")
288                    || s.contains("..")
289                    || !s
290                        .bytes()
291                        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
292            }))
293    {
294        return Err(invalid(directory, "private preparation authority mismatch"));
295    }
296    if reference.parent_depth == 2 && preparation.is_none() {
297        let routes = match &descriptor {
298            RootDescriptor::Legacy(routes) => routes,
299            RootDescriptor::Staged(staged) => &staged.1,
300        };
301        if routes
302            .get(name)
303            .is_none_or(|route| root.join(route) != directory)
304        {
305            return Err(invalid(
306                directory,
307                "portable participant is outside its selected route",
308            ));
309        }
310    }
311    for path in [directory, parent] {
312        match std::fs::symlink_metadata(path) {
313            Ok(metadata) if metadata.file_type().is_symlink() => {
314                return Err(invalid(path, "portable participant location is a symlink"));
315            }
316            Ok(_) => {}
317            // Memory/crash VFS fixtures need not have corresponding OS directories.
318            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
319            Err(error) => return Err(io(path, error)),
320        }
321    }
322    Ok(Some((root, name, reference.root_id)))
323}