zeppelin_embed/lifecycle/namespace_batch/
portable.rs1use super::*;
3
4pub(super) const ROOT_MAGIC: &[u8] = b"ZENS0003";
5const REFERENCE_MAGIC: &[u8] = b"ZENR0002";
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct NamespaceRootId(u128);
10
11impl NamespaceRootId {
12 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 pub fn generate() -> std::io::Result<Self> {
25 Self::from_entropy(fill_entropy)
26 }
27
28 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 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 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
215pub(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 return Ok(None);
232 };
233 let Some(parent) = directory.parent() else {
234 return Ok(None);
235 };
236 if !reference.starts_with(REFERENCE_MAGIC) {
237 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 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}