1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LinuxFileAccess {
59 Read,
61 Write,
63 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#[derive(Clone, PartialEq, Eq, Hash)]
79pub struct LinuxWriteSessionId(String);
80
81impl LinuxWriteSessionId {
82 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 #[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#[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 #[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 #[must_use]
141 pub const fn key(&self) -> &CloudItemKey {
142 &self.key
143 }
144
145 #[must_use]
147 pub const fn base_revision(&self) -> &ContentRevision {
148 &self.base_revision
149 }
150
151 #[must_use]
153 pub const fn base_size(&self) -> u64 {
154 self.base_size
155 }
156
157 #[must_use]
159 pub const fn session_generation(&self) -> SessionGeneration {
160 self.session_generation
161 }
162}
163
164#[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 #[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 #[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 #[must_use]
210 pub const fn id(&self) -> &LinuxWriteSessionId {
211 &self.id
212 }
213
214 #[must_use]
216 pub const fn key(&self) -> &CloudItemKey {
217 &self.key
218 }
219
220 #[must_use]
222 pub const fn size(&self) -> u64 {
223 self.size
224 }
225
226 #[must_use]
228 pub const fn session_generation(&self) -> SessionGeneration {
229 self.session_generation
230 }
231
232 #[must_use]
234 pub const fn snapshot(&self) -> Option<&LocalContentSnapshot> {
235 self.snapshot.as_ref()
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct LinuxWriteCommit {
242 snapshot: LocalContentSnapshot,
243}
244
245impl LinuxWriteCommit {
246 #[must_use]
248 pub const fn new(snapshot: LocalContentSnapshot) -> Self {
249 Self { snapshot }
250 }
251
252 #[must_use]
254 pub const fn snapshot(&self) -> &LocalContentSnapshot {
255 &self.snapshot
256 }
257
258 #[must_use]
260 pub fn into_snapshot(self) -> LocalContentSnapshot {
261 self.snapshot
262 }
263}
264
265#[async_trait]
274pub trait LinuxWritebackStore: Send + Sync {
275 async fn activate_mount(
281 &self,
282 scope: &CloudScope,
283 session_generation: SessionGeneration,
284 ) -> StoreResult<Vec<LocalContentSnapshot>>;
285
286 async fn open_recovered(
292 &self,
293 key: &CloudItemKey,
294 session_generation: SessionGeneration,
295 ) -> StoreResult<Option<LinuxWriteSession>>;
296
297 async fn open_existing(
299 &self,
300 request: &LinuxWriteOpenRequest,
301 base_content: Bytes,
302 ) -> StoreResult<LinuxWriteSession>;
303
304 async fn read(
306 &self,
307 session: &LinuxWriteSessionId,
308 offset: u64,
309 size: u32,
310 ) -> StoreResult<Bytes>;
311
312 async fn write(
314 &self,
315 session: &LinuxWriteSessionId,
316 offset: u64,
317 bytes: Bytes,
318 ) -> StoreResult<LinuxWriteCommit>;
319
320 async fn truncate(
322 &self,
323 session: &LinuxWriteSessionId,
324 size: u64,
325 ) -> StoreResult<LinuxWriteCommit>;
326
327 async fn sync(
329 &self,
330 session: &LinuxWriteSessionId,
331 data_only: bool,
332 ) -> StoreResult<Option<LinuxWriteCommit>>;
333
334 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#[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
456pub 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 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 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 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 #[must_use]
580 pub fn readonly(&self) -> &LinuxReadOnlyEngine<B> {
581 &self.inner.readonly
582 }
583
584 #[must_use]
586 pub fn session_generation(&self) -> SessionGeneration {
587 self.inner.session_generation
588 }
589
590 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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}