aster_forge_cloud_files_linux/
engine.rs

1//! Runtime-neutral read-only Linux engine used by the native FUSE adapter.
2
3use std::{
4    collections::{HashMap, HashSet},
5    sync::{Arc, Mutex, MutexGuard},
6    time::{Duration, SystemTime},
7};
8
9use aster_forge_cloud_files_core::{
10    ByteRange, CloudFilesBackend, CloudItem, CloudItemKey, CloudItemKind, ContentReadRequest,
11    ContentRevision, PageCursor, SessionGeneration,
12};
13use bytes::Bytes;
14
15use crate::{
16    LINUX_ROOT_INODE, LinuxCloudFilesError, LinuxInode, LinuxInodeGeneration, LinuxInodeRecord,
17    LinuxInodeTable, Result,
18};
19
20const FUSE_BLOCK_SIZE: u64 = 512;
21
22fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
23    match mutex.lock() {
24        Ok(guard) => guard,
25        Err(poisoned) => poisoned.into_inner(),
26    }
27}
28
29/// Validates a string-backed cloud item name for one Linux directory component.
30/// # Errors
31///
32/// Returns an error when validation fails or an underlying backend, store, or platform
33/// operation fails.
34pub fn validate_linux_name(name: &str) -> Result<()> {
35    if name.is_empty() {
36        return Err(LinuxCloudFilesError::InvalidName {
37            reason: "name must not be empty",
38        });
39    }
40    if name == "." || name == ".." {
41        return Err(LinuxCloudFilesError::InvalidName {
42            reason: "dot components are reserved",
43        });
44    }
45    if name.contains('/') {
46        return Err(LinuxCloudFilesError::InvalidName {
47            reason: "name must not contain a path separator",
48        });
49    }
50    if name.contains('\0') {
51        return Err(LinuxCloudFilesError::InvalidName {
52            reason: "name must not contain NUL",
53        });
54    }
55    Ok(())
56}
57
58/// Portable Linux node kind mapped to `fuser::FileType` at the native boundary.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum LinuxNodeKind {
61    /// Regular file.
62    File,
63    /// Directory.
64    Directory,
65}
66
67/// Attribute defaults for metadata not present in the product-neutral core item model.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct LinuxAttributePolicy {
70    uid: u32,
71    gid: u32,
72    file_permissions: u16,
73    directory_permissions: u16,
74    block_size: u32,
75    cache_ttl: Duration,
76    fallback_time: SystemTime,
77}
78
79impl LinuxAttributePolicy {
80    /// Creates an explicit Linux attribute policy.
81    /// # Errors
82    ///
83    /// Returns an error when validation fails or an underlying backend, store, or platform
84    /// operation fails.
85    pub fn new(
86        uid: u32,
87        gid: u32,
88        file_permissions: u16,
89        directory_permissions: u16,
90        cache_ttl: Duration,
91    ) -> Result<Self> {
92        if file_permissions & !0o7777 != 0 || directory_permissions & !0o7777 != 0 {
93            return Err(LinuxCloudFilesError::InvalidConfiguration {
94                reason: "permissions must contain only Unix permission and special-mode bits",
95            });
96        }
97        Ok(Self {
98            uid,
99            gid,
100            file_permissions,
101            directory_permissions,
102            block_size: 4096,
103            cache_ttl,
104            fallback_time: SystemTime::UNIX_EPOCH,
105        })
106    }
107
108    /// Returns the mount-owner user ID.
109    #[must_use]
110    pub const fn uid(&self) -> u32 {
111        self.uid
112    }
113
114    /// Returns the mount-owner group ID.
115    #[must_use]
116    pub const fn gid(&self) -> u32 {
117        self.gid
118    }
119
120    /// Returns regular-file permissions.
121    #[must_use]
122    pub const fn file_permissions(&self) -> u16 {
123        self.file_permissions
124    }
125
126    /// Returns directory permissions.
127    #[must_use]
128    pub const fn directory_permissions(&self) -> u16 {
129        self.directory_permissions
130    }
131
132    /// Returns the reported preferred I/O block size.
133    #[must_use]
134    pub const fn block_size(&self) -> u32 {
135        self.block_size
136    }
137
138    /// Returns the kernel entry and attribute cache TTL.
139    #[must_use]
140    pub const fn cache_ttl(&self) -> Duration {
141        self.cache_ttl
142    }
143
144    /// Returns the timestamp used until a platform adapter supplies native times.
145    #[must_use]
146    pub const fn fallback_time(&self) -> SystemTime {
147        self.fallback_time
148    }
149}
150
151impl Default for LinuxAttributePolicy {
152    fn default() -> Self {
153        Self {
154            uid: 0,
155            gid: 0,
156            file_permissions: 0o444,
157            directory_permissions: 0o555,
158            block_size: 4096,
159            cache_ttl: Duration::from_secs(1),
160            fallback_time: SystemTime::UNIX_EPOCH,
161        }
162    }
163}
164
165/// Complete portable attributes for one restored inode.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct LinuxFileAttributes {
168    inode: LinuxInode,
169    size: u64,
170    blocks: u64,
171    kind: LinuxNodeKind,
172    permissions: u16,
173    links: u32,
174    uid: u32,
175    gid: u32,
176    block_size: u32,
177    time: SystemTime,
178}
179
180impl LinuxFileAttributes {
181    /// Returns the restored inode.
182    #[must_use]
183    pub const fn inode(&self) -> LinuxInode {
184        self.inode
185    }
186
187    /// Returns the logical content size.
188    #[must_use]
189    pub const fn size(&self) -> u64 {
190        self.size
191    }
192
193    /// Returns allocated 512-byte block count reported to the kernel.
194    #[must_use]
195    pub const fn blocks(&self) -> u64 {
196        self.blocks
197    }
198
199    /// Returns the portable node kind.
200    #[must_use]
201    pub const fn kind(&self) -> LinuxNodeKind {
202        self.kind
203    }
204
205    /// Returns Unix permission bits.
206    #[must_use]
207    pub const fn permissions(&self) -> u16 {
208        self.permissions
209    }
210
211    /// Returns the reported hard-link count.
212    #[must_use]
213    pub const fn links(&self) -> u32 {
214        self.links
215    }
216
217    /// Returns the owner user ID.
218    #[must_use]
219    pub const fn uid(&self) -> u32 {
220        self.uid
221    }
222
223    /// Returns the owner group ID.
224    #[must_use]
225    pub const fn gid(&self) -> u32 {
226        self.gid
227    }
228
229    /// Returns the preferred I/O block size.
230    #[must_use]
231    pub const fn block_size(&self) -> u32 {
232        self.block_size
233    }
234
235    /// Returns the fallback timestamp used for all native time fields.
236    #[must_use]
237    pub const fn time(&self) -> SystemTime {
238        self.time
239    }
240}
241
242/// Metadata reply for one inode lookup or getattr request.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct LinuxNode {
245    key: CloudItemKey,
246    generation: LinuxInodeGeneration,
247    attributes: LinuxFileAttributes,
248}
249
250impl LinuxNode {
251    /// Returns the stable scoped cloud identity.
252    #[must_use]
253    pub const fn key(&self) -> &CloudItemKey {
254        &self.key
255    }
256
257    /// Returns the inode generation fence.
258    #[must_use]
259    pub const fn generation(&self) -> LinuxInodeGeneration {
260        self.generation
261    }
262
263    /// Returns portable Linux attributes.
264    #[must_use]
265    pub const fn attributes(&self) -> &LinuxFileAttributes {
266        &self.attributes
267    }
268
269    pub(crate) fn with_size(mut self, size: u64) -> Self {
270        self.attributes.size = size;
271        self.attributes.blocks = size.div_ceil(FUSE_BLOCK_SIZE);
272        self
273    }
274}
275
276/// Non-zero open-file handle scoped to one mount process.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278pub struct LinuxFileHandle(u64);
279
280impl LinuxFileHandle {
281    /// Restores a native file handle supplied by FUSE.
282    /// # Errors
283    ///
284    /// Returns an error when validation fails or an underlying backend, store, or platform
285    /// operation fails.
286    pub const fn new(value: u64) -> Result<Self> {
287        if value == 0 {
288            return Err(LinuxCloudFilesError::StaleHandle);
289        }
290        Ok(Self(value))
291    }
292
293    /// Returns the native handle value.
294    #[must_use]
295    pub const fn get(self) -> u64 {
296        self.0
297    }
298}
299
300/// Non-zero open-directory handle scoped to one mount process.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302pub struct LinuxDirectoryHandle(u64);
303
304impl LinuxDirectoryHandle {
305    /// Restores a native directory handle supplied by FUSE.
306    /// # Errors
307    ///
308    /// Returns an error when validation fails or an underlying backend, store, or platform
309    /// operation fails.
310    pub const fn new(value: u64) -> Result<Self> {
311        if value == 0 {
312            return Err(LinuxCloudFilesError::StaleHandle);
313        }
314        Ok(Self(value))
315    }
316
317    /// Returns the native handle value.
318    #[must_use]
319    pub const fn get(self) -> u64 {
320        self.0
321    }
322}
323
324/// One child captured in an open-directory snapshot.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct LinuxDirectoryEntry {
327    inode: LinuxInode,
328    generation: LinuxInodeGeneration,
329    name: String,
330    kind: LinuxNodeKind,
331}
332
333impl LinuxDirectoryEntry {
334    /// Returns the restored child inode.
335    #[must_use]
336    pub const fn inode(&self) -> LinuxInode {
337        self.inode
338    }
339
340    /// Returns the child generation fence.
341    #[must_use]
342    pub const fn generation(&self) -> LinuxInodeGeneration {
343        self.generation
344    }
345
346    /// Returns the exact UTF-8 backend name.
347    #[must_use]
348    pub fn name(&self) -> &str {
349        &self.name
350    }
351
352    /// Returns the portable child kind.
353    #[must_use]
354    pub const fn kind(&self) -> LinuxNodeKind {
355        self.kind
356    }
357
358    pub(crate) fn from_node(name: String, node: &LinuxNode) -> Self {
359        Self {
360            inode: node.attributes.inode,
361            generation: node.generation,
362            name,
363            kind: node.attributes.kind,
364        }
365    }
366}
367
368/// Immutable directory stream captured at `opendir` time.
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct LinuxDirectorySnapshot {
371    directory: LinuxInode,
372    parent: LinuxInode,
373    entries: Arc<[LinuxDirectoryEntry]>,
374}
375
376impl LinuxDirectorySnapshot {
377    /// Returns the opened directory inode.
378    #[must_use]
379    pub const fn directory(&self) -> LinuxInode {
380        self.directory
381    }
382
383    /// Returns the parent inode used by the `..` entry.
384    #[must_use]
385    pub const fn parent(&self) -> LinuxInode {
386        self.parent
387    }
388
389    /// Returns children in the backend enumeration order captured by this handle.
390    #[must_use]
391    pub fn entries(&self) -> &[LinuxDirectoryEntry] {
392        &self.entries
393    }
394
395    pub(crate) fn with_entries(
396        directory: LinuxInode,
397        parent: LinuxInode,
398        entries: Vec<LinuxDirectoryEntry>,
399    ) -> Self {
400        Self {
401            directory,
402            parent,
403            entries: entries.into(),
404        }
405    }
406}
407
408#[derive(Debug, Clone)]
409struct OpenFile {
410    inode: LinuxInode,
411    key: CloudItemKey,
412    revision: ContentRevision,
413    size: u64,
414}
415
416#[derive(Default)]
417struct HandleState {
418    next: u64,
419    files: HashMap<LinuxFileHandle, OpenFile>,
420    directories: HashMap<LinuxDirectoryHandle, LinuxDirectorySnapshot>,
421}
422
423impl HandleState {
424    fn allocate(&mut self) -> Result<u64> {
425        let current = if self.next == 0 { 1 } else { self.next };
426        let Some(next) = current.checked_add(1) else {
427            return Err(LinuxCloudFilesError::HandleExhausted);
428        };
429        self.next = next;
430        Ok(current)
431    }
432}
433
434struct EngineInner<B> {
435    backend: Arc<B>,
436    inodes: Arc<LinuxInodeTable>,
437    attributes: LinuxAttributePolicy,
438    handles: Mutex<HandleState>,
439}
440
441/// Product-neutral read-only engine shared by native FUSE callbacks and deterministic tests.
442pub struct LinuxReadOnlyEngine<B> {
443    inner: Arc<EngineInner<B>>,
444}
445
446impl<B> Clone for LinuxReadOnlyEngine<B> {
447    fn clone(&self) -> Self {
448        Self {
449            inner: self.inner.clone(),
450        }
451    }
452}
453
454impl<B> LinuxReadOnlyEngine<B>
455where
456    B: CloudFilesBackend + 'static,
457{
458    /// Creates a read-only engine from a backend, restored inode table, and attribute policy.
459    pub fn new(
460        backend: Arc<B>,
461        inodes: Arc<LinuxInodeTable>,
462        attributes: LinuxAttributePolicy,
463    ) -> Self {
464        Self {
465            inner: Arc::new(EngineInner {
466                backend,
467                inodes,
468                attributes,
469                handles: Mutex::new(HandleState::default()),
470            }),
471        }
472    }
473
474    /// Returns the active immutable inode table.
475    #[must_use]
476    pub fn inode_table(&self) -> &LinuxInodeTable {
477        &self.inner.inodes
478    }
479
480    /// Returns the active native attribute policy.
481    #[must_use]
482    pub fn attribute_policy(&self) -> &LinuxAttributePolicy {
483        &self.inner.attributes
484    }
485
486    /// Resolves one name by enumerating all backend pages for the parent snapshot.
487    /// # Errors
488    ///
489    /// Returns an error when validation fails or an underlying backend, store, or platform
490    /// operation fails.
491    pub async fn lookup(&self, parent: LinuxInode, name: &str) -> Result<LinuxNode> {
492        validate_linux_name(name)?;
493        let parent_item = self.load_item_for_overlay(parent).await?;
494        if parent_item.kind() != CloudItemKind::Directory {
495            return Err(LinuxCloudFilesError::NotDirectory);
496        }
497        let children = self.load_children(parent_item.key()).await?;
498        let Some(item) = children.into_iter().find(|item| item.name() == name) else {
499            return Err(LinuxCloudFilesError::Backend(
500                aster_forge_cloud_files_core::CloudBackendError::new(
501                    aster_forge_cloud_files_core::CloudBackendErrorKind::NotFound,
502                ),
503            ));
504        };
505        self.node_from_item(&item)
506    }
507
508    /// Loads attributes for one restored inode.
509    /// # Errors
510    ///
511    /// Returns an error when validation fails or an underlying backend, store, or platform
512    /// operation fails.
513    pub async fn getattr(&self, inode: LinuxInode) -> Result<LinuxNode> {
514        let item = self.load_item_for_overlay(inode).await?;
515        self.node_from_item(&item)
516    }
517
518    /// Opens a regular file and captures its exact content revision for subsequent range reads.
519    /// # Errors
520    ///
521    /// Returns an error when validation fails or an underlying backend, store, or platform
522    /// operation fails.
523    pub async fn open_file(&self, inode: LinuxInode) -> Result<LinuxFileHandle> {
524        let item = self.load_item_for_overlay(inode).await?;
525        if item.kind() != CloudItemKind::File {
526            return Err(LinuxCloudFilesError::NotFile);
527        }
528        let Some(content) = item.content() else {
529            return Err(LinuxCloudFilesError::InvalidBackendResponse {
530                reason: "regular file omitted content metadata",
531            });
532        };
533        let mut handles = lock(&self.inner.handles);
534        let handle = LinuxFileHandle(handles.allocate()?);
535        handles.files.insert(
536            handle,
537            OpenFile {
538                inode,
539                key: item.key().clone(),
540                revision: content.revision().clone(),
541                size: content.size(),
542            },
543        );
544        Ok(handle)
545    }
546
547    pub(crate) async fn hydrate_for_write(
548        &self,
549        inode: LinuxInode,
550        session_generation: SessionGeneration,
551    ) -> Result<(crate::LinuxWriteOpenRequest, Bytes)> {
552        let item = self.load_item_for_overlay(inode).await?;
553        self.hydrate_item_for_write(&item, session_generation).await
554    }
555
556    pub(crate) async fn hydrate_item_for_write(
557        &self,
558        item: &CloudItem,
559        session_generation: SessionGeneration,
560    ) -> Result<(crate::LinuxWriteOpenRequest, Bytes)> {
561        if item.kind() != CloudItemKind::File {
562            return Err(LinuxCloudFilesError::NotFile);
563        }
564        let content = item
565            .content()
566            .ok_or(LinuxCloudFilesError::InvalidBackendResponse {
567                reason: "regular file omitted content metadata",
568            })?;
569        let request = ContentReadRequest::whole(
570            item.key().clone(),
571            content.revision().clone(),
572            content.size(),
573        );
574        let response = self.inner.backend.read_content(&request).await?;
575        request.validate_response(&response).map_err(|_| {
576            LinuxCloudFilesError::InvalidBackendResponse {
577                reason: "write hydration violated the requested revision or complete-file extent",
578            }
579        })?;
580        let (_, _, bytes, _) = response.into_parts();
581        Ok((
582            crate::LinuxWriteOpenRequest::new(
583                item.key().clone(),
584                content.revision().clone(),
585                content.size(),
586                session_generation,
587            ),
588            bytes,
589        ))
590    }
591
592    /// Reads an exact range using the revision captured by `open_file`.
593    /// # Errors
594    ///
595    /// Returns an error when validation fails or an underlying backend, store, or platform
596    /// operation fails.
597    pub async fn read_file(
598        &self,
599        inode: LinuxInode,
600        handle: LinuxFileHandle,
601        offset: u64,
602        size: u32,
603    ) -> Result<Bytes> {
604        let opened = {
605            let handles = lock(&self.inner.handles);
606            let Some(opened) = handles.files.get(&handle) else {
607                return Err(LinuxCloudFilesError::StaleHandle);
608            };
609            if opened.inode != inode {
610                return Err(LinuxCloudFilesError::StaleHandle);
611            }
612            opened.clone()
613        };
614        if size == 0 || offset >= opened.size {
615            return Ok(Bytes::new());
616        }
617        let length = u64::from(size).min(opened.size - offset);
618        let range = ByteRange::new(offset, length).map_err(|_| {
619            LinuxCloudFilesError::InvalidBackendResponse {
620                reason: "validated FUSE range could not be represented by the core model",
621            }
622        })?;
623        let request = ContentReadRequest::range(opened.key, opened.revision, opened.size, range);
624        let response = self.inner.backend.read_content(&request).await?;
625        request.validate_response(&response).map_err(|_| {
626            LinuxCloudFilesError::InvalidBackendResponse {
627                reason: "content response violated the requested revision or range",
628            }
629        })?;
630        Ok(response.into_parts().2)
631    }
632
633    /// Releases one file handle. Releasing the same handle twice reports a stale handle.
634    /// # Errors
635    ///
636    /// Returns an error when validation fails or an underlying backend, store, or platform
637    /// operation fails.
638    pub fn release_file(&self, handle: LinuxFileHandle) -> Result<()> {
639        if lock(&self.inner.handles).files.remove(&handle).is_none() {
640            return Err(LinuxCloudFilesError::StaleHandle);
641        }
642        Ok(())
643    }
644
645    /// Opens a directory and freezes one complete paged snapshot for stable FUSE cookies.
646    /// # Errors
647    ///
648    /// Returns an error when validation fails or an underlying backend, store, or platform
649    /// operation fails.
650    pub async fn open_directory(&self, inode: LinuxInode) -> Result<LinuxDirectoryHandle> {
651        let item = self.load_item_for_overlay(inode).await?;
652        if item.kind() != CloudItemKind::Directory {
653            return Err(LinuxCloudFilesError::NotDirectory);
654        }
655        let children = self.load_children(item.key()).await?;
656        let entries = children
657            .iter()
658            .map(|child| {
659                let node = self.node_from_item(child)?;
660                Ok(LinuxDirectoryEntry {
661                    inode: node.attributes.inode,
662                    generation: node.generation,
663                    name: child.name().to_owned(),
664                    kind: node.attributes.kind,
665                })
666            })
667            .collect::<Result<Vec<_>>>()?;
668        let parent = match item.parent_id() {
669            Some(parent_id) => {
670                let parent_key = CloudItemKey::new(item.key().scope().clone(), parent_id.clone());
671                self.inner
672                    .inodes
673                    .by_key(&parent_key)
674                    .ok_or(LinuxCloudFilesError::MissingInodeRecord)?
675                    .inode()
676            }
677            None => inode,
678        };
679        let snapshot = LinuxDirectorySnapshot {
680            directory: inode,
681            parent,
682            entries: entries.into(),
683        };
684        let mut handles = lock(&self.inner.handles);
685        let handle = LinuxDirectoryHandle(handles.allocate()?);
686        handles.directories.insert(handle, snapshot);
687        Ok(handle)
688    }
689
690    /// Returns the immutable snapshot associated with an open directory handle.
691    /// # Errors
692    ///
693    /// Returns an error when validation fails or an underlying backend, store, or platform
694    /// operation fails.
695    pub fn directory_snapshot(
696        &self,
697        inode: LinuxInode,
698        handle: LinuxDirectoryHandle,
699    ) -> Result<LinuxDirectorySnapshot> {
700        let handles = lock(&self.inner.handles);
701        let Some(snapshot) = handles.directories.get(&handle) else {
702            return Err(LinuxCloudFilesError::StaleHandle);
703        };
704        if snapshot.directory != inode {
705            return Err(LinuxCloudFilesError::StaleHandle);
706        }
707        Ok(snapshot.clone())
708    }
709
710    /// Releases one directory snapshot handle.
711    /// # Errors
712    ///
713    /// Returns an error when validation fails or an underlying backend, store, or platform
714    /// operation fails.
715    pub fn release_directory(&self, handle: LinuxDirectoryHandle) -> Result<()> {
716        if lock(&self.inner.handles)
717            .directories
718            .remove(&handle)
719            .is_none()
720        {
721            return Err(LinuxCloudFilesError::StaleHandle);
722        }
723        Ok(())
724    }
725
726    pub(crate) async fn load_item_for_overlay(&self, inode: LinuxInode) -> Result<CloudItem> {
727        let record = self
728            .inner
729            .inodes
730            .by_inode(inode)
731            .ok_or(LinuxCloudFilesError::UnknownInode { inode: inode.get() })?;
732        let item = self.inner.backend.get_item(record.key()).await?;
733        if item.key() != record.key() {
734            return Err(LinuxCloudFilesError::InvalidBackendResponse {
735                reason: "get_item returned a different scoped stable identity",
736            });
737        }
738        if item.is_root() != (inode == LINUX_ROOT_INODE) {
739            return Err(LinuxCloudFilesError::InvalidBackendResponse {
740                reason: "get_item root shape did not match the restored inode role",
741            });
742        }
743        Ok(item)
744    }
745
746    pub(crate) async fn load_children_for_overlay(
747        &self,
748        parent: &CloudItemKey,
749    ) -> Result<Vec<CloudItem>> {
750        let mut cursor: Option<PageCursor> = None;
751        let mut seen_cursors = HashSet::new();
752        let mut seen_names = HashSet::new();
753        let mut children = Vec::new();
754        loop {
755            let page = self
756                .inner
757                .backend
758                .list_children(parent, cursor.as_ref())
759                .await?;
760            let (items, next_cursor) = page.into_parts();
761            for item in items {
762                if item.key().scope() != parent.scope()
763                    || item.parent_id() != Some(parent.item_id())
764                {
765                    return Err(LinuxCloudFilesError::InvalidBackendResponse {
766                        reason: "directory child escaped the requested parent scope",
767                    });
768                }
769                validate_linux_name(item.name())?;
770                if !seen_names.insert(item.name().to_owned()) {
771                    return Err(LinuxCloudFilesError::InvalidBackendResponse {
772                        reason: "directory enumeration returned duplicate child names",
773                    });
774                }
775                children.push(item);
776            }
777            let Some(next_cursor) = next_cursor else {
778                break;
779            };
780            if !seen_cursors.insert(next_cursor.clone()) {
781                return Err(LinuxCloudFilesError::InvalidBackendResponse {
782                    reason: "directory pagination repeated a continuation cursor",
783                });
784            }
785            cursor = Some(next_cursor);
786        }
787        Ok(children)
788    }
789
790    async fn load_children(&self, parent: &CloudItemKey) -> Result<Vec<CloudItem>> {
791        let children = self.load_children_for_overlay(parent).await?;
792        if children
793            .iter()
794            .any(|item| self.inner.inodes.by_key(item.key()).is_none())
795        {
796            return Err(LinuxCloudFilesError::MissingInodeRecord);
797        }
798        Ok(children)
799    }
800
801    fn node_from_item(&self, item: &CloudItem) -> Result<LinuxNode> {
802        if !item.is_root() {
803            validate_linux_name(item.name())?;
804        }
805        let record = self
806            .inner
807            .inodes
808            .by_key(item.key())
809            .ok_or(LinuxCloudFilesError::MissingInodeRecord)?;
810        self.node_from_item_and_record(item, record)
811    }
812
813    pub(crate) fn node_from_item_and_record(
814        &self,
815        item: &CloudItem,
816        record: &LinuxInodeRecord,
817    ) -> Result<LinuxNode> {
818        if !item.is_root() {
819            validate_linux_name(item.name())?;
820        }
821        if item.key() != record.key() {
822            return Err(LinuxCloudFilesError::InvalidBackendResponse {
823                reason: "linux inode record did not match the item stable identity",
824            });
825        }
826        let (kind, size, permissions, links) = match item.kind() {
827            CloudItemKind::File => {
828                let Some(content) = item.content() else {
829                    return Err(LinuxCloudFilesError::InvalidBackendResponse {
830                        reason: "regular file omitted content metadata",
831                    });
832                };
833                (
834                    LinuxNodeKind::File,
835                    content.size(),
836                    self.inner.attributes.file_permissions,
837                    1,
838                )
839            }
840            CloudItemKind::Directory => (
841                LinuxNodeKind::Directory,
842                0,
843                self.inner.attributes.directory_permissions,
844                2,
845            ),
846        };
847        Ok(LinuxNode {
848            key: item.key().clone(),
849            generation: record.generation(),
850            attributes: LinuxFileAttributes {
851                inode: record.inode(),
852                size,
853                blocks: size.div_ceil(FUSE_BLOCK_SIZE),
854                kind,
855                permissions,
856                links,
857                uid: self.inner.attributes.uid,
858                gid: self.inner.attributes.gid,
859                block_size: self.inner.attributes.block_size,
860                time: self.inner.attributes.fallback_time,
861            },
862        })
863    }
864}