aster_forge_cloud_files_linux/
writeback.rs

1//! Durable local-write staging for the Linux FUSE adapter.
2
3use std::{
4    collections::{HashMap, HashSet},
5    fmt,
6    sync::{Arc, Mutex, MutexGuard},
7};
8
9use aster_forge_cloud_files_core::{
10    CloudFilesBackend, CloudItem, CloudItemId, CloudItemKey, CloudItemKind, CloudScope,
11    ContentRevision, DesiredMutation, LocalContentGeneration, LocalContentSnapshot, MutationOrigin,
12    SessionGeneration, StoreResult,
13};
14use async_trait::async_trait;
15use bytes::Bytes;
16
17use crate::{
18    LinuxCloudFilesError, LinuxCreateDirectoryRequest, LinuxCreateFileAcceptance,
19    LinuxCreateFileRequest, LinuxCreatedFile, LinuxDirectoryEntry, LinuxDirectoryHandle,
20    LinuxDirectorySnapshot, LinuxFileHandle, LinuxInode, LinuxInvalidation, LinuxNamespaceItem,
21    LinuxNamespaceMutationStore, LinuxNamespaceOverlay, LinuxNamespaceTombstone, LinuxNode,
22    LinuxReadOnlyEngine, LinuxRemoteChange, LinuxRemoteDelete, LinuxRemoteEntry,
23    LinuxRemoteLocation, LinuxRemoveRequest, LinuxRenameAcceptance, LinuxRenameDestination,
24    LinuxRenameRequest, Result,
25};
26
27fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
28    match mutex.lock() {
29        Ok(guard) => guard,
30        Err(poisoned) => poisoned.into_inner(),
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use super::WritableState;
37    use crate::LinuxCloudFilesError;
38
39    #[test]
40    fn writable_handle_allocator_reports_u64_exhaustion() {
41        let mut state = WritableState {
42            next: u64::MAX,
43            ..WritableState::default()
44        };
45        assert!(matches!(
46            state.allocate(),
47            Err(LinuxCloudFilesError::HandleExhausted)
48        ));
49        assert!(matches!(
50            state.allocate_directory(),
51            Err(LinuxCloudFilesError::HandleExhausted)
52        ));
53    }
54}
55
56/// Access requested for one opened Linux file handle.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LinuxFileAccess {
59    /// The handle reads the revision-bound remote snapshot only.
60    Read,
61    /// The handle writes durable local generations and does not permit reads.
62    Write,
63    /// The handle reads and writes the durable local staging session.
64    ReadWrite,
65}
66
67impl LinuxFileAccess {
68    const fn readable(self) -> bool {
69        matches!(self, Self::Read | Self::ReadWrite)
70    }
71
72    const fn writable(self) -> bool {
73        matches!(self, Self::Write | Self::ReadWrite)
74    }
75}
76
77/// Opaque product-owned identity of one durable local write session.
78#[derive(Clone, PartialEq, Eq, Hash)]
79pub struct LinuxWriteSessionId(String);
80
81impl LinuxWriteSessionId {
82    /// Creates a non-empty session identity and preserves it exactly.
83    /// # Errors
84    ///
85    /// Returns an error when validation fails or an underlying backend, store, or platform
86    /// operation fails.
87    pub fn new(value: impl Into<String>) -> Result<Self> {
88        let value = value.into();
89        if value.is_empty() {
90            return Err(LinuxCloudFilesError::InvalidConfiguration {
91                reason: "write session id must not be empty",
92            });
93        }
94        Ok(Self(value))
95    }
96
97    /// Returns the opaque session identity.
98    #[must_use]
99    pub fn as_str(&self) -> &str {
100        &self.0
101    }
102}
103
104impl fmt::Debug for LinuxWriteSessionId {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter
107            .debug_struct("LinuxWriteSessionId")
108            .field("byte_len", &self.0.len())
109            .finish()
110    }
111}
112
113/// Revision-bound request to create or reopen durable local staging for an existing file.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct LinuxWriteOpenRequest {
116    key: CloudItemKey,
117    base_revision: ContentRevision,
118    base_size: u64,
119    session_generation: SessionGeneration,
120}
121
122impl LinuxWriteOpenRequest {
123    /// Creates an existing-file write request from its exact remote snapshot.
124    #[must_use]
125    pub const fn new(
126        key: CloudItemKey,
127        base_revision: ContentRevision,
128        base_size: u64,
129        session_generation: SessionGeneration,
130    ) -> Self {
131        Self {
132            key,
133            base_revision,
134            base_size,
135            session_generation,
136        }
137    }
138
139    /// Returns the stable item identity.
140    #[must_use]
141    pub const fn key(&self) -> &CloudItemKey {
142        &self.key
143    }
144
145    /// Returns the exact remote content revision hydrated into staging.
146    #[must_use]
147    pub const fn base_revision(&self) -> &ContentRevision {
148        &self.base_revision
149    }
150
151    /// Returns the exact hydrated byte length.
152    #[must_use]
153    pub const fn base_size(&self) -> u64 {
154        self.base_size
155    }
156
157    /// Returns the active mount session generation that owns this open.
158    #[must_use]
159    pub const fn session_generation(&self) -> SessionGeneration {
160        self.session_generation
161    }
162}
163
164/// Product-owned durable staging session returned after the base bytes are installed.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct LinuxWriteSession {
167    id: LinuxWriteSessionId,
168    key: CloudItemKey,
169    size: u64,
170    session_generation: SessionGeneration,
171    snapshot: Option<LocalContentSnapshot>,
172}
173
174impl LinuxWriteSession {
175    /// Creates a staging session response.
176    #[must_use]
177    pub const fn new(
178        id: LinuxWriteSessionId,
179        key: CloudItemKey,
180        size: u64,
181        session_generation: SessionGeneration,
182    ) -> Self {
183        Self {
184            id,
185            key,
186            size,
187            session_generation,
188            snapshot: None,
189        }
190    }
191
192    /// Creates a session reopened from one immutable durable dirty snapshot.
193    #[must_use]
194    pub fn from_recovered(
195        id: LinuxWriteSessionId,
196        snapshot: LocalContentSnapshot,
197        session_generation: SessionGeneration,
198    ) -> Self {
199        Self {
200            id,
201            key: snapshot.item_key().clone(),
202            size: snapshot.size(),
203            session_generation,
204            snapshot: Some(snapshot),
205        }
206    }
207
208    /// Returns the opaque product-owned session identity.
209    #[must_use]
210    pub const fn id(&self) -> &LinuxWriteSessionId {
211        &self.id
212    }
213
214    /// Returns the item whose bytes are staged.
215    #[must_use]
216    pub const fn key(&self) -> &CloudItemKey {
217        &self.key
218    }
219
220    /// Returns the current logical staging size.
221    #[must_use]
222    pub const fn size(&self) -> u64 {
223        self.size
224    }
225
226    /// Returns the mount session generation bound to this writeback session.
227    #[must_use]
228    pub const fn session_generation(&self) -> SessionGeneration {
229        self.session_generation
230    }
231
232    /// Returns the immutable dirty snapshot used to reopen this session, when present.
233    #[must_use]
234    pub const fn snapshot(&self) -> Option<&LocalContentSnapshot> {
235        self.snapshot.as_ref()
236    }
237}
238
239/// Result of one durably accepted write, truncate, flush, or fsync operation.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct LinuxWriteCommit {
242    snapshot: LocalContentSnapshot,
243}
244
245impl LinuxWriteCommit {
246    /// Creates a commit backed by one immutable local generation.
247    #[must_use]
248    pub const fn new(snapshot: LocalContentSnapshot) -> Self {
249        Self { snapshot }
250    }
251
252    /// Returns the immutable dirty snapshot that is safe for upload recovery.
253    #[must_use]
254    pub const fn snapshot(&self) -> &LocalContentSnapshot {
255        &self.snapshot
256    }
257
258    /// Consumes the commit into its immutable snapshot.
259    #[must_use]
260    pub fn into_snapshot(self) -> LocalContentSnapshot {
261        self.snapshot
262    }
263}
264
265/// Product-owned durable local-content implementation used by writable FUSE handles.
266///
267/// A successful mutating method must make both the bytes and returned immutable generation
268/// recoverable before it returns. When remote upload is enabled, the product store should also
269/// atomically persist a core `ContentUploadIntent` for that exact snapshot before acknowledging the
270/// native write. The implementation may compose a filesystem cache, `ContentStorageStore`,
271/// `ContentUploadStore`, and `MutationJournalStore`; the Linux adapter does not allocate operation
272/// identities, select upload transport, or choose those product persistence details.
273#[async_trait]
274pub trait LinuxWritebackStore: Send + Sync {
275    /// Activates one mount generation and returns durable dirty snapshots for this scope.
276    ///
277    /// Before this method returns, the implementation must fence lower mount generations from
278    /// mutating active writeback state. Products commonly compose this boundary with core's
279    /// `MutationJournalStore::activate_session` and their content-storage transaction.
280    async fn activate_mount(
281        &self,
282        scope: &CloudScope,
283        session_generation: SessionGeneration,
284    ) -> StoreResult<Vec<LocalContentSnapshot>>;
285
286    /// Reopens durable staged bytes for one dirty item without fetching its remote base content.
287    ///
288    /// `None` means the item has no active dirty snapshot for the requested mount generation.
289    /// A returned session must be bound to `session_generation` and carry that snapshot through
290    /// [`LinuxWriteSession::snapshot`].
291    async fn open_recovered(
292        &self,
293        key: &CloudItemKey,
294        session_generation: SessionGeneration,
295    ) -> StoreResult<Option<LinuxWriteSession>>;
296
297    /// Installs or reopens staging from an exact complete remote revision.
298    async fn open_existing(
299        &self,
300        request: &LinuxWriteOpenRequest,
301        base_content: Bytes,
302    ) -> StoreResult<LinuxWriteSession>;
303
304    /// Reads current staged bytes, including all earlier committed writes on this session.
305    async fn read(
306        &self,
307        session: &LinuxWriteSessionId,
308        offset: u64,
309        size: u32,
310    ) -> StoreResult<Bytes>;
311
312    /// Durably applies one positioned write and returns its immutable dirty generation.
313    async fn write(
314        &self,
315        session: &LinuxWriteSessionId,
316        offset: u64,
317        bytes: Bytes,
318    ) -> StoreResult<LinuxWriteCommit>;
319
320    /// Durably changes the logical file size and returns its immutable dirty generation.
321    async fn truncate(
322        &self,
323        session: &LinuxWriteSessionId,
324        size: u64,
325    ) -> StoreResult<LinuxWriteCommit>;
326
327    /// Confirms that the latest dirty generation satisfies a flush or fsync durability boundary.
328    async fn sync(
329        &self,
330        session: &LinuxWriteSessionId,
331        data_only: bool,
332    ) -> StoreResult<Option<LinuxWriteCommit>>;
333
334    /// Releases product-owned mutable session state after all FUSE references are gone.
335    async fn close(&self, session: &LinuxWriteSessionId) -> StoreResult<()>;
336}
337
338#[derive(Debug, Clone)]
339enum OpenFile {
340    Writeback {
341        inode: LinuxInode,
342        access: LinuxFileAccess,
343        session: LinuxWriteSessionId,
344        size: u64,
345        generation: Option<LocalContentGeneration>,
346    },
347}
348
349type ParentNameKey = (CloudItemId, String);
350
351/// Parent-bucketed name index used by lookup and directory enumeration.
352///
353/// Keeping the parent as the first hash lookup makes opening one directory proportional to that
354/// directory's overlay children rather than to the complete mounted namespace.
355#[derive(Debug)]
356struct ParentNameIndex<V> {
357    by_parent: HashMap<CloudItemId, HashMap<String, V>>,
358}
359
360impl<V> Default for ParentNameIndex<V> {
361    fn default() -> Self {
362        Self {
363            by_parent: HashMap::new(),
364        }
365    }
366}
367
368impl<V> ParentNameIndex<V> {
369    fn get(&self, key: &ParentNameKey) -> Option<&V> {
370        self.by_parent
371            .get(&key.0)
372            .and_then(|children| children.get(key.1.as_str()))
373    }
374
375    fn contains_key(&self, key: &ParentNameKey) -> bool {
376        self.get(key).is_some()
377    }
378
379    fn insert(&mut self, key: ParentNameKey, value: V) -> Option<V> {
380        self.by_parent
381            .entry(key.0)
382            .or_default()
383            .insert(key.1, value)
384    }
385
386    fn remove(&mut self, key: &ParentNameKey) -> Option<V> {
387        let (removed, empty) = {
388            let children = self.by_parent.get_mut(&key.0)?;
389            let removed = children.remove(key.1.as_str());
390            (removed, children.is_empty())
391        };
392        if empty {
393            self.by_parent.remove(&key.0);
394        }
395        removed
396    }
397
398    fn children(&self, parent: &CloudItemId) -> impl Iterator<Item = (&str, &V)> {
399        self.by_parent
400            .get(parent)
401            .into_iter()
402            .flat_map(|children| children.iter().map(|(name, value)| (name.as_str(), value)))
403    }
404}
405
406#[derive(Default)]
407struct WritableState {
408    next: u64,
409    files: HashMap<LinuxFileHandle, OpenFile>,
410    directories: HashMap<LinuxDirectoryHandle, LinuxDirectorySnapshot>,
411    dirty_snapshots: HashMap<LinuxInode, LocalContentSnapshot>,
412    created_by_inode: HashMap<LinuxInode, LinuxCreatedFile>,
413    created_by_key: HashMap<CloudItemKey, LinuxInode>,
414    created_by_name: ParentNameIndex<LinuxInode>,
415    namespace_by_inode: HashMap<LinuxInode, LinuxNamespaceItem>,
416    namespace_by_key: HashMap<CloudItemKey, LinuxInode>,
417    namespace_by_name: ParentNameIndex<LinuxInode>,
418    remote_by_inode: HashMap<LinuxInode, LinuxRemoteEntry>,
419    remote_by_key: HashMap<CloudItemKey, LinuxInode>,
420    remote_by_name: ParentNameIndex<LinuxInode>,
421    remote_tombstones_by_name: ParentNameIndex<()>,
422    remote_deleted_keys: HashSet<CloudItemKey>,
423    remote_deleted_inodes: HashSet<LinuxInode>,
424    tombstones_by_name: ParentNameIndex<LinuxNamespaceTombstone>,
425    deleted_keys: HashSet<CloudItemKey>,
426}
427
428impl WritableState {
429    fn allocate(&mut self) -> Result<LinuxFileHandle> {
430        let current = if self.next == 0 { 1 } else { self.next };
431        let Some(next) = current.checked_add(1) else {
432            return Err(LinuxCloudFilesError::HandleExhausted);
433        };
434        self.next = next;
435        LinuxFileHandle::new(current)
436    }
437
438    fn allocate_directory(&mut self) -> Result<LinuxDirectoryHandle> {
439        let current = if self.next == 0 { 1 } else { self.next };
440        let Some(next) = current.checked_add(1) else {
441            return Err(LinuxCloudFilesError::HandleExhausted);
442        };
443        self.next = next;
444        LinuxDirectoryHandle::new(current)
445    }
446}
447
448struct WritableEngineInner<B, S> {
449    readonly: LinuxReadOnlyEngine<B>,
450    store: Arc<S>,
451    namespace_store: Option<Arc<dyn LinuxNamespaceMutationStore>>,
452    session_generation: SessionGeneration,
453    state: Mutex<WritableState>,
454}
455
456/// Writable existing-file engine with injected durable local staging.
457pub struct LinuxWritableEngine<B, S> {
458    inner: Arc<WritableEngineInner<B, S>>,
459}
460
461impl<B, S> Clone for LinuxWritableEngine<B, S> {
462    fn clone(&self) -> Self {
463        Self {
464            inner: self.inner.clone(),
465        }
466    }
467}
468
469impl<B, S> LinuxWritableEngine<B, S>
470where
471    B: CloudFilesBackend + 'static,
472    S: LinuxWritebackStore + 'static,
473{
474    /// Activates one mount generation and restores durable dirty snapshots before exposure.
475    /// # Errors
476    ///
477    /// Returns an error when validation fails or an underlying backend, store, or platform
478    /// operation fails.
479    pub async fn activate(
480        readonly: LinuxReadOnlyEngine<B>,
481        store: Arc<S>,
482        session_generation: SessionGeneration,
483    ) -> Result<Self> {
484        Self::activate_inner(readonly, store, None, Vec::new(), session_generation).await
485    }
486
487    /// Activates writeback plus a durable namespace port for native regular-file creation.
488    /// # Errors
489    ///
490    /// Returns an error when validation fails or an underlying backend, store, or platform
491    /// operation fails.
492    pub async fn activate_with_namespace<N>(
493        readonly: LinuxReadOnlyEngine<B>,
494        store: Arc<S>,
495        namespace_store: Arc<N>,
496        session_generation: SessionGeneration,
497    ) -> Result<Self>
498    where
499        N: LinuxNamespaceMutationStore + 'static,
500    {
501        Self::activate_inner(
502            readonly,
503            store,
504            Some(namespace_store),
505            Vec::new(),
506            session_generation,
507        )
508        .await
509    }
510
511    /// Activates namespace/writeback state with product-persisted remote inode mappings.
512    /// # Errors
513    ///
514    /// Returns an error when validation fails or an underlying backend, store, or platform
515    /// operation fails.
516    pub async fn activate_with_namespace_and_remote<N>(
517        readonly: LinuxReadOnlyEngine<B>,
518        store: Arc<S>,
519        namespace_store: Arc<N>,
520        remote_entries: Vec<LinuxRemoteEntry>,
521        session_generation: SessionGeneration,
522    ) -> Result<Self>
523    where
524        N: LinuxNamespaceMutationStore + 'static,
525    {
526        Self::activate_inner(
527            readonly,
528            store,
529            Some(namespace_store),
530            remote_entries,
531            session_generation,
532        )
533        .await
534    }
535
536    async fn activate_inner(
537        readonly: LinuxReadOnlyEngine<B>,
538        store: Arc<S>,
539        namespace_store: Option<Arc<dyn LinuxNamespaceMutationStore>>,
540        remote_entries: Vec<LinuxRemoteEntry>,
541        session_generation: SessionGeneration,
542    ) -> Result<Self> {
543        let created = match namespace_store.as_ref() {
544            Some(namespace_store) => {
545                namespace_store
546                    .activate_namespace(readonly.inode_table().scope(), session_generation)
547                    .await?
548            }
549            None => Vec::new(),
550        };
551        let snapshots = store
552            .activate_mount(readonly.inode_table().scope(), session_generation)
553            .await?;
554        let overlay = match namespace_store.as_ref() {
555            Some(namespace_store) => {
556                namespace_store
557                    .activate_namespace_overlay(readonly.inode_table().scope(), session_generation)
558                    .await?
559            }
560            None => LinuxNamespaceOverlay::default(),
561        };
562        let engine = Self {
563            inner: Arc::new(WritableEngineInner {
564                readonly,
565                store,
566                namespace_store,
567                session_generation,
568                state: Mutex::new(WritableState::default()),
569            }),
570        };
571        engine.restore_namespace_overlay(overlay)?;
572        engine.restore_remote_overlay(remote_entries).await?;
573        engine.restore_created_files(created).await?;
574        engine.restore_dirty_snapshots(snapshots)?;
575        Ok(engine)
576    }
577
578    /// Returns the underlying read-only engine used for metadata and directory operations.
579    #[must_use]
580    pub fn readonly(&self) -> &LinuxReadOnlyEngine<B> {
581        &self.inner.readonly
582    }
583
584    /// Returns the durable mount generation that owns this engine's writeback sessions.
585    #[must_use]
586    pub fn session_generation(&self) -> SessionGeneration {
587        self.inner.session_generation
588    }
589
590    /// Restores product-persisted remote entries before the mount is exposed to the kernel.
591    /// # Errors
592    ///
593    /// Returns an error when validation fails or an underlying backend, store, or platform
594    /// operation fails.
595    pub async fn restore_remote_overlay(
596        &self,
597        entries: impl IntoIterator<Item = LinuxRemoteEntry>,
598    ) -> Result<()> {
599        for entry in entries {
600            self.apply_remote_change(LinuxRemoteChange::Upsert(crate::LinuxRemoteUpsert::new(
601                entry, None, None,
602            )))
603            .await?;
604        }
605        Ok(())
606    }
607
608    /// Applies one already-durable remote namespace transition and returns kernel invalidations.
609    /// # Errors
610    ///
611    /// Returns an error when validation fails or an underlying backend, store, or platform
612    /// operation fails.
613    pub async fn apply_remote_change(
614        &self,
615        change: LinuxRemoteChange,
616    ) -> Result<Vec<LinuxInvalidation>> {
617        match change {
618            LinuxRemoteChange::Upsert(upsert) => {
619                let (entry, previous, replaced) = upsert.into_parts();
620                self.apply_remote_upsert(entry, previous, replaced).await
621            }
622            LinuxRemoteChange::Delete(deleted) => self.apply_remote_delete(deleted).await,
623        }
624    }
625
626    /// Loads attributes, overlaying the current staged size when a writable handle is supplied.
627    /// # Errors
628    ///
629    /// Returns an error when validation fails or an underlying backend, store, or platform
630    /// operation fails.
631    pub async fn getattr(
632        &self,
633        inode: LinuxInode,
634        handle: Option<LinuxFileHandle>,
635    ) -> Result<LinuxNode> {
636        let (namespace, created, remote, deleted) = {
637            let state = lock(&self.inner.state);
638            let restored_key = self
639                .inner
640                .readonly
641                .inode_table()
642                .by_inode(inode)
643                .map(crate::LinuxInodeRecord::key);
644            (
645                state.namespace_by_inode.get(&inode).cloned(),
646                state.created_by_inode.get(&inode).cloned(),
647                state.remote_by_inode.get(&inode).cloned(),
648                state.remote_deleted_inodes.contains(&inode)
649                    || restored_key.is_some_and(|key| {
650                        state.deleted_keys.contains(key) || state.remote_deleted_keys.contains(key)
651                    })
652                    || state.created_by_inode.get(&inode).is_some_and(|created| {
653                        state.deleted_keys.contains(created.item().key())
654                            || state.remote_deleted_keys.contains(created.item().key())
655                    }),
656            )
657        };
658        if deleted && namespace.is_none() && remote.is_none() {
659            return Err(LinuxCloudFilesError::UnknownInode { inode: inode.get() });
660        }
661        let node = match (namespace, created, remote) {
662            (Some(item), _, _) => self
663                .inner
664                .readonly
665                .node_from_item_and_record(item.item(), item.inode_record())?,
666            (None, Some(created), _) => self
667                .inner
668                .readonly
669                .node_from_item_and_record(created.item(), created.inode_record())?,
670            (None, None, Some(remote)) => self
671                .inner
672                .readonly
673                .node_from_item_and_record(remote.item(), remote.inode_record())?,
674            (None, None, None) => self.inner.readonly.getattr(inode).await?,
675        };
676        let size = match handle {
677            Some(handle) => {
678                let state = lock(&self.inner.state);
679                match state.files.get(&handle) {
680                    Some(OpenFile::Writeback {
681                        inode: opened_inode,
682                        size,
683                        ..
684                    }) if *opened_inode == inode => Some(*size),
685                    _ => return Err(LinuxCloudFilesError::StaleHandle),
686                }
687            }
688            None => lock(&self.inner.state)
689                .dirty_snapshots
690                .get(&inode)
691                .map(LocalContentSnapshot::size),
692        };
693        Ok(match size {
694            Some(size) => node.with_size(size),
695            None => node,
696        })
697    }
698
699    /// Resolves one child and overlays its current dirty staging size.
700    /// # Errors
701    ///
702    /// Returns an error when validation fails or an underlying backend, store, or platform
703    /// operation fails.
704    pub async fn lookup(&self, parent: LinuxInode, name: &str) -> Result<LinuxNode> {
705        crate::validate_linux_name(name)?;
706        let parent_node = self.getattr(parent, None).await?;
707        if parent_node.attributes().kind() != crate::LinuxNodeKind::Directory {
708            return Err(LinuxCloudFilesError::NotDirectory);
709        }
710        let parent_key = parent_node.key();
711        let (namespace, created, remote, tombstoned) = {
712            let state = lock(&self.inner.state);
713            let name_key = (parent_key.item_id().clone(), name.to_owned());
714            (
715                state
716                    .namespace_by_name
717                    .get(&name_key)
718                    .and_then(|inode| state.namespace_by_inode.get(inode))
719                    .cloned(),
720                state
721                    .created_by_name
722                    .get(&name_key)
723                    .and_then(|inode| state.created_by_inode.get(inode))
724                    .cloned(),
725                state
726                    .remote_by_name
727                    .get(&name_key)
728                    .and_then(|inode| state.remote_by_inode.get(inode))
729                    .cloned(),
730                state.tombstones_by_name.contains_key(&name_key)
731                    || state.remote_tombstones_by_name.contains_key(&name_key),
732            )
733        };
734        let node = match (namespace, created, remote, tombstoned) {
735            (Some(item), _, _, _) => self
736                .inner
737                .readonly
738                .node_from_item_and_record(item.item(), item.inode_record())?,
739            (None, Some(created), _, _) => self
740                .inner
741                .readonly
742                .node_from_item_and_record(created.item(), created.inode_record())?,
743            (None, None, Some(remote), _) => self
744                .inner
745                .readonly
746                .node_from_item_and_record(remote.item(), remote.inode_record())?,
747            (None, None, None, true) => {
748                return Err(LinuxCloudFilesError::UnknownInode {
749                    inode: parent.get(),
750                });
751            }
752            (None, None, None, false) => self.inner.readonly.lookup(parent, name).await?,
753        };
754        let size = lock(&self.inner.state)
755            .dirty_snapshots
756            .get(&node.attributes().inode())
757            .map(LocalContentSnapshot::size);
758        Ok(match size {
759            Some(size) => node.with_size(size),
760            None => node,
761        })
762    }
763
764    /// Opens an existing file for remote reads or durable local writeback.
765    /// # Errors
766    ///
767    /// Returns an error when validation fails or an underlying backend, store, or platform
768    /// operation fails.
769    pub async fn open_file(
770        &self,
771        inode: LinuxInode,
772        access: LinuxFileAccess,
773    ) -> Result<LinuxFileHandle> {
774        let (namespace, created, remote) = {
775            let state = lock(&self.inner.state);
776            (
777                state.namespace_by_inode.get(&inode).cloned(),
778                state.created_by_inode.get(&inode).cloned(),
779                state.remote_by_inode.get(&inode).cloned(),
780            )
781        };
782        let node = match (namespace.as_ref(), created.as_ref(), remote.as_ref()) {
783            (Some(item), _, _) => self
784                .inner
785                .readonly
786                .node_from_item_and_record(item.item(), item.inode_record())?,
787            (None, Some(created), _) => self
788                .inner
789                .readonly
790                .node_from_item_and_record(created.item(), created.inode_record())?,
791            (None, None, Some(remote)) => self
792                .inner
793                .readonly
794                .node_from_item_and_record(remote.item(), remote.inode_record())?,
795            (None, None, None) => self.inner.readonly.getattr(inode).await?,
796        };
797        if node.attributes().kind() != crate::LinuxNodeKind::File {
798            return Err(LinuxCloudFilesError::NotFile);
799        }
800        let key = node.key().clone();
801        let (session, recovered) = match self
802            .inner
803            .store
804            .open_recovered(&key, self.inner.session_generation)
805            .await?
806        {
807            Some(session) => (session, true),
808            None if created.is_some() => {
809                let namespace_store = self
810                    .inner
811                    .namespace_store
812                    .as_ref()
813                    .ok_or(LinuxCloudFilesError::NamespaceMutationNotConfigured)?;
814                (
815                    namespace_store
816                        .open_created_file(&key, self.inner.session_generation)
817                        .await?,
818                    false,
819                )
820            }
821            None => {
822                let (request, content) = match remote.as_ref() {
823                    Some(remote) => {
824                        self.inner
825                            .readonly
826                            .hydrate_item_for_write(remote.item(), self.inner.session_generation)
827                            .await?
828                    }
829                    None => {
830                        self.inner
831                            .readonly
832                            .hydrate_for_write(inode, self.inner.session_generation)
833                            .await?
834                    }
835                };
836                (
837                    self.inner.store.open_existing(&request, content).await?,
838                    false,
839                )
840            }
841        };
842        self.validate_opened_session(inode, &key, &session, recovered)?;
843        let opened = OpenFile::Writeback {
844            inode,
845            access,
846            session: session.id().clone(),
847            size: session.size(),
848            generation: session.snapshot().map(LocalContentSnapshot::generation),
849        };
850        let mut state = lock(&self.inner.state);
851        let handle = state.allocate()?;
852        state.files.insert(handle, opened);
853        Ok(handle)
854    }
855
856    /// Atomically accepts a durable regular-file create and returns its entry plus open handle.
857    /// # Errors
858    ///
859    /// Returns an error when validation fails or an underlying backend, store, or platform
860    /// operation fails.
861    pub async fn create_file(
862        &self,
863        parent: LinuxInode,
864        name: &str,
865        mode: u32,
866        umask: u32,
867        access: LinuxFileAccess,
868    ) -> Result<(LinuxNode, LinuxFileHandle)> {
869        let namespace_store = self
870            .inner
871            .namespace_store
872            .as_ref()
873            .ok_or(LinuxCloudFilesError::NamespaceMutationNotConfigured)?;
874        let parent_node = self.getattr(parent, None).await?;
875        if parent_node.attributes().kind() != crate::LinuxNodeKind::Directory {
876            return Err(LinuxCloudFilesError::NotDirectory);
877        }
878        let request = LinuxCreateFileRequest::new(
879            parent_node.key().clone(),
880            name,
881            mode,
882            umask,
883            access,
884            self.inner.session_generation,
885        )?;
886        let handle = lock(&self.inner.state).allocate()?;
887        let acceptance = namespace_store.create_file(&request).await?;
888        self.accept_created_file(&request, acceptance, handle)
889    }
890
891    /// Atomically accepts a durable directory create and exposes its stable inode.
892    /// # Errors
893    ///
894    /// Returns an error when validation fails or an underlying backend, store, or platform
895    /// operation fails.
896    pub async fn create_directory(
897        &self,
898        parent: LinuxInode,
899        name: &str,
900        mode: u32,
901        umask: u32,
902    ) -> Result<LinuxNode> {
903        let namespace_store = self
904            .inner
905            .namespace_store
906            .as_ref()
907            .ok_or(LinuxCloudFilesError::NamespaceMutationNotConfigured)?;
908        let parent_node = self.getattr(parent, None).await?;
909        if parent_node.attributes().kind() != crate::LinuxNodeKind::Directory {
910            return Err(LinuxCloudFilesError::NotDirectory);
911        }
912        let request = LinuxCreateDirectoryRequest::new(
913            parent_node.key().clone(),
914            name,
915            mode,
916            umask,
917            self.inner.session_generation,
918        )?;
919        let item = namespace_store.create_directory(&request).await?;
920        self.accept_namespace_create(&request, item)
921    }
922
923    /// Atomically renames or moves one namespace entry while preserving stable identity.
924    /// # Errors
925    ///
926    /// Returns an error when validation fails or an underlying backend, store, or platform
927    /// operation fails.
928    pub async fn rename(
929        &self,
930        parent: LinuxInode,
931        name: &str,
932        new_parent: LinuxInode,
933        new_name: &str,
934        no_replace: bool,
935    ) -> Result<()> {
936        crate::validate_linux_name(name)?;
937        crate::validate_linux_name(new_name)?;
938        if parent == new_parent && name == new_name {
939            return Ok(());
940        }
941        let namespace_store = self
942            .inner
943            .namespace_store
944            .as_ref()
945            .ok_or(LinuxCloudFilesError::NamespaceMutationNotConfigured)?;
946        let old_parent = self.getattr(parent, None).await?;
947        let new_parent = self.getattr(new_parent, None).await?;
948        if old_parent.attributes().kind() != crate::LinuxNodeKind::Directory
949            || new_parent.attributes().kind() != crate::LinuxNodeKind::Directory
950        {
951            return Err(LinuxCloudFilesError::NotDirectory);
952        }
953        let source = self.lookup(parent, name).await?;
954        let destination = match self.lookup(new_parent.attributes().inode(), new_name).await {
955            Ok(node) => Some(LinuxRenameDestination::new(
956                node.key().clone(),
957                node.generation(),
958                node.attributes().kind(),
959            )),
960            Err(error) if error.error_code() == crate::LinuxErrorCode::NotFound => None,
961            Err(error) => return Err(error),
962        };
963        let request = LinuxRenameRequest::new(
964            source.key().clone(),
965            source.generation(),
966            source.attributes().kind(),
967            old_parent.key().clone(),
968            name,
969            new_parent.key().clone(),
970            new_name,
971            destination,
972            no_replace,
973            self.inner.session_generation,
974        )?;
975        let acceptance = namespace_store.rename(&request).await?;
976        self.accept_rename(&request, acceptance)
977    }
978
979    /// Atomically accepts unlink or rmdir and hides the durable tombstone from new lookups.
980    /// # Errors
981    ///
982    /// Returns an error when validation fails or an underlying backend, store, or platform
983    /// operation fails.
984    pub async fn remove(
985        &self,
986        parent: LinuxInode,
987        name: &str,
988        expected_kind: crate::LinuxNodeKind,
989    ) -> Result<()> {
990        let namespace_store = self
991            .inner
992            .namespace_store
993            .as_ref()
994            .ok_or(LinuxCloudFilesError::NamespaceMutationNotConfigured)?;
995        let parent_node = self.getattr(parent, None).await?;
996        if parent_node.attributes().kind() != crate::LinuxNodeKind::Directory {
997            return Err(LinuxCloudFilesError::NotDirectory);
998        }
999        let child = self.lookup(parent, name).await?;
1000        if child.attributes().kind() != expected_kind {
1001            return Err(match expected_kind {
1002                crate::LinuxNodeKind::File => LinuxCloudFilesError::NotFile,
1003                crate::LinuxNodeKind::Directory => LinuxCloudFilesError::NotDirectory,
1004            });
1005        }
1006        let request = LinuxRemoveRequest::new(
1007            child.key().clone(),
1008            child.generation(),
1009            parent_node.key().clone(),
1010            name,
1011            expected_kind,
1012            self.inner.session_generation,
1013        )?;
1014        let tombstone = namespace_store.remove(&request).await?;
1015        self.accept_remove(&request, tombstone)
1016    }
1017
1018    /// Opens a directory snapshot that includes durable local creates accepted before this call.
1019    /// # Errors
1020    ///
1021    /// Returns an error when validation fails or an underlying backend, store, or platform
1022    /// operation fails.
1023    #[expect(
1024        clippy::too_many_lines,
1025        reason = "the directory snapshot keeps backend, created, renamed, and tombstoned entries in one ordering pass"
1026    )]
1027    pub async fn open_directory(&self, inode: LinuxInode) -> Result<LinuxDirectoryHandle> {
1028        let node = self.getattr(inode, None).await?;
1029        if node.attributes().kind() != crate::LinuxNodeKind::Directory {
1030            return Err(LinuxCloudFilesError::NotDirectory);
1031        }
1032        let directory_item = match self.overlay_item(inode) {
1033            Some((item, _)) => item,
1034            None => self.inner.readonly.load_item_for_overlay(inode).await?,
1035        };
1036        let backend_children = self
1037            .inner
1038            .readonly
1039            .load_children_for_overlay(directory_item.key())
1040            .await?;
1041        let parent = match directory_item.parent_id() {
1042            Some(parent_id) => self
1043                .inode_for_key(&CloudItemKey::new(
1044                    directory_item.key().scope().clone(),
1045                    parent_id.clone(),
1046                ))
1047                .ok_or(LinuxCloudFilesError::MissingInodeRecord)?,
1048            None => inode,
1049        };
1050        let (tombstoned_names, overlay_names, mut local) = {
1051            let state = lock(&self.inner.state);
1052            let mut overlay_names = HashSet::new();
1053            let mut local = Vec::new();
1054
1055            for (name, child_inode) in state.namespace_by_name.children(node.key().item_id()) {
1056                if let Some(item) = state.namespace_by_inode.get(child_inode) {
1057                    overlay_names.insert(name.to_owned());
1058                    local.push((
1059                        name.to_owned(),
1060                        item.item().clone(),
1061                        item.inode_record().clone(),
1062                    ));
1063                }
1064            }
1065            for (name, child_inode) in state.created_by_name.children(node.key().item_id()) {
1066                if !overlay_names.contains(name)
1067                    && let Some(created) = state.created_by_inode.get(child_inode)
1068                {
1069                    overlay_names.insert(name.to_owned());
1070                    local.push((
1071                        name.to_owned(),
1072                        created.item().clone(),
1073                        created.inode_record().clone(),
1074                    ));
1075                }
1076            }
1077            for (name, child_inode) in state.remote_by_name.children(node.key().item_id()) {
1078                if !overlay_names.contains(name)
1079                    && let Some(remote) = state.remote_by_inode.get(child_inode)
1080                {
1081                    overlay_names.insert(name.to_owned());
1082                    local.push((
1083                        name.to_owned(),
1084                        remote.item().clone(),
1085                        remote.inode_record().clone(),
1086                    ));
1087                }
1088            }
1089
1090            let mut tombstoned_names = state
1091                .tombstones_by_name
1092                .children(node.key().item_id())
1093                .map(|(name, _)| name.to_owned())
1094                .chain(
1095                    state
1096                        .remote_tombstones_by_name
1097                        .children(node.key().item_id())
1098                        .map(|(name, ())| name.to_owned()),
1099                )
1100                .collect::<HashSet<_>>();
1101            // A current overlay entry is authoritative over an older tombstone for the same
1102            // parent/name. Transition code removes those tombstones, while this subtraction keeps
1103            // a restored legacy state internally consistent during migration.
1104            tombstoned_names.retain(|name| !overlay_names.contains(name));
1105            (tombstoned_names, overlay_names, local)
1106        };
1107        let mut entries = backend_children
1108            .into_iter()
1109            .filter(|item| {
1110                !tombstoned_names.contains(item.name()) && !overlay_names.contains(item.name())
1111            })
1112            .map(|item| {
1113                let record = self
1114                    .record_for_key(item.key())
1115                    .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
1116                let child = self
1117                    .inner
1118                    .readonly
1119                    .node_from_item_and_record(&item, &record)?;
1120                Ok(LinuxDirectoryEntry::from_node(
1121                    item.name().to_owned(),
1122                    &child,
1123                ))
1124            })
1125            .collect::<Result<Vec<_>>>()?;
1126        local.retain(|(name, _, _)| !tombstoned_names.contains(name));
1127        local.sort_by(|left, right| left.0.cmp(&right.0));
1128        for (name, item, record) in local {
1129            if entries.iter().any(|entry| entry.name() == name) {
1130                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1131                    reason: "durable overlay collided with another visible directory entry",
1132                });
1133            }
1134            let child = self
1135                .inner
1136                .readonly
1137                .node_from_item_and_record(&item, &record)?;
1138            entries.push(LinuxDirectoryEntry::from_node(name, &child));
1139        }
1140        let snapshot = LinuxDirectorySnapshot::with_entries(inode, parent, entries);
1141        let mut state = lock(&self.inner.state);
1142        let handle = state.allocate_directory()?;
1143        state.directories.insert(handle, snapshot);
1144        Ok(handle)
1145    }
1146
1147    /// Returns the immutable writable-directory snapshot for one handle.
1148    /// # Errors
1149    ///
1150    /// Returns an error when validation fails or an underlying backend, store, or platform
1151    /// operation fails.
1152    pub fn directory_snapshot(
1153        &self,
1154        inode: LinuxInode,
1155        handle: LinuxDirectoryHandle,
1156    ) -> Result<LinuxDirectorySnapshot> {
1157        let state = lock(&self.inner.state);
1158        let snapshot = state
1159            .directories
1160            .get(&handle)
1161            .ok_or(LinuxCloudFilesError::StaleHandle)?;
1162        if snapshot.directory() != inode {
1163            return Err(LinuxCloudFilesError::StaleHandle);
1164        }
1165        Ok(snapshot.clone())
1166    }
1167
1168    fn overlay_item(&self, inode: LinuxInode) -> Option<(CloudItem, crate::LinuxInodeRecord)> {
1169        let state = lock(&self.inner.state);
1170        if let Some(item) = state.namespace_by_inode.get(&inode) {
1171            return Some((item.item().clone(), item.inode_record().clone()));
1172        }
1173        if let Some(created) = state.created_by_inode.get(&inode) {
1174            return Some((created.item().clone(), created.inode_record().clone()));
1175        }
1176        state
1177            .remote_by_inode
1178            .get(&inode)
1179            .map(|entry| (entry.item().clone(), entry.inode_record().clone()))
1180    }
1181
1182    fn record_for_key(&self, key: &CloudItemKey) -> Option<crate::LinuxInodeRecord> {
1183        let state = lock(&self.inner.state);
1184        self.record_for_key_in_state(&state, key)
1185    }
1186
1187    fn record_for_key_in_state(
1188        &self,
1189        state: &WritableState,
1190        key: &CloudItemKey,
1191    ) -> Option<crate::LinuxInodeRecord> {
1192        state
1193            .namespace_by_key
1194            .get(key)
1195            .and_then(|inode| state.namespace_by_inode.get(inode))
1196            .map(|item| item.inode_record().clone())
1197            .or_else(|| {
1198                state
1199                    .created_by_key
1200                    .get(key)
1201                    .and_then(|inode| state.created_by_inode.get(inode))
1202                    .map(|item| item.inode_record().clone())
1203            })
1204            .or_else(|| {
1205                state
1206                    .remote_by_key
1207                    .get(key)
1208                    .and_then(|inode| state.remote_by_inode.get(inode))
1209                    .map(|item| item.inode_record().clone())
1210            })
1211            .or_else(|| self.inner.readonly.inode_table().by_key(key).cloned())
1212    }
1213
1214    fn inode_for_key(&self, key: &CloudItemKey) -> Option<LinuxInode> {
1215        self.record_for_key(key).map(|record| record.inode())
1216    }
1217
1218    fn inode_for_key_in_state(
1219        &self,
1220        state: &WritableState,
1221        key: &CloudItemKey,
1222    ) -> Option<LinuxInode> {
1223        self.record_for_key_in_state(state, key)
1224            .map(|record| record.inode())
1225    }
1226
1227    fn key_for_inode_in_state(
1228        &self,
1229        state: &WritableState,
1230        inode: LinuxInode,
1231    ) -> Option<CloudItemKey> {
1232        state
1233            .namespace_by_inode
1234            .get(&inode)
1235            .map(|item| item.item().key().clone())
1236            .or_else(|| {
1237                state
1238                    .created_by_inode
1239                    .get(&inode)
1240                    .map(|item| item.item().key().clone())
1241            })
1242            .or_else(|| {
1243                state
1244                    .remote_by_inode
1245                    .get(&inode)
1246                    .map(|item| item.item().key().clone())
1247            })
1248            .or_else(|| {
1249                self.inner
1250                    .readonly
1251                    .inode_table()
1252                    .by_inode(inode)
1253                    .map(|record| record.key().clone())
1254            })
1255    }
1256
1257    fn validate_remote_entry(&self, state: &WritableState, entry: &LinuxRemoteEntry) -> Result<()> {
1258        if entry.item().key() != entry.inode_record().key()
1259            || entry.item().key().scope() != self.inner.readonly.inode_table().scope()
1260            || entry.item().is_root()
1261            || entry.inode_record().inode() == crate::LINUX_ROOT_INODE
1262        {
1263            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1264                reason: "remote overlay entry violated scope, identity, or root boundaries",
1265            });
1266        }
1267        self.inner
1268            .readonly
1269            .node_from_item_and_record(entry.item(), entry.inode_record())?;
1270        if let Some(existing_key) = self.key_for_inode_in_state(state, entry.inode_record().inode())
1271            && existing_key != *entry.item().key()
1272        {
1273            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1274                reason: "remote overlay reused an inode assigned to another stable item",
1275            });
1276        }
1277        if let Some(existing) = self.record_for_key_in_state(state, entry.item().key())
1278            && existing != *entry.inode_record()
1279        {
1280            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1281                reason: "remote overlay changed a stable item inode or generation",
1282            });
1283        }
1284        Ok(())
1285    }
1286
1287    fn validate_remote_delete(
1288        &self,
1289        state: &WritableState,
1290        deleted: &LinuxRemoteDelete,
1291    ) -> Result<()> {
1292        if deleted.key() != deleted.inode_record().key()
1293            || deleted.key().scope() != self.inner.readonly.inode_table().scope()
1294            || deleted.parent_key().scope() != self.inner.readonly.inode_table().scope()
1295            || self.record_for_key_in_state(state, deleted.key()).as_ref()
1296                != Some(deleted.inode_record())
1297        {
1298            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1299                reason: "remote deletion substituted scope, stable identity, or inode generation",
1300            });
1301        }
1302        Ok(())
1303    }
1304
1305    async fn lookup_remote_validation_target(
1306        &self,
1307        parent: LinuxInode,
1308        name: &str,
1309    ) -> Result<Option<LinuxNode>> {
1310        match self.lookup(parent, name).await {
1311            Ok(node) => Ok(Some(node)),
1312            Err(error) if error.error_code() == crate::LinuxErrorCode::NotFound => Ok(None),
1313            Err(error) => Err(error),
1314        }
1315    }
1316
1317    #[expect(
1318        clippy::too_many_lines,
1319        reason = "the remote upsert applies one atomic namespace transition with all identity and replacement checks visible"
1320    )]
1321    async fn apply_remote_upsert(
1322        &self,
1323        entry: LinuxRemoteEntry,
1324        previous: Option<LinuxRemoteLocation>,
1325        replaced: Option<LinuxRemoteDelete>,
1326    ) -> Result<Vec<LinuxInvalidation>> {
1327        let key = entry.item().key().clone();
1328        let inode = entry.inode_record().inode();
1329        let parent_id = entry
1330            .item()
1331            .parent_id()
1332            .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
1333                reason: "remote overlay item omitted its parent",
1334            })?
1335            .clone();
1336        let parent_key = CloudItemKey::new(key.scope().clone(), parent_id.clone());
1337        let parent = self
1338            .inode_for_key(&parent_key)
1339            .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
1340        let target = self
1341            .lookup_remote_validation_target(parent, entry.item().name())
1342            .await?;
1343        match (target.as_ref(), replaced.as_ref()) {
1344            (Some(existing), None) if existing.key() != &key => {
1345                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1346                    reason: "remote upsert omitted the item replaced at its destination",
1347                });
1348            }
1349            (Some(existing), Some(replaced))
1350                if existing.key() == replaced.key()
1351                    && existing.attributes().inode() == replaced.inode_record().inode()
1352                    && existing.generation() == replaced.inode_record().generation()
1353                    && existing.attributes().kind() == replaced.kind()
1354                    && replaced.parent_key() == &parent_key
1355                    && replaced.name() == entry.item().name() => {}
1356            (None | Some(_), None) => {}
1357            _ => {
1358                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1359                    reason: "remote upsert replacement did not match its current destination",
1360                });
1361            }
1362        }
1363
1364        let previous_node = if let Some(location) = previous.as_ref() {
1365            if location.parent_key().scope() != key.scope() {
1366                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1367                    reason: "remote upsert previous location escaped the active scope",
1368                });
1369            }
1370            let previous_parent = self
1371                .inode_for_key(location.parent_key())
1372                .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
1373            let node = self
1374                .lookup_remote_validation_target(previous_parent, location.name())
1375                .await?
1376                .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
1377                    reason: "remote upsert previous location did not exist",
1378                })?;
1379            if node.key() != &key || node.attributes().inode() != inode {
1380                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1381                    reason: "remote upsert previous location named another stable item",
1382                });
1383            }
1384            Some((previous_parent, node))
1385        } else {
1386            None
1387        };
1388
1389        // All in-memory validation and every secondary-index update share one critical section.
1390        // Backend lookups above establish immutable product facts; the lock makes local and remote
1391        // overlay transitions linearizable with respect to those facts.
1392        let name_key = (parent_id, entry.item().name().to_owned());
1393        let mut state = lock(&self.inner.state);
1394        self.validate_remote_entry(&state, &entry)?;
1395        if state.namespace_by_key.contains_key(&key)
1396            || state.created_by_key.contains_key(&key)
1397            || state.deleted_keys.contains(&key)
1398            || state.tombstones_by_name.contains_key(&name_key)
1399        {
1400            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1401                reason: "remote change collided with a pending local namespace mutation",
1402            });
1403        }
1404        if let Some(replaced) = replaced.as_ref() {
1405            self.validate_remote_delete(&state, replaced)?;
1406            if replaced.key() == &key {
1407                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1408                    reason: "remote upsert replaced its own stable identity",
1409                });
1410            }
1411        }
1412
1413        for candidate in [
1414            state.namespace_by_name.get(&name_key),
1415            state.created_by_name.get(&name_key),
1416        ]
1417        .into_iter()
1418        .flatten()
1419        {
1420            if self.key_for_inode_in_state(&state, *candidate).as_ref() != Some(&key) {
1421                return Err(LinuxCloudFilesError::InvalidBackendResponse {
1422                    reason: "remote change destination collided with a local namespace mutation",
1423                });
1424            }
1425        }
1426        if let Some(existing_inode) = state.remote_by_name.get(&name_key)
1427            && self
1428                .key_for_inode_in_state(&state, *existing_inode)
1429                .as_ref()
1430                != Some(&key)
1431            && replaced.as_ref().map(LinuxRemoteDelete::key)
1432                != self
1433                    .key_for_inode_in_state(&state, *existing_inode)
1434                    .as_ref()
1435        {
1436            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1437                reason: "remote upsert did not identify the current destination replacement",
1438            });
1439        }
1440
1441        if let Some(current_inode) = state.remote_by_key.get(&key)
1442            && let Some(current) = state.remote_by_inode.get(current_inode)
1443        {
1444            let current_parent =
1445                current
1446                    .item()
1447                    .parent_id()
1448                    .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
1449                        reason: "current remote overlay item omitted its parent",
1450                    })?;
1451            let current_location = (current_parent, current.item().name());
1452            match previous.as_ref() {
1453                Some(location)
1454                    if current_location == (location.parent_key().item_id(), location.name()) => {}
1455                None if current_location == (&name_key.0, name_key.1.as_str()) => {}
1456                _ => {
1457                    return Err(LinuxCloudFilesError::InvalidBackendResponse {
1458                        reason: "remote upsert previous location was not the current location",
1459                    });
1460                }
1461            }
1462        }
1463
1464        let mut invalidations = Vec::new();
1465        if let Some((previous_parent, _)) = previous_node {
1466            let location =
1467                previous
1468                    .as_ref()
1469                    .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
1470                        reason: "validated previous location disappeared",
1471                    })?;
1472            invalidations.push(LinuxInvalidation::Entry {
1473                parent: previous_parent,
1474                name: location.name().to_owned(),
1475            });
1476        }
1477        if let Some(replaced) = replaced.as_ref() {
1478            let replaced_parent = self
1479                .inode_for_key_in_state(&state, replaced.parent_key())
1480                .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
1481            invalidations.push(LinuxInvalidation::Delete {
1482                parent: replaced_parent,
1483                child: replaced.inode_record().inode(),
1484                name: replaced.name().to_owned(),
1485            });
1486        }
1487        invalidations.push(LinuxInvalidation::Entry {
1488            parent,
1489            name: entry.item().name().to_owned(),
1490        });
1491        invalidations.push(LinuxInvalidation::Inode { inode });
1492
1493        if let Some(location) = previous {
1494            let old_name = (
1495                location.parent_key().item_id().clone(),
1496                location.name().to_owned(),
1497            );
1498            state.remote_by_name.remove(&old_name);
1499            state.remote_tombstones_by_name.insert(old_name, ());
1500        }
1501        if let Some(replaced) = replaced {
1502            let replaced_name = (
1503                replaced.parent_key().item_id().clone(),
1504                replaced.name().to_owned(),
1505            );
1506            if let Some(replaced_inode) = state.remote_by_key.remove(replaced.key()) {
1507                state.remote_by_inode.remove(&replaced_inode);
1508            }
1509            state.remote_by_name.remove(&replaced_name);
1510            state.remote_tombstones_by_name.insert(replaced_name, ());
1511            state.remote_deleted_keys.insert(replaced.key().clone());
1512            state
1513                .remote_deleted_inodes
1514                .insert(replaced.inode_record().inode());
1515        }
1516        state.remote_tombstones_by_name.remove(&name_key);
1517        state.remote_deleted_keys.remove(&key);
1518        state.remote_deleted_inodes.remove(&inode);
1519        state.remote_by_key.insert(key, inode);
1520        state.remote_by_name.insert(name_key, inode);
1521        state.remote_by_inode.insert(inode, entry);
1522        Ok(invalidations)
1523    }
1524
1525    async fn apply_remote_delete(
1526        &self,
1527        deleted: LinuxRemoteDelete,
1528    ) -> Result<Vec<LinuxInvalidation>> {
1529        let parent = self
1530            .inode_for_key(deleted.parent_key())
1531            .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
1532        let current = self
1533            .lookup_remote_validation_target(parent, deleted.name())
1534            .await?
1535            .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
1536                reason: "remote deletion location did not exist",
1537            })?;
1538        if current.key() != deleted.key()
1539            || current.attributes().inode() != deleted.inode_record().inode()
1540            || current.generation() != deleted.inode_record().generation()
1541            || current.attributes().kind() != deleted.kind()
1542        {
1543            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1544                reason: "remote deletion did not match the current namespace entry",
1545            });
1546        }
1547        let inode = deleted.inode_record().inode();
1548        let name_key = (
1549            deleted.parent_key().item_id().clone(),
1550            deleted.name().to_owned(),
1551        );
1552        let mut state = lock(&self.inner.state);
1553        self.validate_remote_delete(&state, &deleted)?;
1554        if state.namespace_by_key.contains_key(deleted.key())
1555            || state.created_by_key.contains_key(deleted.key())
1556            || state.deleted_keys.contains(deleted.key())
1557            || state.tombstones_by_name.contains_key(&name_key)
1558        {
1559            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1560                reason: "remote deletion collided with a pending local namespace mutation",
1561            });
1562        }
1563        if let Some(current_inode) = state.remote_by_name.get(&name_key)
1564            && *current_inode != inode
1565        {
1566            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1567                reason: "remote deletion location changed before commit",
1568            });
1569        }
1570        state.remote_by_key.remove(deleted.key());
1571        state.remote_by_inode.remove(&inode);
1572        state.remote_by_name.remove(&name_key);
1573        state.remote_tombstones_by_name.insert(name_key, ());
1574        state.remote_deleted_keys.insert(deleted.key().clone());
1575        state.remote_deleted_inodes.insert(inode);
1576        Ok(vec![LinuxInvalidation::Delete {
1577            parent,
1578            child: inode,
1579            name: deleted.name().to_owned(),
1580        }])
1581    }
1582
1583    /// Releases one writable-directory snapshot handle.
1584    /// # Errors
1585    ///
1586    /// Returns an error when validation fails or an underlying backend, store, or platform
1587    /// operation fails.
1588    pub fn release_directory(&self, handle: LinuxDirectoryHandle) -> Result<()> {
1589        if lock(&self.inner.state)
1590            .directories
1591            .remove(&handle)
1592            .is_none()
1593        {
1594            return Err(LinuxCloudFilesError::StaleHandle);
1595        }
1596        Ok(())
1597    }
1598
1599    /// Reads from the remote revision or current durable staging, depending on the handle mode.
1600    /// # Errors
1601    ///
1602    /// Returns an error when validation fails or an underlying backend, store, or platform
1603    /// operation fails.
1604    pub async fn read_file(
1605        &self,
1606        inode: LinuxInode,
1607        handle: LinuxFileHandle,
1608        offset: u64,
1609        size: u32,
1610    ) -> Result<Bytes> {
1611        let opened = {
1612            let state = lock(&self.inner.state);
1613            state
1614                .files
1615                .get(&handle)
1616                .cloned()
1617                .ok_or(LinuxCloudFilesError::StaleHandle)?
1618        };
1619        match opened {
1620            OpenFile::Writeback {
1621                inode: opened_inode,
1622                access,
1623                session,
1624                size: logical_size,
1625                ..
1626            } if opened_inode == inode && access.readable() => {
1627                if size == 0 || offset >= logical_size {
1628                    return Ok(Bytes::new());
1629                }
1630                let expected = u64::from(size).min(logical_size - offset);
1631                let bytes = self.inner.store.read(&session, offset, size).await?;
1632                let actual = u64::try_from(bytes.len()).map_err(|_| {
1633                    LinuxCloudFilesError::InvalidBackendResponse {
1634                        reason: "writeback read length cannot be represented as u64",
1635                    }
1636                })?;
1637                if actual != expected {
1638                    return Err(LinuxCloudFilesError::InvalidBackendResponse {
1639                        reason: "writeback read returned a partial non-EOF range",
1640                    });
1641                }
1642                Ok(bytes)
1643            }
1644            OpenFile::Writeback { access, .. } if !access.readable() => {
1645                Err(LinuxCloudFilesError::AccessModeMismatch)
1646            }
1647            OpenFile::Writeback { .. } => Err(LinuxCloudFilesError::StaleHandle),
1648        }
1649    }
1650
1651    /// Durably applies a positioned write before reporting accepted bytes to FUSE.
1652    /// # Errors
1653    ///
1654    /// Returns an error when validation fails or an underlying backend, store, or platform
1655    /// operation fails.
1656    pub async fn write_file(
1657        &self,
1658        inode: LinuxInode,
1659        handle: LinuxFileHandle,
1660        offset: u64,
1661        bytes: Bytes,
1662    ) -> Result<u32> {
1663        let byte_count =
1664            u32::try_from(bytes.len()).map_err(|_| LinuxCloudFilesError::InvalidConfiguration {
1665                reason: "FUSE write length cannot be represented as u32",
1666            })?;
1667        if bytes.is_empty() {
1668            return Ok(0);
1669        }
1670        let end = offset.checked_add(u64::from(byte_count)).ok_or(
1671            LinuxCloudFilesError::InvalidConfiguration {
1672                reason: "FUSE write range exceeds u64",
1673            },
1674        )?;
1675        let session = self.write_session(inode, handle)?;
1676        let commit = self.inner.store.write(&session, offset, bytes).await?;
1677        self.apply_commit(handle, inode, commit, Some(end), false)?;
1678        Ok(byte_count)
1679    }
1680
1681    /// Durably truncates or extends the staged file.
1682    /// # Errors
1683    ///
1684    /// Returns an error when validation fails or an underlying backend, store, or platform
1685    /// operation fails.
1686    pub async fn truncate_file(
1687        &self,
1688        inode: LinuxInode,
1689        handle: LinuxFileHandle,
1690        size: u64,
1691    ) -> Result<LinuxNode> {
1692        let session = self.write_session(inode, handle)?;
1693        let commit = self.inner.store.truncate(&session, size).await?;
1694        self.apply_commit(handle, inode, commit, Some(size), false)?;
1695        self.getattr(inode, Some(handle)).await
1696    }
1697
1698    /// Opens, truncates, and releases one existing file for handle-less `setattr` requests.
1699    /// # Errors
1700    ///
1701    /// Returns an error when validation fails or an underlying backend, store, or platform
1702    /// operation fails.
1703    pub async fn truncate_once(&self, inode: LinuxInode, size: u64) -> Result<LinuxNode> {
1704        let handle = self.open_file(inode, LinuxFileAccess::Write).await?;
1705        let result = self.truncate_file(inode, handle, size).await;
1706        let release = self.release_file(handle).await;
1707        match (result, release) {
1708            (Ok(node), Ok(())) => Ok(node),
1709            (Err(error), _) | (Ok(_), Err(error)) => Err(error),
1710        }
1711    }
1712
1713    /// Applies an idempotent flush or explicit fsync durability boundary.
1714    /// # Errors
1715    ///
1716    /// Returns an error when validation fails or an underlying backend, store, or platform
1717    /// operation fails.
1718    pub async fn sync_file(
1719        &self,
1720        inode: LinuxInode,
1721        handle: LinuxFileHandle,
1722        data_only: bool,
1723    ) -> Result<()> {
1724        let Some(session) = self.sync_session(inode, handle)? else {
1725            return Ok(());
1726        };
1727        if let Some(commit) = self.inner.store.sync(&session, data_only).await? {
1728            self.apply_commit(handle, inode, commit, None, true)?;
1729        }
1730        Ok(())
1731    }
1732
1733    fn sync_session(
1734        &self,
1735        inode: LinuxInode,
1736        handle: LinuxFileHandle,
1737    ) -> Result<Option<LinuxWriteSessionId>> {
1738        let state = lock(&self.inner.state);
1739        match state.files.get(&handle) {
1740            Some(OpenFile::Writeback {
1741                inode: opened_inode,
1742                access,
1743                session,
1744                ..
1745            }) if *opened_inode == inode && access.writable() => Ok(Some(session.clone())),
1746            Some(OpenFile::Writeback {
1747                inode: opened_inode,
1748                access: LinuxFileAccess::Read,
1749                ..
1750            }) if *opened_inode == inode => Ok(None),
1751            Some(OpenFile::Writeback { .. }) | None => Err(LinuxCloudFilesError::StaleHandle),
1752        }
1753    }
1754
1755    /// Releases one file handle and its product-owned writeback session.
1756    /// # Errors
1757    ///
1758    /// Returns an error when validation fails or an underlying backend, store, or platform
1759    /// operation fails.
1760    pub async fn release_file(&self, handle: LinuxFileHandle) -> Result<()> {
1761        let opened = lock(&self.inner.state)
1762            .files
1763            .remove(&handle)
1764            .ok_or(LinuxCloudFilesError::StaleHandle)?;
1765        match opened {
1766            OpenFile::Writeback { session, .. } => {
1767                self.inner.store.close(&session).await?;
1768                Ok(())
1769            }
1770        }
1771    }
1772
1773    fn write_session(
1774        &self,
1775        inode: LinuxInode,
1776        handle: LinuxFileHandle,
1777    ) -> Result<LinuxWriteSessionId> {
1778        let state = lock(&self.inner.state);
1779        match state.files.get(&handle) {
1780            Some(OpenFile::Writeback {
1781                inode: opened_inode,
1782                access,
1783                session,
1784                ..
1785            }) if *opened_inode == inode && access.writable() => Ok(session.clone()),
1786            Some(OpenFile::Writeback { .. }) => Err(LinuxCloudFilesError::AccessModeMismatch),
1787            None => Err(LinuxCloudFilesError::StaleHandle),
1788        }
1789    }
1790
1791    fn accept_created_file(
1792        &self,
1793        request: &LinuxCreateFileRequest,
1794        acceptance: LinuxCreateFileAcceptance,
1795        handle: LinuxFileHandle,
1796    ) -> Result<(LinuxNode, LinuxFileHandle)> {
1797        let (created, session) = acceptance.into_parts();
1798        self.validate_created_file(&created, Some(request))?;
1799        self.validate_created_session(&created, &session)?;
1800        let node = self
1801            .inner
1802            .readonly
1803            .node_from_item_and_record(created.item(), created.inode_record())?;
1804        let inode = created.inode_record().inode();
1805        let key = created.item().key().clone();
1806        let name_key = (
1807            request.parent_key().item_id().clone(),
1808            request.name().to_owned(),
1809        );
1810        let mut state = lock(&self.inner.state);
1811        self.ensure_created_file_is_unique(&state, &created, &name_key)?;
1812        if state.files.values().any(|opened| match opened {
1813            OpenFile::Writeback {
1814                session: opened_session,
1815                ..
1816            } => opened_session == session.id(),
1817        }) {
1818            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1819                reason: "namespace store reused an active write session identity",
1820            });
1821        }
1822        state.tombstones_by_name.remove(&name_key);
1823        state.remote_tombstones_by_name.remove(&name_key);
1824        state.created_by_key.insert(key, inode);
1825        state.created_by_name.insert(name_key, inode);
1826        state.created_by_inode.insert(inode, created);
1827        state.files.insert(
1828            handle,
1829            OpenFile::Writeback {
1830                inode,
1831                access: request.access(),
1832                session: session.id().clone(),
1833                size: 0,
1834                generation: None,
1835            },
1836        );
1837        Ok((node, handle))
1838    }
1839
1840    fn accept_namespace_create(
1841        &self,
1842        request: &LinuxCreateDirectoryRequest,
1843        item: LinuxNamespaceItem,
1844    ) -> Result<LinuxNode> {
1845        self.validate_namespace_create(request, &item)?;
1846        let node = self
1847            .inner
1848            .readonly
1849            .node_from_item_and_record(item.item(), item.inode_record())?;
1850        let name_key = (
1851            request.parent_key().item_id().clone(),
1852            request.name().to_owned(),
1853        );
1854        let mut state = lock(&self.inner.state);
1855        self.ensure_namespace_item_is_unique(&state, &item, &name_key, None)?;
1856        let inode = item.inode_record().inode();
1857        state.tombstones_by_name.remove(&name_key);
1858        state.remote_tombstones_by_name.remove(&name_key);
1859        state
1860            .namespace_by_key
1861            .insert(item.item().key().clone(), inode);
1862        state.namespace_by_name.insert(name_key, inode);
1863        state.namespace_by_inode.insert(inode, item);
1864        Ok(node)
1865    }
1866
1867    fn accept_rename(
1868        &self,
1869        request: &LinuxRenameRequest,
1870        acceptance: LinuxRenameAcceptance,
1871    ) -> Result<()> {
1872        let (item, source, replaced) = acceptance.into_parts();
1873        self.validate_rename_acceptance(request, &item, &source, replaced.as_ref())?;
1874        let new_name_key = (
1875            request.new_parent_key().item_id().clone(),
1876            request.new_name().to_owned(),
1877        );
1878        let old_name_key = (
1879            request.old_parent_key().item_id().clone(),
1880            request.old_name().to_owned(),
1881        );
1882        let inode = item.inode_record().inode();
1883        let mut state = lock(&self.inner.state);
1884        self.ensure_namespace_item_is_unique(&state, &item, &new_name_key, Some(request.key()))?;
1885        state.namespace_by_name.remove(&old_name_key);
1886        state.created_by_name.remove(&old_name_key);
1887        state.tombstones_by_name.insert(old_name_key, source);
1888        if let Some(replaced) = replaced {
1889            let replaced_name_key = (
1890                replaced.parent_key().item_id().clone(),
1891                replaced.name().to_owned(),
1892            );
1893            if let Some(replaced_inode) = state
1894                .namespace_by_key
1895                .remove(replaced.key())
1896                .or_else(|| state.created_by_key.remove(replaced.key()))
1897                .or_else(|| {
1898                    self.inner
1899                        .readonly
1900                        .inode_table()
1901                        .by_key(replaced.key())
1902                        .map(crate::LinuxInodeRecord::inode)
1903                })
1904            {
1905                state.namespace_by_inode.remove(&replaced_inode);
1906                state.created_by_name.remove(&replaced_name_key);
1907            }
1908            state.deleted_keys.insert(replaced.key().clone());
1909            state.tombstones_by_name.insert(replaced_name_key, replaced);
1910        }
1911        state.deleted_keys.remove(request.key());
1912        state.tombstones_by_name.remove(&new_name_key);
1913        state.remote_tombstones_by_name.remove(&new_name_key);
1914        state.namespace_by_key.insert(request.key().clone(), inode);
1915        state.namespace_by_name.insert(new_name_key.clone(), inode);
1916        state.namespace_by_inode.insert(inode, item);
1917        if state.created_by_key.contains_key(request.key()) {
1918            state.created_by_name.insert(new_name_key, inode);
1919        }
1920        Ok(())
1921    }
1922
1923    fn accept_remove(
1924        &self,
1925        request: &LinuxRemoveRequest,
1926        tombstone: LinuxNamespaceTombstone,
1927    ) -> Result<()> {
1928        Self::validate_remove_acceptance(request, &tombstone)?;
1929        let name_key = (
1930            request.parent_key().item_id().clone(),
1931            request.name().to_owned(),
1932        );
1933        let mut state = lock(&self.inner.state);
1934        if let Some(inode) = state
1935            .namespace_by_key
1936            .remove(request.key())
1937            .or_else(|| state.created_by_key.get(request.key()).copied())
1938        {
1939            state.namespace_by_inode.remove(&inode);
1940        }
1941        state.namespace_by_name.remove(&name_key);
1942        state.created_by_name.remove(&name_key);
1943        state.deleted_keys.insert(request.key().clone());
1944        state.tombstones_by_name.insert(name_key, tombstone);
1945        Ok(())
1946    }
1947
1948    fn validate_namespace_create(
1949        &self,
1950        request: &LinuxCreateDirectoryRequest,
1951        item: &LinuxNamespaceItem,
1952    ) -> Result<()> {
1953        self.validate_namespace_item(item)?;
1954        let DesiredMutation::Create {
1955            scope,
1956            parent_id,
1957            name,
1958            kind,
1959        } = item.mutation_intent().desired()
1960        else {
1961            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1962                reason: "directory create did not persist a create mutation",
1963            });
1964        };
1965        if item.item().kind() != CloudItemKind::Directory
1966            || item.item().content().is_some()
1967            || item.item().parent_id() != Some(request.parent_key().item_id())
1968            || item.item().name() != request.name()
1969            || scope != request.parent_key().scope()
1970            || parent_id != request.parent_key().item_id()
1971            || name != request.name()
1972            || *kind != CloudItemKind::Directory
1973            || item.mutation_intent().session_generation() != request.session_generation()
1974        {
1975            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1976                reason: "directory create acceptance did not match the native request",
1977            });
1978        }
1979        Ok(())
1980    }
1981
1982    fn validate_rename_acceptance(
1983        &self,
1984        request: &LinuxRenameRequest,
1985        item: &LinuxNamespaceItem,
1986        source: &LinuxNamespaceTombstone,
1987        replaced: Option<&LinuxNamespaceTombstone>,
1988    ) -> Result<()> {
1989        self.validate_namespace_item(item)?;
1990        let DesiredMutation::ModifyMetadata {
1991            key,
1992            parent_id,
1993            name,
1994        } = item.mutation_intent().desired()
1995        else {
1996            return Err(LinuxCloudFilesError::InvalidBackendResponse {
1997                reason: "rename did not persist a metadata mutation",
1998            });
1999        };
2000        let expected_kind = match request.kind() {
2001            crate::LinuxNodeKind::File => CloudItemKind::File,
2002            crate::LinuxNodeKind::Directory => CloudItemKind::Directory,
2003        };
2004        if item.item().key() != request.key()
2005            || item.inode_record().key() != request.key()
2006            || item.inode_record().generation() != request.inode_generation()
2007            || item.item().kind() != expected_kind
2008            || item.item().parent_id() != Some(request.new_parent_key().item_id())
2009            || item.item().name() != request.new_name()
2010            || key != request.key()
2011            || parent_id.as_ref() != Some(request.new_parent_key().item_id())
2012            || name.as_deref() != Some(request.new_name())
2013            || item.mutation_intent().origin() != MutationOrigin::PlatformCommand
2014            || item.mutation_intent().session_generation() != request.session_generation()
2015            || source.key() != request.key()
2016            || source.inode_generation() != request.inode_generation()
2017            || source.parent_key() != request.old_parent_key()
2018            || source.name() != request.old_name()
2019            || source.kind() != request.kind()
2020            || source.mutation_intent().operation_id() != item.mutation_intent().operation_id()
2021        {
2022            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2023                reason: "rename acceptance substituted namespace identity or intent",
2024            });
2025        }
2026        match (request.destination(), replaced) {
2027            (None, None) => {}
2028            (Some(_), _) if request.no_replace() => {
2029                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2030                    reason: "no-replace rename accepted an existing destination",
2031                });
2032            }
2033            (Some(destination), Some(replaced))
2034                if replaced.key() == destination.key()
2035                    && replaced.inode_generation() == destination.inode_generation()
2036                    && replaced.kind() == destination.kind()
2037                    && replaced.parent_key() == request.new_parent_key()
2038                    && replaced.name() == request.new_name() => {}
2039            _ => {
2040                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2041                    reason: "rename replacement tombstone did not match the destination",
2042                });
2043            }
2044        }
2045        Ok(())
2046    }
2047
2048    fn validate_remove_acceptance(
2049        request: &LinuxRemoveRequest,
2050        tombstone: &LinuxNamespaceTombstone,
2051    ) -> Result<()> {
2052        let intent = tombstone.mutation_intent();
2053        if tombstone.key() != request.key()
2054            || tombstone.inode_generation() != request.inode_generation()
2055            || tombstone.parent_key() != request.parent_key()
2056            || tombstone.name() != request.name()
2057            || tombstone.kind() != request.kind()
2058            || intent.origin() != MutationOrigin::PlatformCommand
2059            || intent.session_generation() != request.session_generation()
2060            || !matches!(intent.desired(), DesiredMutation::Delete { key } if key == request.key())
2061        {
2062            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2063                reason: "remove acceptance substituted namespace identity or intent",
2064            });
2065        }
2066        Ok(())
2067    }
2068
2069    fn validate_namespace_item(&self, item: &LinuxNamespaceItem) -> Result<()> {
2070        item.mutation_intent()
2071            .validate_for_persistence()
2072            .map_err(|_| LinuxCloudFilesError::InvalidBackendResponse {
2073                reason: "namespace item returned an invalid mutation intent",
2074            })?;
2075        if item.item().is_root()
2076            || item.item().key() != item.inode_record().key()
2077            || item.inode_record().inode() == crate::LINUX_ROOT_INODE
2078            || item.item().key().scope() != self.inner.readonly.inode_table().scope()
2079            || item.mutation_intent().session_generation() > self.inner.session_generation
2080        {
2081            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2082                reason: "namespace item returned an invalid stable mapping",
2083            });
2084        }
2085        crate::validate_linux_name(item.item().name())?;
2086        Ok(())
2087    }
2088
2089    fn ensure_namespace_item_is_unique(
2090        &self,
2091        state: &WritableState,
2092        item: &LinuxNamespaceItem,
2093        name_key: &(CloudItemId, String),
2094        replacing_key: Option<&CloudItemKey>,
2095    ) -> Result<()> {
2096        let record = item.inode_record();
2097        let static_key_conflict = self
2098            .inner
2099            .readonly
2100            .inode_table()
2101            .by_key(item.item().key())
2102            .is_some_and(|existing| replacing_key != Some(existing.key()));
2103        let static_inode_conflict = self
2104            .inner
2105            .readonly
2106            .inode_table()
2107            .by_inode(record.inode())
2108            .is_some_and(|existing| replacing_key != Some(existing.key()));
2109        let dynamic_key_conflict = state
2110            .namespace_by_key
2111            .get(item.item().key())
2112            .is_some_and(|_| replacing_key != Some(item.item().key()));
2113        let dynamic_inode_conflict = state
2114            .namespace_by_inode
2115            .get(&record.inode())
2116            .is_some_and(|existing| replacing_key != Some(existing.item().key()));
2117        let name_conflict = state
2118            .namespace_by_name
2119            .get(name_key)
2120            .or_else(|| state.created_by_name.get(name_key))
2121            .is_some_and(|inode| *inode != record.inode());
2122        if static_key_conflict
2123            || static_inode_conflict
2124            || dynamic_key_conflict
2125            || dynamic_inode_conflict
2126            || name_conflict
2127        {
2128            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2129                reason: "namespace store returned a duplicate key, inode, or parent/name mapping",
2130            });
2131        }
2132        Ok(())
2133    }
2134
2135    fn validate_created_session(
2136        &self,
2137        created: &LinuxCreatedFile,
2138        session: &LinuxWriteSession,
2139    ) -> Result<()> {
2140        if session.key() != created.item().key() {
2141            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2142                reason: "create staging session returned a different stable item identity",
2143            });
2144        }
2145        if session.size() != 0 || session.snapshot().is_some() {
2146            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2147                reason: "create staging session was not a clean empty file",
2148            });
2149        }
2150        if session.session_generation() != self.inner.session_generation {
2151            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2152                reason: "create staging session used another mount generation",
2153            });
2154        }
2155        Ok(())
2156    }
2157
2158    fn validate_created_file(
2159        &self,
2160        created: &LinuxCreatedFile,
2161        request: Option<&LinuxCreateFileRequest>,
2162    ) -> Result<()> {
2163        let item = created.item();
2164        let record = created.inode_record();
2165        let intent = created.mutation_intent();
2166        intent.validate_for_persistence().map_err(|_| {
2167            LinuxCloudFilesError::InvalidBackendResponse {
2168                reason: "namespace store returned an invalid durable mutation intent",
2169            }
2170        })?;
2171        if item.kind() != CloudItemKind::File || item.is_root() {
2172            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2173                reason: "namespace store create did not return a non-root regular file",
2174            });
2175        }
2176        if item.content().is_none_or(|content| content.size() != 0) {
2177            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2178                reason: "namespace store create did not return empty file metadata",
2179            });
2180        }
2181        if record.key() != item.key() || record.inode() == crate::LINUX_ROOT_INODE {
2182            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2183                reason: "namespace store inode allocation did not match the created item",
2184            });
2185        }
2186        if item.key().scope() != self.inner.readonly.inode_table().scope() {
2187            return Err(LinuxCloudFilesError::ScopeMismatch);
2188        }
2189        let DesiredMutation::Create {
2190            scope,
2191            parent_id,
2192            name,
2193            kind,
2194        } = intent.desired()
2195        else {
2196            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2197                reason: "namespace store did not persist a create mutation intent",
2198            });
2199        };
2200        if intent.origin() != MutationOrigin::PlatformCommand
2201            || scope != item.key().scope()
2202            || item.parent_id() != Some(parent_id)
2203            || item.name() != name
2204            || *kind != CloudItemKind::File
2205        {
2206            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2207                reason: "namespace store mutation intent did not match the created file",
2208            });
2209        }
2210        if intent.session_generation() > self.inner.session_generation {
2211            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2212                reason: "recovered create intent came from a future mount generation",
2213            });
2214        }
2215        if let Some(request) = request
2216            && (request.parent_key().scope() != scope
2217                || request.parent_key().item_id() != parent_id
2218                || request.name() != name
2219                || request.session_generation() != intent.session_generation())
2220        {
2221            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2222                reason: "namespace store create acceptance did not match the native request",
2223            });
2224        }
2225        crate::validate_linux_name(item.name())?;
2226        Ok(())
2227    }
2228
2229    fn ensure_created_file_is_unique(
2230        &self,
2231        state: &WritableState,
2232        created: &LinuxCreatedFile,
2233        name_key: &(CloudItemId, String),
2234    ) -> Result<()> {
2235        let record = created.inode_record();
2236        if self
2237            .inner
2238            .readonly
2239            .inode_table()
2240            .by_key(created.item().key())
2241            .is_some()
2242            || self
2243                .inner
2244                .readonly
2245                .inode_table()
2246                .by_inode(record.inode())
2247                .is_some()
2248            || state.created_by_key.contains_key(created.item().key())
2249            || state.created_by_inode.contains_key(&record.inode())
2250            || state.created_by_name.contains_key(name_key)
2251        {
2252            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2253                reason: "namespace store returned a duplicate key, inode, or parent/name mapping",
2254            });
2255        }
2256        Ok(())
2257    }
2258
2259    async fn restore_created_files(&self, created: Vec<LinuxCreatedFile>) -> Result<()> {
2260        for created in created {
2261            self.validate_created_file(&created, None)?;
2262            let parent_id = created
2263                .item()
2264                .parent_id()
2265                .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
2266                    reason: "recovered create omitted its parent identity",
2267                })?
2268                .clone();
2269            let parent_key =
2270                CloudItemKey::new(created.item().key().scope().clone(), parent_id.clone());
2271            let parent_inode = self.inode_for_key(&parent_key).ok_or(
2272                LinuxCloudFilesError::InvalidBackendResponse {
2273                    reason: "recovered create parent omitted a restored inode record",
2274                },
2275            )?;
2276            let parent = self.getattr(parent_inode, None).await?;
2277            if parent.attributes().kind() != crate::LinuxNodeKind::Directory {
2278                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2279                    reason: "recovered create parent was not a directory",
2280                });
2281            }
2282            let mut state = lock(&self.inner.state);
2283            let name_key = state
2284                .namespace_by_key
2285                .get(created.item().key())
2286                .and_then(|inode| state.namespace_by_inode.get(inode))
2287                .and_then(|item| {
2288                    item.item().parent_id().map(|current_parent| {
2289                        (current_parent.clone(), item.item().name().to_owned())
2290                    })
2291                })
2292                .unwrap_or_else(|| (parent_id, created.item().name().to_owned()));
2293            self.ensure_created_file_is_unique(&state, &created, &name_key)?;
2294            let inode = created.inode_record().inode();
2295            state
2296                .created_by_key
2297                .insert(created.item().key().clone(), inode);
2298            state.created_by_name.insert(name_key, inode);
2299            state.created_by_inode.insert(inode, created);
2300        }
2301        Ok(())
2302    }
2303
2304    fn restore_namespace_overlay(&self, overlay: LinuxNamespaceOverlay) -> Result<()> {
2305        let (items, tombstones) = overlay.into_parts();
2306        for item in items {
2307            self.validate_namespace_item(&item)?;
2308            let parent_id = item
2309                .item()
2310                .parent_id()
2311                .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
2312                    reason: "restored namespace item omitted its parent identity",
2313                })?
2314                .clone();
2315            let name_key = (parent_id, item.item().name().to_owned());
2316            let mut state = lock(&self.inner.state);
2317            self.ensure_namespace_item_is_unique(&state, &item, &name_key, None)?;
2318            let inode = item.inode_record().inode();
2319            state
2320                .namespace_by_key
2321                .insert(item.item().key().clone(), inode);
2322            state.namespace_by_name.insert(name_key, inode);
2323            state.namespace_by_inode.insert(inode, item);
2324        }
2325        for tombstone in tombstones {
2326            if tombstone.key().scope() != self.inner.readonly.inode_table().scope()
2327                || tombstone.parent_key().scope() != self.inner.readonly.inode_table().scope()
2328                || tombstone.mutation_intent().session_generation() > self.inner.session_generation
2329            {
2330                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2331                    reason: "restored namespace tombstone escaped the active scope or generation",
2332                });
2333            }
2334            let name_key = (
2335                tombstone.parent_key().item_id().clone(),
2336                tombstone.name().to_owned(),
2337            );
2338            let mut state = lock(&self.inner.state);
2339            let has_current_item = state.namespace_by_key.contains_key(tombstone.key());
2340            if matches!(
2341                tombstone.mutation_intent().desired(),
2342                DesiredMutation::Delete { key } if key == tombstone.key()
2343            ) || !has_current_item
2344                && tombstone.mutation_intent().desired().existing_item_key()
2345                    != Some(tombstone.key())
2346            {
2347                state.deleted_keys.insert(tombstone.key().clone());
2348            }
2349            if state
2350                .tombstones_by_name
2351                .insert(name_key, tombstone)
2352                .is_some()
2353            {
2354                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2355                    reason: "restored namespace overlay contained duplicate tombstones",
2356                });
2357            }
2358        }
2359        Ok(())
2360    }
2361
2362    fn item_key(&self, inode: LinuxInode) -> Result<CloudItemKey> {
2363        let state = lock(&self.inner.state);
2364        if let Some(created) = state.created_by_inode.get(&inode) {
2365            return Ok(created.item().key().clone());
2366        }
2367        if let Some(item) = state.namespace_by_inode.get(&inode) {
2368            return Ok(item.item().key().clone());
2369        }
2370        if let Some(item) = state.remote_by_inode.get(&inode) {
2371            return Ok(item.item().key().clone());
2372        }
2373        self.inner
2374            .readonly
2375            .inode_table()
2376            .by_inode(inode)
2377            .map(|record| record.key().clone())
2378            .ok_or(LinuxCloudFilesError::UnknownInode { inode: inode.get() })
2379    }
2380
2381    fn restore_dirty_snapshots(&self, snapshots: Vec<LocalContentSnapshot>) -> Result<()> {
2382        let mut state = lock(&self.inner.state);
2383        for snapshot in snapshots {
2384            if snapshot.item_key().scope() != self.inner.readonly.inode_table().scope() {
2385                return Err(LinuxCloudFilesError::ScopeMismatch);
2386            }
2387            let inode = match self
2388                .inner
2389                .readonly
2390                .inode_table()
2391                .by_key(snapshot.item_key())
2392            {
2393                Some(record) => record.inode(),
2394                None => state
2395                    .created_by_key
2396                    .get(snapshot.item_key())
2397                    .or_else(|| state.namespace_by_key.get(snapshot.item_key()))
2398                    .or_else(|| state.remote_by_key.get(snapshot.item_key()))
2399                    .copied()
2400                    .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
2401                        reason:
2402                            "writeback recovery returned an item without a restored inode record",
2403                    })?,
2404            };
2405            if inode == crate::LINUX_ROOT_INODE {
2406                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2407                    reason: "writeback recovery returned a dirty snapshot for the mount root",
2408                });
2409            }
2410            if state
2411                .dirty_snapshots
2412                .insert(inode, snapshot.clone())
2413                .is_some_and(|previous| previous != snapshot)
2414            {
2415                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2416                    reason: "writeback recovery returned conflicting snapshots for one inode",
2417                });
2418            }
2419        }
2420        Ok(())
2421    }
2422
2423    fn validate_opened_session(
2424        &self,
2425        inode: LinuxInode,
2426        key: &CloudItemKey,
2427        session: &LinuxWriteSession,
2428        recovered: bool,
2429    ) -> Result<()> {
2430        if session.key() != key {
2431            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2432                reason: "writeback store opened a different stable item identity",
2433            });
2434        }
2435        if session.session_generation() != self.inner.session_generation {
2436            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2437                reason: "writeback store opened a session for a different mount generation",
2438            });
2439        }
2440        let Some(snapshot) = session.snapshot() else {
2441            if recovered {
2442                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2443                    reason: "recovered writeback session omitted its immutable dirty snapshot",
2444                });
2445            }
2446            return Ok(());
2447        };
2448        if snapshot.item_key() != key || snapshot.size() != session.size() {
2449            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2450                reason: "writeback session snapshot did not match its item or logical size",
2451            });
2452        }
2453        let mut state = lock(&self.inner.state);
2454        match state.dirty_snapshots.get(&inode) {
2455            Some(current) if current.generation() > snapshot.generation() => {
2456                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2457                    reason: "recovered writeback session regressed the active dirty generation",
2458                });
2459            }
2460            Some(current)
2461                if current.generation() == snapshot.generation() && current != snapshot =>
2462            {
2463                return Err(LinuxCloudFilesError::InvalidBackendResponse {
2464                    reason: "one dirty generation identified different immutable snapshots",
2465                });
2466            }
2467            _ => {
2468                state.dirty_snapshots.insert(inode, snapshot.clone());
2469            }
2470        }
2471        Ok(())
2472    }
2473
2474    fn apply_commit(
2475        &self,
2476        handle: LinuxFileHandle,
2477        inode: LinuxInode,
2478        commit: LinuxWriteCommit,
2479        minimum_size: Option<u64>,
2480        allow_same_generation: bool,
2481    ) -> Result<()> {
2482        let snapshot = commit.into_snapshot();
2483        let expected_key = self.item_key(inode)?;
2484        if snapshot.item_key() != &expected_key {
2485            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2486                reason: "writeback commit returned a different stable item identity",
2487            });
2488        }
2489        if minimum_size.is_some_and(|minimum| snapshot.size() < minimum) {
2490            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2491                reason: "writeback commit size omitted accepted bytes",
2492            });
2493        }
2494        let mut state = lock(&self.inner.state);
2495        let (opened_inode, previous_generation) = match state.files.get(&handle) {
2496            Some(OpenFile::Writeback {
2497                inode, generation, ..
2498            }) => (*inode, *generation),
2499            None => return Err(LinuxCloudFilesError::StaleHandle),
2500        };
2501        if opened_inode != inode {
2502            return Err(LinuxCloudFilesError::StaleHandle);
2503        }
2504        if previous_generation.is_some_and(|previous| {
2505            snapshot.generation() < previous
2506                || (!allow_same_generation && snapshot.generation() == previous)
2507        }) {
2508            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2509                reason: "writeback commit did not advance the immutable local generation",
2510            });
2511        }
2512        let current = state.dirty_snapshots.get(&inode).cloned();
2513        if allow_same_generation
2514            && current.as_ref().is_some_and(|current| {
2515                snapshot.generation() == current.generation() && current != &snapshot
2516            })
2517        {
2518            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2519                reason: "one dirty generation identified different immutable snapshots",
2520            });
2521        }
2522        if current.as_ref().is_some_and(|current| {
2523            snapshot.generation() < current.generation()
2524                || (!allow_same_generation && snapshot.generation() == current.generation())
2525        }) {
2526            if allow_same_generation
2527                && current
2528                    .as_ref()
2529                    .is_some_and(|current| snapshot.generation() < current.generation())
2530            {
2531                return Ok(());
2532            }
2533            return Err(LinuxCloudFilesError::InvalidBackendResponse {
2534                reason: "writeback commit did not advance the active dirty generation",
2535            });
2536        }
2537        let Some(OpenFile::Writeback {
2538            size, generation, ..
2539        }) = state.files.get_mut(&handle)
2540        else {
2541            return Err(LinuxCloudFilesError::StaleHandle);
2542        };
2543        *size = snapshot.size();
2544        *generation = Some(snapshot.generation());
2545        if current
2546            .as_ref()
2547            .is_none_or(|current| snapshot.generation() >= current.generation())
2548        {
2549            state.dirty_snapshots.insert(inode, snapshot);
2550        }
2551        Ok(())
2552    }
2553}