aster_forge_cloud_files_linux/
namespace.rs

1//! Durable Linux namespace mutation acceptance for native file creation.
2
3use aster_forge_cloud_files_core::{
4    CloudItem, CloudItemKey, CloudScope, MutationIntent, SessionGeneration,
5};
6use async_trait::async_trait;
7
8use crate::{
9    LinuxFileAccess, LinuxInodeGeneration, LinuxInodeRecord, LinuxNodeKind, LinuxWriteSession,
10    Result, validate_linux_name,
11};
12
13/// Result returned by the product-owned Linux namespace store.
14pub type LinuxNamespaceStoreResult<T> = std::result::Result<T, LinuxNamespaceMutationStoreError>;
15
16/// Stable classification of a Linux namespace persistence failure.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum LinuxNamespaceMutationStoreErrorKind {
19    /// The requested parent, item, or durable staging record does not exist.
20    NotFound,
21    /// The requested parent/name pair already exists.
22    AlreadyExists,
23    /// A directory removal was rejected because durable children still exist.
24    DirectoryNotEmpty,
25    /// The requested operation expected a regular file but resolved a directory.
26    IsDirectory,
27    /// The requested operation expected a directory but resolved another item kind.
28    NotDirectory,
29    /// The product store does not implement this namespace operation.
30    Unsupported,
31    /// A newer mount generation rejected the request.
32    Fenced,
33    /// Durable identity or namespace state conflicts with the requested transition.
34    Conflict,
35    /// The product store did not durably complete the requested transaction.
36    PersistenceFailure,
37}
38
39/// Product-store failure returned at the Linux namespace boundary.
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
41#[error("linux namespace store {kind:?}: {context}")]
42pub struct LinuxNamespaceMutationStoreError {
43    kind: LinuxNamespaceMutationStoreErrorKind,
44    context: String,
45}
46
47impl LinuxNamespaceMutationStoreError {
48    /// Creates a classified store error with adapter-owned diagnostic context.
49    pub fn new(kind: LinuxNamespaceMutationStoreErrorKind, context: impl Into<String>) -> Self {
50        Self {
51            kind,
52            context: context.into(),
53        }
54    }
55
56    /// Returns the stable store-error classification.
57    #[must_use]
58    pub const fn kind(&self) -> LinuxNamespaceMutationStoreErrorKind {
59        self.kind
60    }
61
62    /// Returns implementation diagnostic context. Product layers own user-facing text.
63    #[must_use]
64    pub fn context(&self) -> &str {
65        &self.context
66    }
67}
68
69/// Native request facts for one regular-file create operation.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct LinuxCreateFileRequest {
72    parent_key: CloudItemKey,
73    name: String,
74    mode: u32,
75    umask: u32,
76    access: LinuxFileAccess,
77    session_generation: SessionGeneration,
78}
79
80impl LinuxCreateFileRequest {
81    /// Creates a request without allocating any product identity.
82    /// # Errors
83    ///
84    /// Returns an error when validation fails or an underlying backend, store, or platform
85    /// operation fails.
86    pub fn new(
87        parent_key: CloudItemKey,
88        name: impl Into<String>,
89        mode: u32,
90        umask: u32,
91        access: LinuxFileAccess,
92        session_generation: SessionGeneration,
93    ) -> Result<Self> {
94        let name = name.into();
95        validate_linux_name(&name)?;
96        Ok(Self {
97            parent_key,
98            name,
99            mode,
100            umask,
101            access,
102            session_generation,
103        })
104    }
105
106    /// Returns the stable parent identity resolved from the requested inode.
107    #[must_use]
108    pub const fn parent_key(&self) -> &CloudItemKey {
109        &self.parent_key
110    }
111
112    /// Returns the exact requested Linux directory-component name.
113    #[must_use]
114    pub fn name(&self) -> &str {
115        &self.name
116    }
117
118    /// Returns the raw create mode supplied by FUSE.
119    #[must_use]
120    pub const fn mode(&self) -> u32 {
121        self.mode
122    }
123
124    /// Returns the caller umask supplied by FUSE.
125    #[must_use]
126    pub const fn umask(&self) -> u32 {
127        self.umask
128    }
129
130    /// Returns the access mode requested for the newly opened handle.
131    #[must_use]
132    pub const fn access(&self) -> LinuxFileAccess {
133        self.access
134    }
135
136    /// Returns the active mount generation accepting this create.
137    #[must_use]
138    pub const fn session_generation(&self) -> SessionGeneration {
139        self.session_generation
140    }
141}
142
143/// Durable product allocation restored into the local Linux namespace.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct LinuxCreatedFile {
146    item: CloudItem,
147    inode_record: LinuxInodeRecord,
148    mutation_intent: MutationIntent,
149}
150
151impl LinuxCreatedFile {
152    /// Creates a durable local namespace record from product-allocated values.
153    #[must_use]
154    pub const fn new(
155        item: CloudItem,
156        inode_record: LinuxInodeRecord,
157        mutation_intent: MutationIntent,
158    ) -> Self {
159        Self {
160            item,
161            inode_record,
162            mutation_intent,
163        }
164    }
165
166    /// Returns the product-neutral local item state.
167    #[must_use]
168    pub const fn item(&self) -> &CloudItem {
169        &self.item
170    }
171
172    /// Returns the stable Linux inode/generation allocation.
173    #[must_use]
174    pub const fn inode_record(&self) -> &LinuxInodeRecord {
175        &self.inode_record
176    }
177
178    /// Returns the create mutation intent persisted in the same product transaction.
179    #[must_use]
180    pub const fn mutation_intent(&self) -> &MutationIntent {
181        &self.mutation_intent
182    }
183
184    /// Consumes the record into its durable fields.
185    #[must_use]
186    pub fn into_parts(self) -> (CloudItem, LinuxInodeRecord, MutationIntent) {
187        (self.item, self.inode_record, self.mutation_intent)
188    }
189}
190
191/// Complete durable acceptance returned before FUSE reports create success.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct LinuxCreateFileAcceptance {
194    created: LinuxCreatedFile,
195    session: LinuxWriteSession,
196}
197
198impl LinuxCreateFileAcceptance {
199    /// Creates an acceptance containing durable namespace, mutation, and staging state.
200    #[must_use]
201    pub const fn new(created: LinuxCreatedFile, session: LinuxWriteSession) -> Self {
202        Self { created, session }
203    }
204
205    /// Returns the durable local namespace allocation.
206    #[must_use]
207    pub const fn created(&self) -> &LinuxCreatedFile {
208        &self.created
209    }
210
211    /// Returns the empty local staging session opened by the transaction.
212    #[must_use]
213    pub const fn session(&self) -> &LinuxWriteSession {
214        &self.session
215    }
216
217    /// Consumes the acceptance into the durable record and opened staging session.
218    #[must_use]
219    pub fn into_parts(self) -> (LinuxCreatedFile, LinuxWriteSession) {
220        (self.created, self.session)
221    }
222}
223
224/// Stable namespace item materialized by a product transaction.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct LinuxNamespaceItem {
227    item: CloudItem,
228    inode_record: LinuxInodeRecord,
229    mutation_intent: MutationIntent,
230}
231
232impl LinuxNamespaceItem {
233    /// Creates a durable namespace item with its stable native mapping and mutation intent.
234    #[must_use]
235    pub const fn new(
236        item: CloudItem,
237        inode_record: LinuxInodeRecord,
238        mutation_intent: MutationIntent,
239    ) -> Self {
240        Self {
241            item,
242            inode_record,
243            mutation_intent,
244        }
245    }
246
247    /// Returns current product-neutral metadata for the local namespace overlay.
248    #[must_use]
249    pub const fn item(&self) -> &CloudItem {
250        &self.item
251    }
252
253    /// Returns the stable inode/generation record preserved across rename and restart.
254    #[must_use]
255    pub const fn inode_record(&self) -> &LinuxInodeRecord {
256        &self.inode_record
257    }
258
259    /// Returns the durable core mutation that produced this local overlay state.
260    #[must_use]
261    pub const fn mutation_intent(&self) -> &MutationIntent {
262        &self.mutation_intent
263    }
264
265    /// Consumes the item into its durable fields.
266    #[must_use]
267    pub fn into_parts(self) -> (CloudItem, LinuxInodeRecord, MutationIntent) {
268        (self.item, self.inode_record, self.mutation_intent)
269    }
270}
271
272impl From<LinuxCreatedFile> for LinuxNamespaceItem {
273    fn from(value: LinuxCreatedFile) -> Self {
274        let (item, inode_record, mutation_intent) = value.into_parts();
275        Self::new(item, inode_record, mutation_intent)
276    }
277}
278
279/// Durable local deletion marker retained until remote mutation reconciliation completes.
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct LinuxNamespaceTombstone {
282    key: CloudItemKey,
283    inode_generation: LinuxInodeGeneration,
284    parent_key: CloudItemKey,
285    name: String,
286    kind: LinuxNodeKind,
287    mutation_intent: MutationIntent,
288}
289
290impl LinuxNamespaceTombstone {
291    /// Creates a tombstone from the exact removed namespace facts.
292    /// # Errors
293    ///
294    /// Returns an error when validation fails or an underlying backend, store, or platform
295    /// operation fails.
296    pub fn new(
297        key: CloudItemKey,
298        inode_generation: LinuxInodeGeneration,
299        parent_key: CloudItemKey,
300        name: impl Into<String>,
301        kind: LinuxNodeKind,
302        mutation_intent: MutationIntent,
303    ) -> Result<Self> {
304        let name = name.into();
305        validate_linux_name(&name)?;
306        Ok(Self {
307            key,
308            inode_generation,
309            parent_key,
310            name,
311            kind,
312            mutation_intent,
313        })
314    }
315
316    /// Returns the stable removed item identity.
317    #[must_use]
318    pub const fn key(&self) -> &CloudItemKey {
319        &self.key
320    }
321
322    /// Returns the removed inode generation used to reject substituted tombstones.
323    #[must_use]
324    pub const fn inode_generation(&self) -> LinuxInodeGeneration {
325        self.inode_generation
326    }
327
328    /// Returns the exact former parent identity.
329    #[must_use]
330    pub const fn parent_key(&self) -> &CloudItemKey {
331        &self.parent_key
332    }
333
334    /// Returns the exact former directory-component name.
335    #[must_use]
336    pub fn name(&self) -> &str {
337        &self.name
338    }
339
340    /// Returns whether the removed entry was a regular file or directory.
341    #[must_use]
342    pub const fn kind(&self) -> LinuxNodeKind {
343        self.kind
344    }
345
346    /// Returns the durable delete intent.
347    #[must_use]
348    pub const fn mutation_intent(&self) -> &MutationIntent {
349        &self.mutation_intent
350    }
351}
352
353/// Restored local namespace state not represented by the legacy regular-file create list.
354#[derive(Debug, Clone, Default, PartialEq, Eq)]
355pub struct LinuxNamespaceOverlay {
356    items: Vec<LinuxNamespaceItem>,
357    tombstones: Vec<LinuxNamespaceTombstone>,
358}
359
360impl LinuxNamespaceOverlay {
361    /// Creates a restart overlay from current local entries and deletion markers.
362    #[must_use]
363    pub const fn new(
364        items: Vec<LinuxNamespaceItem>,
365        tombstones: Vec<LinuxNamespaceTombstone>,
366    ) -> Self {
367        Self { items, tombstones }
368    }
369
370    /// Returns current local namespace entries.
371    #[must_use]
372    pub fn items(&self) -> &[LinuxNamespaceItem] {
373        &self.items
374    }
375
376    /// Returns current local deletion markers.
377    #[must_use]
378    pub fn tombstones(&self) -> &[LinuxNamespaceTombstone] {
379        &self.tombstones
380    }
381
382    /// Consumes the overlay into its durable collections.
383    #[must_use]
384    pub fn into_parts(self) -> (Vec<LinuxNamespaceItem>, Vec<LinuxNamespaceTombstone>) {
385        (self.items, self.tombstones)
386    }
387}
388
389/// Native facts for one durable directory create.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct LinuxCreateDirectoryRequest {
392    parent_key: CloudItemKey,
393    name: String,
394    mode: u32,
395    umask: u32,
396    session_generation: SessionGeneration,
397}
398
399impl LinuxCreateDirectoryRequest {
400    /// Creates a directory request without allocating product identity.
401    /// # Errors
402    ///
403    /// Returns an error when validation fails or an underlying backend, store, or platform
404    /// operation fails.
405    pub fn new(
406        parent_key: CloudItemKey,
407        name: impl Into<String>,
408        mode: u32,
409        umask: u32,
410        session_generation: SessionGeneration,
411    ) -> Result<Self> {
412        let name = name.into();
413        validate_linux_name(&name)?;
414        Ok(Self {
415            parent_key,
416            name,
417            mode,
418            umask,
419            session_generation,
420        })
421    }
422
423    #[must_use]
424    pub const fn parent_key(&self) -> &CloudItemKey {
425        &self.parent_key
426    }
427
428    #[must_use]
429    pub fn name(&self) -> &str {
430        &self.name
431    }
432
433    #[must_use]
434    pub const fn mode(&self) -> u32 {
435        self.mode
436    }
437
438    #[must_use]
439    pub const fn umask(&self) -> u32 {
440        self.umask
441    }
442
443    #[must_use]
444    pub const fn session_generation(&self) -> SessionGeneration {
445        self.session_generation
446    }
447}
448
449/// Native facts for one rename or move that preserves stable item identity.
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct LinuxRenameRequest {
452    key: CloudItemKey,
453    inode_generation: LinuxInodeGeneration,
454    kind: LinuxNodeKind,
455    old_parent_key: CloudItemKey,
456    old_name: String,
457    new_parent_key: CloudItemKey,
458    new_name: String,
459    destination: Option<LinuxRenameDestination>,
460    no_replace: bool,
461    session_generation: SessionGeneration,
462}
463
464impl LinuxRenameRequest {
465    #[expect(
466        clippy::too_many_arguments,
467        reason = "the request preserves exact native source, destination, and generation facts"
468    )]
469    /// # Errors
470    ///
471    /// Returns an error when validation fails or an underlying backend, store, or platform
472    /// operation fails.
473    pub fn new(
474        key: CloudItemKey,
475        inode_generation: LinuxInodeGeneration,
476        kind: LinuxNodeKind,
477        old_parent_key: CloudItemKey,
478        old_name: impl Into<String>,
479        new_parent_key: CloudItemKey,
480        new_name: impl Into<String>,
481        destination: Option<LinuxRenameDestination>,
482        no_replace: bool,
483        session_generation: SessionGeneration,
484    ) -> Result<Self> {
485        let old_name = old_name.into();
486        let new_name = new_name.into();
487        validate_linux_name(&old_name)?;
488        validate_linux_name(&new_name)?;
489        Ok(Self {
490            key,
491            inode_generation,
492            kind,
493            old_parent_key,
494            old_name,
495            new_parent_key,
496            new_name,
497            destination,
498            no_replace,
499            session_generation,
500        })
501    }
502
503    #[must_use]
504    pub const fn key(&self) -> &CloudItemKey {
505        &self.key
506    }
507
508    #[must_use]
509    pub const fn inode_generation(&self) -> LinuxInodeGeneration {
510        self.inode_generation
511    }
512
513    #[must_use]
514    pub const fn kind(&self) -> LinuxNodeKind {
515        self.kind
516    }
517
518    #[must_use]
519    pub const fn old_parent_key(&self) -> &CloudItemKey {
520        &self.old_parent_key
521    }
522
523    #[must_use]
524    pub fn old_name(&self) -> &str {
525        &self.old_name
526    }
527
528    #[must_use]
529    pub const fn new_parent_key(&self) -> &CloudItemKey {
530        &self.new_parent_key
531    }
532
533    #[must_use]
534    pub fn new_name(&self) -> &str {
535        &self.new_name
536    }
537
538    #[must_use]
539    pub const fn destination(&self) -> Option<&LinuxRenameDestination> {
540        self.destination.as_ref()
541    }
542
543    #[must_use]
544    pub const fn no_replace(&self) -> bool {
545        self.no_replace
546    }
547
548    #[must_use]
549    pub const fn session_generation(&self) -> SessionGeneration {
550        self.session_generation
551    }
552}
553
554/// Existing destination resolved before a rename transaction.
555#[derive(Debug, Clone, PartialEq, Eq)]
556pub struct LinuxRenameDestination {
557    key: CloudItemKey,
558    inode_generation: LinuxInodeGeneration,
559    kind: LinuxNodeKind,
560}
561
562impl LinuxRenameDestination {
563    #[must_use]
564    pub const fn new(
565        key: CloudItemKey,
566        inode_generation: LinuxInodeGeneration,
567        kind: LinuxNodeKind,
568    ) -> Self {
569        Self {
570            key,
571            inode_generation,
572            kind,
573        }
574    }
575
576    #[must_use]
577    pub const fn key(&self) -> &CloudItemKey {
578        &self.key
579    }
580
581    #[must_use]
582    pub const fn inode_generation(&self) -> LinuxInodeGeneration {
583        self.inode_generation
584    }
585
586    #[must_use]
587    pub const fn kind(&self) -> LinuxNodeKind {
588        self.kind
589    }
590}
591
592/// Durable rename result, including a destination replacement tombstone when applicable.
593#[derive(Debug, Clone, PartialEq, Eq)]
594pub struct LinuxRenameAcceptance {
595    item: LinuxNamespaceItem,
596    source: LinuxNamespaceTombstone,
597    replaced: Option<LinuxNamespaceTombstone>,
598}
599
600impl LinuxRenameAcceptance {
601    #[must_use]
602    pub const fn new(
603        item: LinuxNamespaceItem,
604        source: LinuxNamespaceTombstone,
605        replaced: Option<LinuxNamespaceTombstone>,
606    ) -> Self {
607        Self {
608            item,
609            source,
610            replaced,
611        }
612    }
613
614    #[must_use]
615    pub const fn item(&self) -> &LinuxNamespaceItem {
616        &self.item
617    }
618
619    #[must_use]
620    pub const fn source(&self) -> &LinuxNamespaceTombstone {
621        &self.source
622    }
623
624    #[must_use]
625    pub const fn replaced(&self) -> Option<&LinuxNamespaceTombstone> {
626        self.replaced.as_ref()
627    }
628
629    #[must_use]
630    pub fn into_parts(
631        self,
632    ) -> (
633        LinuxNamespaceItem,
634        LinuxNamespaceTombstone,
635        Option<LinuxNamespaceTombstone>,
636    ) {
637        (self.item, self.source, self.replaced)
638    }
639}
640
641/// Native facts for one unlink or rmdir transaction.
642#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct LinuxRemoveRequest {
644    key: CloudItemKey,
645    inode_generation: LinuxInodeGeneration,
646    parent_key: CloudItemKey,
647    name: String,
648    kind: LinuxNodeKind,
649    session_generation: SessionGeneration,
650}
651
652impl LinuxRemoveRequest {
653    /// # Errors
654    ///
655    /// Returns an error when validation fails or an underlying backend, store, or platform
656    /// operation fails.
657    pub fn new(
658        key: CloudItemKey,
659        inode_generation: LinuxInodeGeneration,
660        parent_key: CloudItemKey,
661        name: impl Into<String>,
662        kind: LinuxNodeKind,
663        session_generation: SessionGeneration,
664    ) -> Result<Self> {
665        let name = name.into();
666        validate_linux_name(&name)?;
667        Ok(Self {
668            key,
669            inode_generation,
670            parent_key,
671            name,
672            kind,
673            session_generation,
674        })
675    }
676
677    #[must_use]
678    pub const fn key(&self) -> &CloudItemKey {
679        &self.key
680    }
681
682    #[must_use]
683    pub const fn inode_generation(&self) -> LinuxInodeGeneration {
684        self.inode_generation
685    }
686
687    #[must_use]
688    pub const fn parent_key(&self) -> &CloudItemKey {
689        &self.parent_key
690    }
691
692    #[must_use]
693    pub fn name(&self) -> &str {
694        &self.name
695    }
696
697    #[must_use]
698    pub const fn kind(&self) -> LinuxNodeKind {
699        self.kind
700    }
701
702    #[must_use]
703    pub const fn session_generation(&self) -> SessionGeneration {
704        self.session_generation
705    }
706}
707
708/// Product-owned durable namespace transaction used by native Linux create.
709///
710/// `create_file` must atomically persist the stable `CloudItemKey`, inode/generation record,
711/// complete core mutation intent, empty staging state, and mount-generation comparison before it
712/// returns. The port never asks Forge to derive identities from a name, path, hash, or callback.
713#[async_trait]
714pub trait LinuxNamespaceMutationStore: Send + Sync {
715    /// Activates a mount generation and returns non-terminal local creates for startup recovery.
716    async fn activate_namespace(
717        &self,
718        scope: &CloudScope,
719        session_generation: SessionGeneration,
720    ) -> LinuxNamespaceStoreResult<Vec<LinuxCreatedFile>>;
721
722    /// Atomically accepts a create and opens its empty local staging session.
723    async fn create_file(
724        &self,
725        request: &LinuxCreateFileRequest,
726    ) -> LinuxNamespaceStoreResult<LinuxCreateFileAcceptance>;
727
728    /// Opens local staging for an already accepted create that is not remotely materialized yet.
729    async fn open_created_file(
730        &self,
731        key: &CloudItemKey,
732        session_generation: SessionGeneration,
733    ) -> LinuxNamespaceStoreResult<LinuxWriteSession>;
734
735    /// Restores durable local directory creates, renames, and tombstones for a newer mount.
736    async fn activate_namespace_overlay(
737        &self,
738        _scope: &CloudScope,
739        _session_generation: SessionGeneration,
740    ) -> LinuxNamespaceStoreResult<LinuxNamespaceOverlay> {
741        Ok(LinuxNamespaceOverlay::default())
742    }
743
744    /// Atomically accepts a durable directory create.
745    async fn create_directory(
746        &self,
747        _request: &LinuxCreateDirectoryRequest,
748    ) -> LinuxNamespaceStoreResult<LinuxNamespaceItem> {
749        Err(LinuxNamespaceMutationStoreError::new(
750            LinuxNamespaceMutationStoreErrorKind::Unsupported,
751            "directory create is not implemented by this namespace store",
752        ))
753    }
754
755    /// Atomically accepts a stable-identity rename or move.
756    async fn rename(
757        &self,
758        _request: &LinuxRenameRequest,
759    ) -> LinuxNamespaceStoreResult<LinuxRenameAcceptance> {
760        Err(LinuxNamespaceMutationStoreError::new(
761            LinuxNamespaceMutationStoreErrorKind::Unsupported,
762            "rename is not implemented by this namespace store",
763        ))
764    }
765
766    /// Atomically accepts unlink or rmdir and returns its durable tombstone.
767    async fn remove(
768        &self,
769        _request: &LinuxRemoveRequest,
770    ) -> LinuxNamespaceStoreResult<LinuxNamespaceTombstone> {
771        Err(LinuxNamespaceMutationStoreError::new(
772            LinuxNamespaceMutationStoreErrorKind::Unsupported,
773            "remove is not implemented by this namespace store",
774        ))
775    }
776}