1use super::lock::StoreLock;
8use super::{AccessMode, OpenOptions, Store, StoreError};
9use crate::ingest::{DocId, IngestDocument};
10use crate::meta::Predicate;
11use crate::vfs::{StdVfs, SyncKind, Vfs};
12use std::collections::BTreeMap;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use xxhash_rust::xxh3::xxh3_64;
16mod portable;
17mod relocate;
18pub use portable::NamespaceRootId;
19pub use relocate::namespace_relocate;
20#[cfg(any(test, feature = "test-seams"))]
21pub use relocate::namespace_relocate_with_steps;
22
23pub(crate) struct PrivatePreparation {
25 root_id: NamespaceRootId,
26 name: String,
27 directory: PathBuf,
28}
29
30impl PrivatePreparation {
31 fn new(
32 root_id: NamespaceRootId,
33 name: &str,
34 directory: &Path,
35 _coordinator: &StoreLock,
36 ) -> Self {
37 Self {
38 root_id,
39 name: name.to_owned(),
40 directory: directory.to_path_buf(),
41 }
42 }
43}
44
45pub(super) fn validate_private_preparation(
46 vfs: &dyn Vfs,
47 directory: &Path,
48 authority: Option<&PrivatePreparation>,
49) -> Result<(), StoreError> {
50 if portable::authority_with_preparation(vfs, directory, authority)?.is_none() {
51 return Err(invalid(
52 directory,
53 "private preparation requires portable authority",
54 ));
55 }
56 Ok(())
57}
58
59const RECORD: &str = ".ze-namespaces";
60const REFERENCE: &str = ".ze-namespace-root";
61const PREPARED: &str = ".ze-prepared";
62const MAGIC: &[u8] = b"ZENS0001";
63const MAX_RECORD: usize = 1024 * 1024;
64static NEXT: AtomicU64 = AtomicU64::new(0);
65type Routes = BTreeMap<String, String>;
66
67pub struct NamespaceMutation {
69 pub name: String,
71 pub options: OpenOptions,
73 pub upserts: Vec<IngestDocument>,
75 pub deletes: Vec<DocId>,
77 pub delete_where: Option<Predicate>,
79}
80
81pub struct LiveNamespaceMutation<'a> {
83 pub store: &'a Store,
85 pub mutation: NamespaceMutation,
87}
88
89pub fn namespace_batch_live(
91 root: &Path,
92 participants: Vec<LiveNamespaceMutation<'_>>,
93) -> Result<Vec<u64>, StoreError> {
94 execute_live(root, participants, &StdVfs, &mut |_| Ok(()), None, false)
95}
96
97#[cfg(any(test, feature = "test-seams"))]
99#[doc(hidden)]
100pub fn namespace_batch_live_with_steps(
101 root: &Path,
102 participants: Vec<LiveNamespaceMutation<'_>>,
103 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
104) -> Result<Vec<u64>, StoreError> {
105 execute_live(root, participants, &StdVfs, step, None, false)
106}
107
108#[cfg(any(test, feature = "test-seams"))]
110#[doc(hidden)]
111pub fn namespace_batch_live_on_vfs(
112 root: &Path,
113 participants: Vec<LiveNamespaceMutation<'_>>,
114 vfs: &dyn Vfs,
115) -> Result<Vec<u64>, StoreError> {
116 execute_live(root, participants, vfs, &mut |_| Ok(()), None, false)
117}
118
119fn execute_live(
120 root: &Path,
121 mut participants: Vec<LiveNamespaceMutation<'_>>,
122 vfs: &dyn Vfs,
123 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
124 coordinator: Option<&StoreLock>,
125 logical_delete: bool,
126) -> Result<Vec<u64>, StoreError> {
127 use crate::ingest::wal_payload::{self, TransactionBinding};
128 use std::sync::Arc;
129 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
130 let minimum = if logical_delete { 1 } else { 2 };
131 if !(minimum..=128).contains(&participants.len()) {
132 return Err(invalid(
133 &root,
134 "namespace batch requires 2..128 participants",
135 ));
136 }
137 let _coordinator = match coordinator {
138 Some(_) => None,
139 None => Some(StoreLock::acquire(&root).map_err(StoreError::Lock)?),
140 };
141 reclamation::run(vfs, &root, step)?;
142 let deleting = participants
143 .iter()
144 .any(|p| !p.mutation.deletes.is_empty() || p.mutation.delete_where.is_some());
145 if deleting && !logical_delete {
146 reclamation::require_retired_erased(vfs, &root)?;
147 }
148 normalize_accepted(vfs, &root, step)?;
149 let (mut root_id, existing_descriptor) = root_record_vfs(vfs, &root)?;
150 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
151 root_id = Some(NamespaceRootId::generate().map_err(|e| io(&root, e))?);
152 }
153 let existing_routes = match existing_descriptor {
154 RootDescriptor::Legacy(routes) => routes,
155 RootDescriptor::Staged(_) => return Err(invalid(&root, "pending adoption")),
156 };
157 let order = participants
158 .iter()
159 .enumerate()
160 .map(|(index, p)| (p.mutation.name.clone(), index))
161 .collect::<BTreeMap<_, _>>();
162 if order.len() != participants.len() {
163 return Err(invalid(&root, "duplicate participant"));
164 }
165 let mut identities = std::collections::BTreeSet::new();
166 for (name, index) in &order {
167 let p = participants
168 .get(*index)
169 .ok_or_else(|| invalid(&root, "participant index"))?;
170 if !name_valid(name) || p.mutation.options.access_mode != AccessMode::ReadWrite {
171 return Err(invalid(&root, "invalid or read-only participant"));
172 }
173 let logical = root.join(name);
174 if std::fs::symlink_metadata(&logical)
175 .map_err(|e| io(&logical, e))?
176 .file_type()
177 .is_symlink()
178 {
179 return Err(invalid(
180 &logical,
181 "namespace links are not transaction participants",
182 ));
183 }
184 let selected = std::fs::canonicalize(resolve(&logical)?).map_err(|e| io(&logical, e))?;
185 let directory = std::fs::canonicalize(&p.store.directory).map_err(|e| io(&logical, e))?;
186 if selected != directory || !identities.insert(directory) {
187 return Err(invalid(&logical, "foreign or aliased namespace handle"));
188 }
189 }
190 let ordered = order
193 .values()
194 .map(|index| {
195 participants
196 .get(*index)
197 .ok_or_else(|| invalid(&root, "participant index"))
198 })
199 .collect::<Result<Vec<_>, _>>()?;
200 let mut maintenance = Vec::new();
201 let mut states = Vec::new();
202 let mut owners = Vec::new();
203 let mut wals = Vec::new();
204 let mut actives = Vec::new();
205 for p in &ordered {
206 maintenance.push(
207 p.store
208 .maintenance
209 .lock()
210 .map_err(|_| invalid(&root, "maintenance lock"))?,
211 );
212 }
213 for p in &ordered {
214 let state = p
215 .store
216 .state
217 .lock()
218 .map_err(|_| invalid(&root, "state lock"))?;
219 match *state {
220 super::StoreState::Open => {}
221 super::StoreState::Closing => return Err(StoreError::Closing),
222 super::StoreState::Closed => return Err(StoreError::Closed),
223 }
224 states.push(state);
225 }
226 for p in &ordered {
227 let owner = p
228 .store
229 .writer_lock
230 .lock()
231 .map_err(|_| invalid(&root, "writer lock"))?;
232 if owner.is_none() {
233 return Err(StoreError::ReadOnly);
234 }
235 owners.push(owner);
236 }
237 for p in &ordered {
238 wals.push(
239 p.store
240 .wal_writer
241 .lock()
242 .map_err(|_| invalid(&root, "WAL lock"))?,
243 );
244 }
245 for p in &ordered {
246 actives.push(
247 p.store
248 .active
249 .lock()
250 .map_err(|_| invalid(&root, "active lock"))?,
251 );
252 }
253 let mut source_snapshots = Vec::new();
254 for ((p, wal), active) in ordered.iter().zip(&wals).zip(&actives) {
256 wal.as_ref()
259 .ok_or(StoreError::ReadOnly)?
260 .manifest_publication()?
261 .complete();
262 if read_optional_vfs(
263 p.store.vfs.as_ref(),
264 &p.store.directory.join(crate::ingest::PURGE_INTENT_FILE),
265 )?
266 .is_some()
267 {
268 return Err(StoreError::StoreBusy {
269 path: p.store.directory.clone(),
270 });
271 }
272 active.as_ref().ok_or(StoreError::Closed)?;
273 if !p.mutation.deletes.is_empty() || p.mutation.delete_where.is_some() {
274 p.store.require_no_snapshot_views()?;
275 }
276 let snapshot = p
277 .store
278 .snapshot
279 .read()
280 .map_err(|_| invalid(&root, "snapshot lock"))?
281 .as_ref()
282 .cloned()
283 .ok_or(StoreError::Closed)?;
284 super::resolve_open_schema(
285 true,
286 &snapshot,
287 p.mutation.options.schema.as_ref(),
288 AccessMode::ReadOnly,
289 )?;
290 super::validate_epoch_identity(
291 p.store.epoch_identity(),
292 p.mutation
293 .options
294 .epoch
295 .as_ref()
296 .map(crate::epoch::StoreEpoch::identity),
297 true,
298 AccessMode::ReadOnly,
299 )?;
300 let tokenizer = crate::fts::tokenizer::Analyzer::new(
301 p.mutation
302 .options
303 .tokenizer
304 .clone()
305 .unwrap_or_else(crate::fts::tokenizer::TokenizerConfig::text_default),
306 )
307 .map_err(StoreError::Tokenizer)?;
308 if tokenizer.config() != p.store.tokenizer.config() {
309 return Err(invalid(&root, "participant tokenizer mismatch"));
310 }
311 source_snapshots.push(snapshot);
312 }
313 let mut staged = Vec::new();
314 for (((p, wal), active), snapshot) in ordered
315 .iter()
316 .zip(&wals)
317 .zip(&actives)
318 .zip(&source_snapshots)
319 {
320 let writer = wal.as_ref().ok_or(StoreError::ReadOnly)?;
321 let current = active.as_ref().ok_or(StoreError::Closed)?;
322 match p
323 .store
324 .stage_namespace(&p.mutation, current, snapshot, writer.durable_end())
325 {
326 Ok(stage) => staged.push(stage),
327 Err(error) => {
328 for (prior, stage) in ordered.iter().zip(staged) {
329 cleanup_stage(prior.store, stage, None)?;
330 }
331 return Err(invalid(&p.store.directory, &error.to_string()));
332 }
333 }
334 }
335 let transaction = (u128::from(std::process::id()) << 96)
336 | (u128::from(
337 std::time::SystemTime::now()
338 .duration_since(std::time::UNIX_EPOCH)
339 .map_err(|_| invalid(&root, "transaction clock"))?
340 .as_nanos() as u64,
341 ) << 32)
342 | u128::from(NEXT.fetch_add(1, Ordering::Relaxed));
343 let mut descriptor = StagedDescriptor(BTreeMap::new(), existing_routes);
344 let mut lengths = Vec::new();
345 let mut selections = Vec::new();
346 for (p, stage) in ordered.iter().zip(&staged) {
347 let wal_path = p.store.directory.join("wal.ze");
348 lengths.push(p.store.vfs.open(&wal_path).map_err(|e| io(&wal_path, e))?);
349 if stage.records.is_empty() {
350 selections.push(None);
351 continue;
352 }
353 let bytes =
354 crate::manifest::encode_manifest(&stage.manifest).map_err(StoreError::Manifest)?;
355 let first_seq = wals
356 .get(selections.len())
357 .and_then(|wal| wal.as_ref())
358 .ok_or(StoreError::ReadOnly)?
359 .durable_end()
360 .checked_add(1)
361 .ok_or(StoreError::GenerationOverflow)?;
362 let binding = TransactionBinding {
363 transaction,
364 participant: match root_id {
365 Some(id) => portable::participant_id(id, &p.mutation.name)?,
366 None => participant_identity(&root, &p.mutation.name)?,
367 },
368 first_seq,
369 last_seq: first_seq
370 .checked_add(stage.records.len() as u64 - 1)
371 .ok_or(StoreError::GenerationOverflow)?,
372 manifest_digest: xxh3_64(&bytes),
373 final_generation: stage.active.generation,
374 };
375 let selection = StagedSelection {
376 manifest: format!(".ze-manifest-{transaction}"),
377 binding,
378 logical_delete,
379 };
380 descriptor
381 .0
382 .insert(p.mutation.name.clone(), selection.clone());
383 selections.push(Some(selection));
384 }
385 if descriptor.0.is_empty() {
386 let generations = ordered
387 .iter()
388 .zip(&staged)
389 .map(|(p, s)| (p.mutation.name.clone(), s.active.generation))
390 .collect::<BTreeMap<_, _>>();
391 return participants
392 .iter()
393 .map(|p| {
394 generations
395 .get(&p.mutation.name)
396 .copied()
397 .ok_or_else(|| invalid(&root, "missing generation"))
398 })
399 .collect();
400 }
401 let decision = encode_root_record(root_id, &RootDescriptor::Staged(descriptor.clone()))?;
402 let prepared = (|| {
403 if root_id.is_some() {
404 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
405 publish(
406 vfs,
407 &root,
408 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
409 step,
410 )?;
411 step("bootstrap root published").map_err(|e| io(&root, e))?;
412 } else {
413 sync_dir(vfs, &root, step)?;
416 }
417 for p in &ordered {
418 let logical = root.join(&p.mutation.name);
419 let reference = logical.join(REFERENCE);
420 if read_optional_vfs(vfs, &reference)?.is_none() {
421 publish_reference(
422 vfs,
423 &logical,
424 &encode_participant_reference(root_id, &root, &p.mutation.name, 1)?,
425 step,
426 )?;
427 step("participant reference installed").map_err(|e| io(&logical, e))?;
428 }
429 }
430 }
431 for (((p, stage), selection), wal) in ordered
432 .iter()
433 .zip(&mut staged)
434 .zip(&selections)
435 .zip(&mut wals)
436 {
437 let Some(selection) = selection else {
438 continue;
439 };
440 let bytes =
441 crate::manifest::encode_manifest(&stage.manifest).map_err(StoreError::Manifest)?;
442 durable_write(
443 p.store.vfs.as_ref(),
444 &p.store.directory.join(&selection.manifest),
445 &bytes,
446 step,
447 )?;
448 sync_dir(p.store.vfs.as_ref(), &p.store.directory, step)?;
449 let count = u32::try_from(stage.records.len())
450 .map_err(|_| invalid(&root, "prepared member count"))?;
451 let frames = stage
452 .records
453 .iter()
454 .zip(0_u32..)
455 .map(|((op, payload), index)| {
456 wal_payload::encode_prepared(selection.binding, index, count, *op, payload)
457 .map_err(|e| invalid(&root, &e.to_string()))
458 })
459 .collect::<Result<Vec<_>, _>>()?;
460 let refs = frames
461 .iter()
462 .map(|frame| (wal_payload::PREPARED_MUTATION_V1, frame.as_slice()))
463 .collect::<Vec<_>>();
464 wal.as_mut()
465 .ok_or(StoreError::ReadOnly)?
466 .commit_many(&refs)?;
467 step("prepared append").map_err(|e| io(&p.store.directory, e))?;
468 p.store
469 .vfs
470 .sync(&p.store.directory.join("wal.ze"), SyncKind::Full)
471 .map_err(|e| io(&p.store.directory, e))?;
472 step("prepared WAL sync").map_err(|e| io(&p.store.directory, e))?;
473 let active = Arc::get_mut(&mut stage.active.segment)
474 .ok_or_else(|| invalid(&root, "private active ownership"))?;
475 for (row, seq) in stage.rows.iter().zip(selection.binding.first_seq..) {
476 for row in row {
477 active.set_sequence(*row, crate::wal::LogSeq::new(seq))?;
478 }
479 }
480 }
481 if root_id.is_none() {
482 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
483 publish(
484 vfs,
485 &root,
486 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
487 step,
488 )?;
489 }
490 for p in &ordered {
491 let logical = root.join(&p.mutation.name);
492 let reference = logical.join(REFERENCE);
493 if read_optional_vfs(vfs, &reference)?.is_none() {
494 durable_write(
495 vfs,
496 &reference,
497 &encode_participant_reference(root_id, &root, &p.mutation.name, 1)?,
498 step,
499 )?;
500 sync_dir(vfs, &logical, step)?;
501 }
502 }
503 }
504 Ok::<_, StoreError>(())
505 })();
506 if let Err(error) = prepared {
507 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
508 return Err(error);
509 }
510 let mut snapshots = Vec::new();
511 for p in &ordered {
512 match p.store.snapshot.write() {
513 Ok(snapshot) => snapshots.push(snapshot),
514 Err(_) => {
515 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
516 return Err(invalid(&root, "snapshot lock"));
517 }
518 }
519 }
520 let mut publication_attempted = false;
521 let published = (|| {
522 let temporary = root.join(".ze-namespaces.tmp");
523 durable_write(vfs, &temporary, &decision, step)?;
524 let publications = wals
525 .iter()
526 .map(|wal| {
527 wal.as_ref()
528 .ok_or(StoreError::ReadOnly)?
529 .manifest_publication()
530 .map(|publication| publication.arm())
531 })
532 .collect::<Result<Vec<_>, StoreError>>()?;
533 publication_attempted = true;
534 vfs.rename(&temporary, &root.join(RECORD))
535 .map_err(|e| io(&root, e))?;
536 step("commit rename").map_err(|e| io(&root, e))?;
537 sync_dir(vfs, &root, step)?;
538 Ok(publications)
539 })();
540 let mut publications = match published {
541 Ok(publications) => publications,
542 Err(error) => {
543 if publication_attempted {
544 for wal in &mut wals {
545 **wal = None;
546 }
547 } else {
548 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
549 }
550 return Err(error);
551 }
552 };
553 let adopted = (|| {
556 for ((active, stage), snapshot) in actives.iter_mut().zip(&staged).zip(&mut snapshots) {
557 **active = Some(crate::ingest::ActiveState {
558 generation: stage.active.generation,
559 segment: Arc::clone(&stage.active.segment),
560 });
561 **snapshot = Some(Arc::clone(&stage.snapshot));
562 }
563 step("live states installed").map_err(|e| io(&root, e))?;
564 for ((p, stage), selection) in ordered.iter().zip(&staged).zip(&selections) {
565 if let Some(selection) = selection {
566 accept(
567 p.store.vfs.as_ref(),
568 &p.store.directory,
569 selection,
570 &stage.manifest,
571 step,
572 )?;
573 }
574 }
575 publish(
576 vfs,
577 &root,
578 &encode_root_record(root_id, &RootDescriptor::Legacy(descriptor.1.clone()))?,
579 step,
580 )
581 })();
582 if let Err(error) = adopted {
583 for wal in &mut wals {
584 **wal = None;
585 }
586 return Err(error);
587 }
588 drop(snapshots);
589 drop(actives);
590 let mut completed = BTreeMap::new();
591 for ((p, stage), wal) in ordered.iter().zip(&staged).zip(&mut wals) {
592 let generation = if read_optional_vfs(
593 p.store.vfs.as_ref(),
594 &p.store.directory.join(crate::ingest::PURGE_INTENT_FILE),
595 )?
596 .is_some()
597 {
598 match p
599 .store
600 .complete_namespace_purge_locked(wal.as_mut().ok_or(StoreError::ReadOnly)?)
601 {
602 Ok(generation) => generation,
603 Err(error) => {
604 for wal in &mut wals {
605 **wal = None;
606 }
607 return Err(StoreError::PurgeRecovery {
608 detail: error.to_string(),
609 });
610 }
611 }
612 } else {
613 stage.active.generation
614 };
615 publications.push(
618 wal.as_ref()
619 .ok_or(StoreError::ReadOnly)?
620 .manifest_publication()?
621 .arm(),
622 );
623 completed.insert(p.mutation.name.clone(), generation);
624 }
625 if deleting && !logical_delete {
626 reclamation::run(vfs, &root, step)?;
627 reclamation::require_retired_erased(vfs, &root)?;
628 }
629 for publication in publications {
630 publication.complete();
631 }
632 let by_name = completed;
633 drop(wals);
634 drop(owners);
635 drop(states);
636 drop(maintenance);
637 drop(ordered);
638 participants
639 .iter_mut()
640 .map(|p| {
641 by_name
642 .get(&p.mutation.name)
643 .copied()
644 .ok_or_else(|| invalid(&root, "missing generation"))
645 })
646 .collect()
647}
648
649fn cleanup_stage(
650 store: &Store,
651 stage: crate::ingest::NamespaceStage,
652 selection: Option<&StagedSelection>,
653) -> Result<(), StoreError> {
654 let mut paths = stage
655 .replacements
656 .iter()
657 .map(|id| store.directory.join(id.file_name()))
658 .collect::<Vec<_>>();
659 if let Some(selection) = selection {
660 paths.push(store.directory.join(&selection.manifest));
661 }
662 for path in paths {
663 match store.vfs.delete(&path) {
664 Ok(()) => {}
665 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
666 Err(e) => return Err(io(&path, e)),
667 }
668 }
669 store
670 .vfs
671 .sync(&store.directory, SyncKind::Full)
672 .map_err(|e| io(&store.directory, e))
673}
674
675pub(super) fn io(path: &Path, source: std::io::Error) -> StoreError {
676 StoreError::Io {
677 path: path.to_path_buf(),
678 source,
679 }
680}
681pub(crate) fn invalid(path: &Path, message: &str) -> StoreError {
682 io(
683 path,
684 std::io::Error::new(std::io::ErrorKind::InvalidData, message),
685 )
686}
687pub(super) fn name_valid(name: &str) -> bool {
688 name.as_bytes()
689 .first()
690 .is_some_and(u8::is_ascii_alphanumeric)
691 && name.len() <= 255
692 && name
693 .bytes()
694 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
695}
696pub(super) fn envelope(body: &[u8]) -> Vec<u8> {
697 let mut bytes = MAGIC.to_vec();
698 bytes.extend_from_slice(body);
699 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
700 bytes
701}
702pub(super) fn body<'a>(path: &Path, bytes: &'a [u8]) -> Result<&'a [u8], StoreError> {
703 let end = bytes
704 .len()
705 .checked_sub(8)
706 .ok_or_else(|| invalid(path, "short namespace record"))?;
707 let prefix = bytes
708 .get(..end)
709 .ok_or_else(|| invalid(path, "namespace record bounds"))?;
710 if bytes.len() > MAX_RECORD
711 || !prefix.starts_with(MAGIC)
712 || bytes.get(end..) != Some(xxh3_64(prefix).to_le_bytes().as_slice())
713 {
714 return Err(invalid(path, "namespace record checksum/version"));
715 }
716 prefix
717 .get(MAGIC.len()..)
718 .ok_or_else(|| invalid(path, "namespace record header"))
719}
720pub(super) fn read_optional(path: &Path) -> Result<Option<Vec<u8>>, StoreError> {
721 match StdVfs.open(path) {
722 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
723 Err(e) => Err(io(path, e)),
724 Ok(n) if n > MAX_RECORD as u64 => Err(invalid(path, "namespace record too large")),
725 Ok(_) => StdVfs.read(path).map(Some).map_err(|e| io(path, e)),
726 }
727}
728fn encode(routes: &Routes) -> Vec<u8> {
729 let mut text = String::new();
732 for (name, destination) in routes {
733 text.push_str(name);
734 text.push('\t');
735 text.push_str(destination);
736 text.push('\n');
737 }
738 envelope(text.as_bytes())
739}
740#[derive(Clone, Debug, Eq, PartialEq)]
741enum RootDescriptor {
742 Legacy(Routes),
743 Staged(StagedDescriptor),
744}
745fn routes(root: &Path) -> Result<Routes, StoreError> {
746 match root_descriptor_vfs(&StdVfs, root)? {
747 RootDescriptor::Legacy(routes) => Ok(routes),
748 RootDescriptor::Staged(_) => Err(invalid(
749 &root.join(RECORD),
750 "staged manifest requires transaction adoption",
751 )),
752 }
753}
754fn root_descriptor(root: &Path) -> Result<RootDescriptor, StoreError> {
755 root_descriptor_vfs(&StdVfs, root)
756}
757fn root_descriptor_vfs(vfs: &dyn Vfs, root: &Path) -> Result<RootDescriptor, StoreError> {
758 root_record_vfs(vfs, root).map(|(_, descriptor)| descriptor)
759}
760fn encode_root_record(
761 id: Option<NamespaceRootId>,
762 descriptor: &RootDescriptor,
763) -> Result<Vec<u8>, StoreError> {
764 match id {
765 Some(id) => portable::encode_root(id, descriptor),
766 None => match descriptor {
767 RootDescriptor::Legacy(routes) => Ok(encode(routes)),
768 RootDescriptor::Staged(staged) => encode_staged(staged),
769 },
770 }
771}
772
773fn encode_participant_reference(
774 id: Option<NamespaceRootId>,
775 root: &Path,
776 name: &str,
777 parent_depth: u8,
778) -> Result<Vec<u8>, StoreError> {
779 match id {
780 Some(root_id) => portable::encode_reference(&portable::Reference {
781 root_id,
782 parent_depth,
783 name: name.to_owned(),
784 }),
785 None => Ok(envelope(
786 root.to_str()
787 .ok_or_else(|| invalid(root, "root UTF-8"))?
788 .as_bytes(),
789 )),
790 }
791}
792
793fn root_record_vfs(
794 vfs: &dyn Vfs,
795 root: &Path,
796) -> Result<(Option<NamespaceRootId>, RootDescriptor), StoreError> {
797 let path = root.join(RECORD);
798 let Some(bytes) = read_optional_vfs(vfs, &path)? else {
799 return Ok((None, RootDescriptor::Legacy(Routes::new())));
800 };
801 if bytes.starts_with(portable::ROOT_MAGIC) {
802 return portable::decode_root(&path, &bytes).map(|(id, descriptor)| (Some(id), descriptor));
803 }
804 decode_descriptor(&path, &bytes).map(|descriptor| (None, descriptor))
805}
806fn decode_descriptor(path: &Path, bytes: &[u8]) -> Result<RootDescriptor, StoreError> {
807 if bytes.starts_with(STAGED_MAGIC) || bytes.starts_with(LOGICAL_STAGED_MAGIC) {
808 return decode_staged(path, bytes).map(RootDescriptor::Staged);
809 }
810 decode_routes(path, bytes).map(RootDescriptor::Legacy)
811}
812fn decode_routes(path: &Path, bytes: &[u8]) -> Result<Routes, StoreError> {
813 let text = std::str::from_utf8(body(path, bytes)?)
814 .map_err(|_| invalid(path, "namespace record UTF-8"))?;
815 let mut result = Routes::new();
816 for line in text.split_terminator('\n') {
817 let (name, destination) = line
818 .split_once('\t')
819 .ok_or_else(|| invalid(path, "namespace route"))?;
820 let (transaction, child) = destination
821 .split_once('/')
822 .ok_or_else(|| invalid(path, "namespace destination"))?;
823 if !name_valid(name)
824 || child != name
825 || !transaction.starts_with(".ze-batch-")
826 || !transaction
827 .bytes()
828 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
829 || transaction.contains("..")
830 || result
831 .insert(name.to_owned(), destination.to_owned())
832 .is_some()
833 {
834 return Err(invalid(path, "invalid or duplicate namespace route"));
835 }
836 }
837 if encode(&result) != bytes {
838 return Err(invalid(path, "noncanonical namespace record"));
839 }
840 Ok(result)
841}
842
843pub(super) fn validate_store_path(path: &Path) -> Result<(), StoreError> {
845 match std::fs::metadata(path) {
846 Ok(metadata) if !metadata.is_dir() => {
847 return Err(StoreError::NotDirectory {
848 path: path.to_path_buf(),
849 });
850 }
851 Ok(_) => {}
852 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
853 Err(error) => return Err(io(path, error)),
854 }
855 Ok(())
856}
857
858pub(super) fn resolve(path: &Path) -> Result<PathBuf, StoreError> {
860 validate_store_path(path)?;
861 let Some(parent) = path.parent() else {
862 return Ok(path.to_path_buf());
863 };
864 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
865 return Ok(path.to_path_buf());
866 };
867 let portable = portable::authority(&StdVfs, path)?;
868 let parent = portable.map_or(parent, |(root, _, _)| root);
869 let reference = if portable.is_none() {
870 read_optional(&path.join(REFERENCE))?
871 } else {
872 None
873 };
874 if let Some(reference) = reference {
875 let expected = body(&path.join(REFERENCE), &reference)?;
876 let canonical = std::fs::canonicalize(parent).map_err(|e| io(parent, e))?;
877 if canonical.to_str().map(str::as_bytes) != Some(expected)
878 || read_optional(&parent.join(RECORD))?.is_none()
879 {
880 return Err(invalid(
881 path,
882 "namespace requires its original transaction root",
883 ));
884 }
885 }
886 let selected = match root_descriptor(parent)? {
887 RootDescriptor::Legacy(mut routes) => routes.remove(name),
888 RootDescriptor::Staged(mut descriptor) => descriptor.1.remove(name),
889 };
890 match selected {
891 None => Ok(path.to_path_buf()),
892 Some(destination) => {
893 let selected = parent.join(&destination);
894 if let Some((_, _, id)) = portable
895 && portable::authority(&StdVfs, &selected)?.map(|(_, _, selected_id)| selected_id)
896 != Some(id)
897 {
898 return Err(invalid(&selected, "foreign portable route participant"));
899 }
900 let transaction = selected
901 .parent()
902 .ok_or_else(|| invalid(path, "missing transaction directory"))?;
903 let intent_path = transaction.join("intent.ze");
904 let intent = read_optional(&intent_path)?
905 .ok_or_else(|| invalid(&intent_path, "missing transaction intent"))?;
906 let intent_body = std::str::from_utf8(body(&intent_path, &intent)?)
907 .map_err(|_| invalid(&intent_path, "intent UTF-8"))?;
908 if !intent_body
909 .lines()
910 .any(|line| line == format!("{name}\t{destination}"))
911 {
912 return Err(invalid(
913 &intent_path,
914 "decision disagrees with preparation intent",
915 ));
916 }
917 let prepared_path = selected.join(PREPARED);
918 let bytes = read_optional(&prepared_path)?
919 .ok_or_else(|| invalid(&prepared_path, "missing namespace preparation"))?;
920 if body(&prepared_path, &bytes)? != destination.as_bytes() {
921 return Err(invalid(
922 &prepared_path,
923 "namespace preparation identity mismatch",
924 ));
925 }
926 for filename in ["manifest.ze", "wal.ze"] {
930 let required = selected.join(filename);
931 let length = StdVfs.open(&required).map_err(|e| io(&required, e))?;
932 if length == 0 {
933 return Err(invalid(&required, "empty committed preparation file"));
934 }
935 }
936 Ok(selected)
937 }
938 }
939}
940
941pub(super) fn durable_write(
942 vfs: &dyn Vfs,
943 path: &Path,
944 bytes: &[u8],
945 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
946) -> Result<(), StoreError> {
947 vfs.write(path, bytes).map_err(|e| io(path, e))?;
948 step("write").map_err(|e| io(path, e))?;
949 vfs.sync(path, SyncKind::Full).map_err(|e| io(path, e))?;
950 step("file sync").map_err(|e| io(path, e))
951}
952fn publish_reference(
954 vfs: &dyn Vfs,
955 directory: &Path,
956 bytes: &[u8],
957 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
958) -> Result<(), StoreError> {
959 let temporary = directory.join(".ze-namespace-root.tmp");
960 let reference = directory.join(REFERENCE);
961 durable_write(vfs, &temporary, bytes, step)?;
962 vfs.rename(&temporary, &reference)
963 .map_err(|e| io(&reference, e))?;
964 step("reference rename").map_err(|e| io(&reference, e))?;
965 sync_dir(vfs, directory, step)
966}
967
968pub(super) fn sync_dir(
969 vfs: &dyn Vfs,
970 path: &Path,
971 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
972) -> Result<(), StoreError> {
973 vfs.sync(path, SyncKind::Full).map_err(|e| io(path, e))?;
974 step("directory sync").map_err(|e| io(path, e))
975}
976
977pub(super) fn sync_portable_publication_for_open(
978 vfs: &dyn Vfs,
979 directory: &Path,
980) -> Result<(), StoreError> {
981 if let Some((root, _, _)) = portable::authority(vfs, directory)? {
982 if let Some(parent) = root.parent() {
985 let parent = if parent.as_os_str().is_empty() {
986 Path::new(".")
987 } else {
988 parent
989 };
990 sync_dir(vfs, parent, &mut |_| Ok(()))?;
991 }
992 sync_dir(vfs, root, &mut |_| Ok(()))?;
993 }
994 Ok(())
995}
996
997fn publish(
998 vfs: &dyn Vfs,
999 root: &Path,
1000 bytes: &[u8],
1001 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1002) -> Result<(), StoreError> {
1003 let temporary = root.join(".ze-namespaces.tmp");
1004 durable_write(vfs, &temporary, bytes, step)?;
1005 vfs.rename(&temporary, &root.join(RECORD))
1006 .map_err(|e| io(root, e))?;
1007 step("commit rename").map_err(|e| io(root, e))?;
1008 sync_dir(vfs, root, step)
1009}
1010
1011pub fn namespace_batch(
1020 root: &Path,
1021 mutations: Vec<NamespaceMutation>,
1022) -> Result<Vec<u64>, StoreError> {
1023 execute(root, mutations, Operation::Batch, &mut |_| Ok(()))
1024}
1025
1026#[cfg(any(test, feature = "test-seams"))]
1028#[doc(hidden)]
1029pub fn namespace_batch_with_steps(
1030 root: &Path,
1031 mutations: Vec<NamespaceMutation>,
1032 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1033) -> Result<Vec<u64>, StoreError> {
1034 execute(root, mutations, Operation::Batch, step)
1035}
1036
1037pub fn namespace_declare_cascade(
1040 root: &Path,
1041 participants: Vec<NamespaceMutation>,
1042 rule: super::CascadeRule,
1043) -> Result<(), StoreError> {
1044 execute(
1045 root,
1046 participants,
1047 Operation::Declare(rule),
1048 &mut |_| Ok(()),
1049 )
1050 .map(|_| ())
1051}
1052
1053pub fn namespace_delete_cascade(
1057 root: &Path,
1058 participants: Vec<NamespaceMutation>,
1059) -> Result<Vec<u64>, StoreError> {
1060 execute(root, participants, Operation::Cascade, &mut |_| Ok(()))
1061}
1062
1063#[cfg(any(test, feature = "test-seams"))]
1065#[doc(hidden)]
1066pub fn namespace_delete_cascade_with_steps(
1067 root: &Path,
1068 participants: Vec<NamespaceMutation>,
1069 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1070) -> Result<Vec<u64>, StoreError> {
1071 execute(root, participants, Operation::Cascade, step)
1072}
1073
1074enum Operation {
1075 Batch,
1076 Cascade,
1077 Declare(super::CascadeRule),
1078}
1079
1080fn execute(
1081 root: &Path,
1082 mut mutations: Vec<NamespaceMutation>,
1083 operation: Operation,
1084 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1085) -> Result<Vec<u64>, StoreError> {
1086 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
1087 let minimum = if matches!(operation, Operation::Batch) {
1088 2
1089 } else {
1090 1
1091 };
1092 if mutations.len() < minimum || mutations.len() > 128 {
1093 return Err(invalid(
1094 &root,
1095 "namespace batch requires 2..128 participants",
1096 ));
1097 }
1098 if !matches!(operation, Operation::Batch)
1099 && mutations.iter().any(|m| {
1100 !m.upserts.is_empty()
1101 || m.delete_where.is_some()
1102 || (matches!(operation, Operation::Declare(_)) && !m.deletes.is_empty())
1103 })
1104 {
1105 return Err(invalid(
1106 &root,
1107 "cascade operations reject upserts/filters; declarations reject deletes",
1108 ));
1109 }
1110 let _coordinator = StoreLock::acquire(&root).map_err(StoreError::Lock)?;
1111 reclamation::run(&StdVfs, &root, step)?;
1112 if root_record_vfs(&StdVfs, &root)?.0.is_some() {
1113 normalize_accepted(&StdVfs, &root, step)?;
1114 }
1115 routes(&root)?;
1116 let mut ordered = BTreeMap::new();
1117 for (index, mutation) in mutations.iter().enumerate() {
1118 if !name_valid(&mutation.name)
1119 || ordered.insert(mutation.name.clone(), index).is_some()
1120 || mutation.options.access_mode != AccessMode::ReadWrite
1121 {
1122 return Err(invalid(
1123 &root,
1124 "invalid, duplicate, or read-only namespace participant",
1125 ));
1126 }
1127 }
1128 let mut sources = BTreeMap::new();
1130 for (name, index) in &ordered {
1131 let mutation = mutations
1132 .get(*index)
1133 .ok_or_else(|| invalid(&root, "participant index"))?;
1134 let path = root.join(name);
1135 if std::fs::symlink_metadata(&path)
1136 .map_err(|e| io(&path, e))?
1137 .file_type()
1138 .is_symlink()
1139 {
1140 return Err(invalid(
1141 &path,
1142 "namespace links are not transaction participants",
1143 ));
1144 }
1145 let mut options = mutation.options.clone();
1146 options.access_mode = AccessMode::ReadOnly;
1147 drop(Store::open(&path, options)?);
1149 let mut options = mutation.options.clone();
1150 if matches!(operation, Operation::Cascade) {
1151 options.durability_mode = super::durability::DurabilityMode::Durable;
1152 options.commit_tier = super::durability::CommitTier::Durable;
1153 }
1154 sources.insert(name.clone(), Store::open(&path, options)?);
1155 }
1156 match operation {
1157 Operation::Declare(rule) => {
1158 super::cascade::declare(&root, &sources, rule, step)?;
1159 return Ok(Vec::new());
1160 }
1161 Operation::Cascade => super::cascade::expand(&root, &sources, &mut mutations)?,
1162 Operation::Batch => {}
1163 }
1164 let logical_delete = matches!(operation, Operation::Cascade);
1165 let input_order = mutations.iter().map(|m| m.name.clone()).collect::<Vec<_>>();
1166 let live = mutations
1167 .into_iter()
1168 .map(|mutation| {
1169 sources
1170 .get(&mutation.name)
1171 .map(|store| LiveNamespaceMutation { store, mutation })
1172 .ok_or_else(|| invalid(&root, "missing source"))
1173 })
1174 .collect::<Result<Vec<_>, _>>()?;
1175 let generations = execute_live(
1176 &root,
1177 live,
1178 &StdVfs,
1179 step,
1180 Some(&_coordinator),
1181 logical_delete,
1182 )?;
1183 if !logical_delete {
1184 return Ok(generations);
1185 }
1186 let mut generations = Vec::new();
1190 for name in input_order {
1191 let store = sources
1192 .get(&name)
1193 .ok_or_else(|| invalid(&root, "missing source"))?;
1194 generations.push(store.seal()?);
1195 step("cascade checkpoint").map_err(|e| io(&root, e))?;
1196 }
1197 Ok(generations)
1198}
1199
1200#[cfg(test)]
1201#[allow(clippy::expect_used)]
1202mod tests {
1203 use super::*;
1204 use crate::vfs::crash::{CrashVfs, MemoryVfs};
1205
1206 #[test]
1207 fn root_decision_power_cuts_never_publish_a_torn_or_partial_selection() {
1208 let root = Path::new("/root");
1209 let before = encode(&Routes::new());
1210 let after = encode(&Routes::from([
1211 ("a".into(), ".ze-batch-1/a".into()),
1212 ("b".into(), ".ze-batch-1/b".into()),
1213 ]));
1214 let memory = MemoryVfs::new();
1215 memory
1216 .insert(root.join(RECORD), before.clone())
1217 .expect("seed decision");
1218 let crash = CrashVfs::new(memory).expect("recorder");
1219 publish(&crash, root, &after, &mut |_| Ok(())).expect("publish");
1220 let states = crash.crash_states().expect("power cuts");
1221 assert!(!states.was_capped());
1222 for state in states.iter() {
1223 let bytes = state
1224 .vfs()
1225 .read(&root.join(RECORD))
1226 .expect("previous or new root");
1227 assert!(bytes == before || bytes == after, "{:?}", state.kind());
1228 }
1229 eprintln!("ZE-239 root power-cut states: {}", states.len());
1230 }
1231}
1232
1233#[cfg(test)]
1234#[allow(clippy::expect_used)]
1235mod descriptor_tests {
1236 use super::*;
1237 #[test]
1238 fn nonparticipant_route_survives_v2_publish() {
1239 let temporary = tempfile::tempdir().expect("root");
1240 let root = temporary.path();
1241 let destination = ".ze-batch-1/c";
1242 let selected = root.join(destination);
1243 std::fs::create_dir_all(&selected).expect("selected directory");
1244 std::fs::create_dir(root.join("c")).expect("logical directory");
1245 let routes = Routes::from([("c".into(), destination.into())]);
1246 std::fs::write(root.join(".ze-batch-1/intent.ze"), encode(&routes)).expect("intent");
1247 std::fs::write(selected.join(PREPARED), envelope(destination.as_bytes()))
1248 .expect("prepared marker");
1249 for filename in ["manifest.ze", "wal.ze"] {
1250 std::fs::write(selected.join(filename), b"retained").expect("payload");
1251 }
1252 publish(&StdVfs, root, &encode(&routes), &mut |_| Ok(())).expect("legacy publish");
1253 assert_eq!(resolve(&root.join("c")).expect("legacy resolve"), selected);
1254 let descriptor = StagedDescriptor(
1255 BTreeMap::from([(
1256 "a".into(),
1257 StagedSelection {
1258 logical_delete: false,
1259 manifest: ".ze-manifest-1".into(),
1260 binding: crate::ingest::wal_payload::TransactionBinding {
1261 transaction: 1,
1262 participant: 2,
1263 first_seq: 3,
1264 last_seq: 3,
1265 manifest_digest: 4,
1266 final_generation: 5,
1267 },
1268 },
1269 )]),
1270 routes,
1271 );
1272 publish(
1273 &StdVfs,
1274 root,
1275 &encode_staged(&descriptor).expect("v2"),
1276 &mut |_| Ok(()),
1277 )
1278 .expect("v2 publish");
1279 assert_eq!(resolve(&root.join("c")).expect("v2 resolve"), selected);
1280 }
1281
1282 #[test]
1283 #[allow(clippy::indexing_slicing)]
1284 fn staged_descriptor_round_trips_and_old_reader_rejects() {
1285 let binding = crate::ingest::wal_payload::TransactionBinding {
1286 transaction: 1,
1287 participant: 2,
1288 first_seq: 3,
1289 last_seq: 3,
1290 manifest_digest: 4,
1291 final_generation: 5,
1292 };
1293 let descriptor = StagedDescriptor(
1294 BTreeMap::from([(
1295 "a".into(),
1296 StagedSelection {
1297 logical_delete: false,
1298 manifest: ".ze-manifest-1".into(),
1299 binding,
1300 },
1301 )]),
1302 Routes::new(),
1303 );
1304 let bytes = encode_staged(&descriptor).expect("encode");
1305 assert_eq!(
1306 bytes.iter().map(|b| format!("{b:02x}")).collect::<String>(),
1307 "5a454e5330303032010000000100610e002e7a652d6d616e69666573742d3101000000000000000000000000000000020000000000000000000000000000000300000000000000030000000000000004000000000000000500000000000000100000005a454e5330303031a3d24bff963b33d2986d4df50a4f2e73"
1308 );
1309 for end in 0..bytes.len() {
1310 assert!(decode_staged(Path::new("root"), &bytes[..end]).is_err());
1311 }
1312 let mut bad = bytes.clone();
1313 bad[7] = b'3';
1314 assert!(decode_staged(Path::new("root"), &bad).is_err());
1315 let mut bad = bytes.clone();
1316 bad[40] ^= 1;
1317 assert!(decode_staged(Path::new("root"), &bad).is_err());
1318 assert!(body(Path::new("root"), &bytes).is_err());
1319 assert_eq!(
1320 decode_staged(Path::new("root"), &bytes).expect("decode"),
1321 descriptor
1322 );
1323 let mut logical = descriptor;
1324 logical.0.get_mut("a").expect("participant").logical_delete = true;
1325 let bytes = encode_staged(&logical).expect("logical encode");
1326 assert!(bytes.starts_with(LOGICAL_STAGED_MAGIC));
1327 assert!(body(Path::new("root"), &bytes).is_err());
1328 assert_eq!(
1329 decode_staged(Path::new("root"), &bytes).expect("logical decode"),
1330 logical
1331 );
1332 }
1333}
1334
1335const STAGED_MAGIC: &[u8] = b"ZENS0002";
1340const LOGICAL_STAGED_MAGIC: &[u8] = b"ZENS0003";
1341#[derive(Clone, Debug, Eq, PartialEq)]
1342struct StagedSelection {
1343 logical_delete: bool,
1344 manifest: String,
1345 binding: crate::ingest::wal_payload::TransactionBinding,
1346}
1347#[derive(Clone, Debug, Eq, PartialEq)]
1348struct StagedDescriptor(BTreeMap<String, StagedSelection>, Routes);
1349
1350fn encode_staged(descriptor: &StagedDescriptor) -> Result<Vec<u8>, StoreError> {
1351 let path = Path::new(RECORD);
1352 let logical_delete = descriptor
1353 .0
1354 .values()
1355 .next()
1356 .is_some_and(|s| s.logical_delete);
1357 let mut bytes = if logical_delete {
1358 LOGICAL_STAGED_MAGIC
1359 } else {
1360 STAGED_MAGIC
1361 }
1362 .to_vec();
1363 let count =
1364 u32::try_from(descriptor.0.len()).map_err(|_| invalid(path, "participant count"))?;
1365 if count == 0 {
1366 return Err(invalid(path, "empty staged decision"));
1367 }
1368 bytes.extend_from_slice(&count.to_le_bytes());
1369 let mut transaction = None;
1370 let mut identities = std::collections::BTreeSet::new();
1371 for (name, selection) in &descriptor.0 {
1372 if selection.logical_delete != logical_delete
1373 || !name_valid(name)
1374 || !selection.manifest.starts_with(".ze-manifest-")
1375 || !name_valid(selection.manifest.trim_start_matches('.'))
1376 || selection.manifest.contains("..")
1377 || transaction.is_some_and(|id| id != selection.binding.transaction)
1378 || !identities.insert(selection.binding.participant)
1379 {
1380 return Err(invalid(path, "staged participant identity or manifest"));
1381 }
1382 transaction = Some(selection.binding.transaction);
1383 for text in [name, &selection.manifest] {
1384 let length =
1385 u16::try_from(text.len()).map_err(|_| invalid(path, "selection length"))?;
1386 bytes.extend_from_slice(&length.to_le_bytes());
1387 bytes.extend_from_slice(text.as_bytes());
1388 }
1389 bytes.extend_from_slice(
1390 &selection
1391 .binding
1392 .encode()
1393 .map_err(|_| invalid(path, "transaction binding"))?,
1394 );
1395 }
1396 let routes = encode(&descriptor.1);
1397 let length = u32::try_from(routes.len()).map_err(|_| invalid(path, "route table length"))?;
1398 bytes.extend_from_slice(&length.to_le_bytes());
1399 bytes.extend_from_slice(&routes);
1400 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1401 if bytes.len() > MAX_RECORD {
1402 return Err(invalid(path, "staged decision too large"));
1403 }
1404 Ok(bytes)
1405}
1406fn decode_staged(path: &Path, bytes: &[u8]) -> Result<StagedDescriptor, StoreError> {
1407 let end = bytes
1408 .len()
1409 .checked_sub(8)
1410 .ok_or_else(|| invalid(path, "short staged decision"))?;
1411 let prefix = bytes
1412 .get(..end)
1413 .ok_or_else(|| invalid(path, "staged bounds"))?;
1414 if bytes.len() > MAX_RECORD
1415 || !(prefix.starts_with(STAGED_MAGIC) || prefix.starts_with(LOGICAL_STAGED_MAGIC))
1416 || bytes.get(end..) != Some(xxh3_64(prefix).to_le_bytes().as_slice())
1417 {
1418 return Err(invalid(path, "staged checksum/version"));
1419 }
1420 let mut remaining = prefix
1421 .get(8..)
1422 .ok_or_else(|| invalid(path, "staged header"))?;
1423 fn take<'a>(path: &Path, bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], StoreError> {
1424 let result = bytes
1425 .get(..n)
1426 .ok_or_else(|| invalid(path, "truncated staged decision"))?;
1427 *bytes = bytes
1428 .get(n..)
1429 .ok_or_else(|| invalid(path, "staged bounds"))?;
1430 Ok(result)
1431 }
1432 let count = u32::from_le_bytes(
1433 take(path, &mut remaining, 4)?
1434 .try_into()
1435 .map_err(|_| invalid(path, "count"))?,
1436 );
1437 let mut result = BTreeMap::new();
1438 for _ in 0..count {
1439 let mut texts = Vec::new();
1440 for _ in 0..2 {
1441 let n = u16::from_le_bytes(
1442 take(path, &mut remaining, 2)?
1443 .try_into()
1444 .map_err(|_| invalid(path, "length"))?,
1445 );
1446 texts.push(
1447 std::str::from_utf8(take(path, &mut remaining, usize::from(n))?)
1448 .map_err(|_| invalid(path, "selection UTF-8"))?
1449 .to_owned(),
1450 );
1451 }
1452 let mut texts = texts.into_iter();
1453 let name = texts.next().ok_or_else(|| invalid(path, "name"))?;
1454 let manifest = texts.next().ok_or_else(|| invalid(path, "manifest"))?;
1455 let binding =
1456 crate::ingest::wal_payload::TransactionBinding::decode(take(path, &mut remaining, 64)?)
1457 .map_err(|_| invalid(path, "transaction binding"))?;
1458 if result
1459 .insert(
1460 name,
1461 StagedSelection {
1462 manifest,
1463 binding,
1464 logical_delete: prefix.starts_with(LOGICAL_STAGED_MAGIC),
1465 },
1466 )
1467 .is_some()
1468 {
1469 return Err(invalid(path, "duplicate participant"));
1470 }
1471 }
1472 let route_length = u32::from_le_bytes(
1473 take(path, &mut remaining, 4)?
1474 .try_into()
1475 .map_err(|_| invalid(path, "route table length"))?,
1476 );
1477 let route_length =
1478 usize::try_from(route_length).map_err(|_| invalid(path, "route table length"))?;
1479 let routes = decode_routes(path, take(path, &mut remaining, route_length)?)?;
1480 let descriptor = StagedDescriptor(result, routes);
1481 if !remaining.is_empty() || encode_staged(&descriptor)? != bytes {
1482 return Err(invalid(path, "noncanonical staged decision"));
1483 }
1484 Ok(descriptor)
1485}
1486
1487fn read_optional_vfs(vfs: &dyn Vfs, path: &Path) -> Result<Option<Vec<u8>>, StoreError> {
1488 match vfs.open(path) {
1489 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
1490 Err(e) => Err(io(path, e)),
1491 Ok(n) if n > MAX_RECORD as u64 => Err(invalid(path, "namespace record too large")),
1492 Ok(_) => vfs.read(path).map(Some).map_err(|e| io(path, e)),
1493 }
1494}
1495
1496fn location(directory: &Path) -> Result<(&Path, &str), StoreError> {
1497 let name = directory
1498 .file_name()
1499 .and_then(|name| name.to_str())
1500 .ok_or_else(|| invalid(directory, "participant name"))?;
1501 let parent = directory
1502 .parent()
1503 .ok_or_else(|| invalid(directory, "participant root"))?;
1504 let root = if parent
1505 .file_name()
1506 .and_then(|name| name.to_str())
1507 .is_some_and(|name| name.starts_with(".ze-batch-"))
1508 {
1509 parent
1510 .parent()
1511 .ok_or_else(|| invalid(directory, "route root"))?
1512 } else {
1513 parent
1514 };
1515 Ok((root, name))
1516}
1517fn reader_location<'a>(
1518 vfs: &dyn Vfs,
1519 directory: &'a Path,
1520 preparation: Option<&PrivatePreparation>,
1521) -> Result<(&'a Path, &'a str, Option<NamespaceRootId>), StoreError> {
1522 if let Some((root, name, id)) =
1523 portable::authority_with_preparation(vfs, directory, preparation)?
1524 {
1525 return Ok((root, name, Some(id)));
1526 }
1527 location(directory).map(|(root, name)| (root, name, None))
1528}
1529fn reader_participant_identity(
1530 vfs: &dyn Vfs,
1531 directory: &Path,
1532 preparation: Option<&PrivatePreparation>,
1533) -> Result<u128, StoreError> {
1534 let (root, name, id) = reader_location(vfs, directory, preparation)?;
1535 match id {
1536 Some(id) => portable::participant_id(id, name),
1537 None => participant_identity(root, name),
1538 }
1539}
1540fn acceptance_path(
1541 directory: &Path,
1542 binding: crate::ingest::wal_payload::TransactionBinding,
1543) -> PathBuf {
1544 directory.join(format!(".ze-accepted-{}", binding.transaction))
1545}
1546fn accepted(
1547 vfs: &dyn Vfs,
1548 directory: &Path,
1549 binding: crate::ingest::wal_payload::TransactionBinding,
1550) -> Result<bool, StoreError> {
1551 let path = acceptance_path(directory, binding);
1552 let Some(bytes) = read_optional_vfs(vfs, &path)? else {
1553 return Ok(false);
1554 };
1555 let recorded = crate::ingest::wal_payload::TransactionBinding::decode(body(&path, &bytes)?)
1556 .map_err(|e| invalid(&path, &e.to_string()))?;
1557 if recorded != binding {
1558 return Err(invalid(&path, "local acceptance binding mismatch"));
1559 }
1560 Ok(true)
1561}
1562pub(crate) fn owns_purge_obligation(
1565 vfs: &dyn Vfs,
1566 directory: &Path,
1567 token: u64,
1568 preparation: Option<&PrivatePreparation>,
1569) -> Result<bool, StoreError> {
1570 for path in vfs.list(directory).map_err(|e| io(directory, e))? {
1571 let Some(transaction) = path
1572 .file_name()
1573 .and_then(|name| name.to_str())
1574 .and_then(|name| name.strip_prefix(".ze-accepted-"))
1575 .and_then(|name| name.parse::<u128>().ok())
1576 else {
1577 continue;
1578 };
1579 if xxh3_64(&transaction.to_le_bytes()) != token {
1580 continue;
1581 }
1582 let bytes = vfs.read(&path).map_err(|e| io(&path, e))?;
1583 let binding = crate::ingest::wal_payload::TransactionBinding::decode(body(&path, &bytes)?)
1584 .map_err(|e| invalid(&path, &e.to_string()))?;
1585 if binding.transaction != transaction {
1586 return Err(invalid(&path, "purge acceptance transaction mismatch"));
1587 }
1588 return Ok(true);
1589 }
1590 if preparation.is_none()
1591 && let Some(selected) = selection(vfs, directory)?
1592 && xxh3_64(&selected.binding.transaction.to_le_bytes()) == token
1593 {
1594 staged_manifest(vfs, directory, &selected)?;
1595 return Ok(true);
1596 }
1597 Ok(false)
1598}
1599
1600fn selection(vfs: &dyn Vfs, directory: &Path) -> Result<Option<StagedSelection>, StoreError> {
1601 let (root, name, _) = reader_location(vfs, directory, None)?;
1602 let RootDescriptor::Staged(mut descriptor) = root_descriptor_vfs(vfs, root)? else {
1603 return Ok(None);
1604 };
1605 let Some(selected) = descriptor.0.remove(name) else {
1606 return Ok(None);
1607 };
1608 let identity = reader_participant_identity(vfs, directory, None)?;
1609 if selected.binding.participant != identity {
1610 return Err(invalid(directory, "participant identity mismatch"));
1611 }
1612 Ok(Some(selected))
1613}
1614fn selected_manifest(
1615 vfs: &dyn Vfs,
1616 directory: &Path,
1617 selected: &StagedSelection,
1618) -> Result<crate::manifest::Manifest, StoreError> {
1619 let path = directory.join(&selected.manifest);
1620 let bytes = vfs.read(&path).map_err(|e| io(&path, e))?;
1621 if xxh3_64(&bytes) != selected.binding.manifest_digest {
1622 return Err(invalid(&path, "staged manifest digest mismatch"));
1623 }
1624 let manifest = crate::manifest::decode_manifest(&path.display().to_string(), &bytes)
1625 .map_err(StoreError::Manifest)?;
1626 if manifest.generation != selected.binding.final_generation {
1627 return Err(invalid(&path, "staged generation mismatch"));
1628 }
1629 Ok(manifest)
1630}
1631
1632fn staged_manifest(
1633 vfs: &dyn Vfs,
1634 directory: &Path,
1635 selected: &StagedSelection,
1636) -> Result<crate::manifest::Manifest, StoreError> {
1637 let manifest = selected_manifest(vfs, directory, selected)?;
1638 let path = directory.join(&selected.manifest);
1639 let wal = crate::wal::WalReader::open(vfs, &directory.join("wal.ze"))
1640 .map_err(StoreError::Wal)?
1641 .into_clean()
1642 .map_err(StoreError::WalRecovery)?;
1643 let selected_records = wal
1645 .records()
1646 .iter()
1647 .filter(|r| (selected.binding.first_seq..=selected.binding.last_seq).contains(&r.seq.get()))
1648 .collect::<Vec<_>>();
1649 let member_count = selected
1650 .binding
1651 .last_seq
1652 .checked_sub(selected.binding.first_seq)
1653 .and_then(|count| count.checked_add(1))
1654 .ok_or_else(|| invalid(&path, "invalid committed prepared range"))?;
1655 if selected_records.len() as u64 != member_count {
1656 return Err(invalid(&path, "committed prepared range incomplete"));
1657 }
1658 for (record, index) in selected_records.iter().zip(0_u32..) {
1659 let payload = record
1660 .payload()
1661 .map_err(|e| invalid(&path, &e.to_string()))?;
1662 let member = crate::ingest::wal_payload::decode_prepared(payload)
1663 .map_err(|e| invalid(&path, &e.to_string()))?;
1664 if record.op != crate::ingest::wal_payload::PREPARED_MUTATION_V1
1665 || member.binding != selected.binding
1666 || member.index != index
1667 {
1668 return Err(invalid(&path, "committed prepared binding mismatch"));
1669 }
1670 }
1671 Ok(manifest)
1672}
1673
1674pub(super) fn manifest_for_open(
1675 vfs: &dyn Vfs,
1676 directory: &Path,
1677 preparation: Option<&PrivatePreparation>,
1678) -> Result<PathBuf, StoreError> {
1679 let canonical = directory.join("manifest.ze");
1680 if vfs
1681 .open(&canonical)
1682 .is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound)
1683 && vfs
1684 .list(directory)
1685 .map_err(|e| io(directory, e))?
1686 .iter()
1687 .any(|path| {
1688 path.file_name()
1689 .and_then(|name| name.to_str())
1690 .is_some_and(|name| name.starts_with(".ze-accepted-"))
1691 })
1692 {
1693 return Err(invalid(&canonical, "missing locally accepted manifest"));
1694 }
1695 if preparation.is_some() {
1696 portable::authority_with_preparation(vfs, directory, preparation)?;
1697 return Ok(canonical);
1698 }
1699 match selection(vfs, directory)? {
1700 Some(selected) if !accepted(vfs, directory, selected.binding)? => {
1701 staged_manifest(vfs, directory, &selected)?;
1702 Ok(directory.join(selected.manifest))
1703 }
1704 _ => Ok(directory.join("manifest.ze")),
1705 }
1706}
1707fn accept(
1708 vfs: &dyn Vfs,
1709 directory: &Path,
1710 selected: &StagedSelection,
1711 manifest: &crate::manifest::Manifest,
1712 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1713) -> Result<(), StoreError> {
1714 let wal = crate::wal::WalReader::open(vfs, &directory.join("wal.ze"))
1717 .map_err(StoreError::Wal)?
1718 .into_clean()
1719 .map_err(StoreError::WalRecovery)?;
1720 let mut ids = Vec::new();
1721 for record in wal.records().iter().filter(|record| {
1722 (selected.binding.first_seq..=selected.binding.last_seq).contains(&record.seq.get())
1723 }) {
1724 let payload = record
1725 .payload()
1726 .map_err(|e| invalid(directory, &e.to_string()))?;
1727 let member = crate::ingest::wal_payload::decode_prepared(payload)
1728 .map_err(|e| invalid(directory, &e.to_string()))?;
1729 if member.binding != selected.binding {
1730 return Err(invalid(directory, "purge binding mismatch"));
1731 }
1732 if let crate::ingest::wal_payload::MutationPayload::Delete(deleted) = member.mutation {
1733 ids.extend(deleted);
1734 }
1735 }
1736 ids.sort_unstable();
1737 ids.dedup();
1738 if !selected.logical_delete {
1739 crate::ingest::namespace_purge_intent(vfs, directory, selected.binding.transaction, ids)
1740 .map_err(|e| StoreError::PurgeRecovery {
1741 detail: e.to_string(),
1742 })?;
1743 step("purge obligation adopted").map_err(|e| io(directory, e))?;
1744 }
1745 let bytes = crate::manifest::encode_manifest(manifest).map_err(StoreError::Manifest)?;
1746 let temporary = directory.join(".manifest.ze.tmp");
1747 durable_write(vfs, &temporary, &bytes, step)?;
1748 vfs.rename(&temporary, &directory.join("manifest.ze"))
1749 .map_err(|e| io(directory, e))?;
1750 step("accept manifest rename").map_err(|e| io(directory, e))?;
1751 sync_dir(vfs, directory, step)?;
1752 let binding = selected
1753 .binding
1754 .encode()
1755 .map_err(|e| invalid(directory, &e.to_string()))?;
1756 let committed = acceptance_path(directory, selected.binding);
1757 let temporary = committed.with_extension("tmp");
1758 durable_write(vfs, &temporary, &envelope(&binding), step)?;
1759 vfs.rename(&temporary, &committed)
1760 .map_err(|e| io(&committed, e))?;
1761 step("accept binding rename").map_err(|e| io(&committed, e))?;
1762 sync_dir(vfs, directory, step)
1763}
1764
1765pub(super) fn adopt_for_open(vfs: &dyn Vfs, directory: &Path) -> Result<(), StoreError> {
1766 if let Some(selected) = selection(vfs, directory)? {
1767 let (root, _, _) = reader_location(vfs, directory, None)?;
1768 vfs.sync(root, SyncKind::Full).map_err(|e| io(root, e))?;
1771 if accepted(vfs, directory, selected.binding)? {
1772 vfs.sync(directory, SyncKind::Full)
1773 .map_err(|e| io(directory, e))?;
1774 } else {
1775 let manifest = staged_manifest(vfs, directory, &selected)?;
1776 accept(vfs, directory, &selected, &manifest, &mut |_| Ok(()))?;
1777 }
1778 }
1779 Ok(())
1780}
1781fn normalize_accepted(
1782 vfs: &dyn Vfs,
1783 root: &Path,
1784 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1785) -> Result<(), StoreError> {
1786 let (root_id, descriptor) = root_record_vfs(vfs, root)?;
1787 let RootDescriptor::Staged(descriptor) = descriptor else {
1788 return Ok(());
1789 };
1790 for (name, selected) in &descriptor.0 {
1791 let directory = root.join(descriptor.1.get(name).map_or(name.as_str(), String::as_str));
1792 if !accepted(vfs, &directory, selected.binding)? {
1793 return Err(StoreError::StoreBusy { path: directory });
1794 }
1795 sync_dir(vfs, &directory, step)?;
1796 }
1797 publish(
1798 vfs,
1799 root,
1800 &encode_root_record(root_id, &RootDescriptor::Legacy(descriptor.1))?,
1801 step,
1802 )
1803}
1804
1805pub(crate) fn transaction_decisions(
1806 vfs: &dyn Vfs,
1807 directory: &Path,
1808 records: &[crate::wal::VisibleRecord],
1809 absorbed: u64,
1810 preparation: Option<&PrivatePreparation>,
1811) -> Result<BTreeMap<u128, crate::ingest::wal_payload::TransactionBinding>, StoreError> {
1812 let mut decisions = BTreeMap::new();
1813 let mut checked = std::collections::BTreeSet::new();
1814 for record in records.iter().filter(|r| {
1815 r.seq.get() > absorbed && r.op == crate::ingest::wal_payload::PREPARED_MUTATION_V1
1816 }) {
1817 let payload = record.payload().map_err(|source| StoreError::WalRecord {
1818 seq: record.seq,
1819 source,
1820 })?;
1821 let member = crate::ingest::wal_payload::decode_prepared(payload).map_err(|source| {
1822 StoreError::WalMutation {
1823 seq: record.seq,
1824 op: record.op,
1825 source,
1826 }
1827 })?;
1828 if !checked.insert(member.binding.transaction) {
1829 continue;
1830 }
1831 let identity = reader_participant_identity(vfs, directory, preparation)?;
1832 if member.binding.participant != identity {
1833 return Err(StoreError::WalMutation {
1834 seq: record.seq,
1835 op: record.op,
1836 source: crate::ingest::wal_payload::PayloadError::TransactionBinding,
1837 });
1838 }
1839 if accepted(vfs, directory, member.binding)? {
1840 decisions.insert(member.binding.transaction, member.binding);
1841 } else if preparation.is_none()
1842 && let Some(selected) = selection(vfs, directory)?
1843 && selected.binding.transaction == member.binding.transaction
1844 {
1845 if selected.binding != member.binding {
1846 return Err(invalid(directory, "root prepared binding mismatch"));
1847 }
1848 staged_manifest(vfs, directory, &selected)?;
1849 decisions.insert(member.binding.transaction, selected.binding);
1850 }
1851 }
1852 Ok(decisions)
1853}
1854
1855fn participant_identity(root: &Path, name: &str) -> Result<u128, StoreError> {
1856 let canonical = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
1857 Ok(xxhash_rust::xxh3::xxh3_128(
1858 format!("{}\t{name}", canonical.display()).as_bytes(),
1859 ))
1860}
1861
1862fn rollback_stages(
1863 ordered: &[&LiveNamespaceMutation<'_>],
1864 staged: Vec<crate::ingest::NamespaceStage>,
1865 selections: &[Option<StagedSelection>],
1866 lengths: &[u64],
1867 wals: &mut [std::sync::MutexGuard<'_, Option<crate::ingest::StoreWal>>],
1868) -> Result<(), StoreError> {
1869 let result = (|| {
1870 for ((((p, stage), selection), length), wal) in ordered
1871 .iter()
1872 .zip(staged)
1873 .zip(selections)
1874 .zip(lengths)
1875 .zip(wals.iter_mut())
1876 {
1877 wal.as_mut()
1878 .ok_or(StoreError::ReadOnly)?
1879 .abort_namespace_suffix(
1880 p.store.vfs.as_ref(),
1881 &p.store.directory.join("wal.ze"),
1882 *length,
1883 p.store.durability_policy,
1884 stage.manifest.log_seq,
1885 &p.store.accounting,
1886 )?;
1887 cleanup_stage(p.store, stage, selection.as_ref())?;
1888 }
1889 Ok(())
1890 })();
1891 if result.is_err() {
1892 for wal in wals {
1893 **wal = None;
1894 }
1895 }
1896 result
1897}
1898
1899#[cfg(any(test, feature = "test-seams"))]
1901#[doc(hidden)]
1902pub fn namespace_reclaim(root: &Path) -> Result<(), StoreError> {
1903 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
1904 let _owner = StoreLock::acquire(&root).map_err(StoreError::Lock)?;
1905 reclamation::run(&StdVfs, &root, &mut |_| Ok(()))
1906}
1907
1908#[cfg(test)]
1909mod portable_tests;
1910mod reclamation;
1911pub(super) use reclamation::{
1912 admission as reader_admission, for_open as reclaim_for_open, lease as reader_lease,
1913 refuse as refuse_retired,
1914};
1915
1916#[cfg(any(test, feature = "test-seams"))]
1917#[doc(hidden)]
1918pub fn namespace_reclaim_on_vfs(
1919 root: &Path,
1920 vfs: &dyn Vfs,
1921 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1922) -> Result<(), StoreError> {
1923 let _owner = StoreLock::acquire(root).map_err(StoreError::Lock)?;
1924 reclamation::run(vfs, root, step)
1925}