libfuse-fs 0.1.13

FUSE Filesystem Library
Documentation
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.

#![allow(clippy::useless_conversion)]
use std::collections::BTreeMap;
use std::sync::Arc;

#[cfg(target_os = "linux")]
use super::InodeHandle;
#[cfg(target_os = "linux")]
use super::file_handle::FileHandle;
use super::statx::StatExt;
use super::{Inode, InodeData};

#[derive(Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)]
/// Identify an inode in `PassthroughFs` by `InodeId`.
pub struct InodeId {
    #[cfg(target_os = "linux")]
    pub ino: libc::ino64_t,
    #[cfg(target_os = "macos")]
    pub ino: libc::ino_t,
    pub dev: libc::dev_t,
    pub mnt: u64,
}

impl InodeId {
    #[inline]
    pub(super) fn from_stat(st: &StatExt) -> Self {
        InodeId {
            ino: st.st.st_ino,
            dev: st.st.st_dev,
            mnt: st.mnt_id,
        }
    }
}

/// A store for inodes, which allows to map inodes to their data and vice versa.
///
/// It maintains a mapping of inodes to their data, as well as a mapping of inode IDs and file handles to inodes.
/// The store is designed to ensure that the same inodes are used for the same files, even if they are opened multiple times.
/// The store is not thread-safe, so it should be used in a single-threaded context or protected by a mutex.
/// The store uses `BTreeMap` to maintain the mappings, which allows for efficient lookups and insertions.
#[derive(Default)]
pub struct InodeStore {
    data: BTreeMap<Inode, Arc<InodeData>>, // Maps inodes to their data.
    by_id: BTreeMap<InodeId, Inode>,       // Maps real inode IDs to inodes.
    /// Linux-only secondary index keyed by `name_to_handle_at` output.
    /// Used to dedup inodes across mounts. macOS lacks the syscall.
    #[cfg(target_os = "linux")]
    by_handle: BTreeMap<Arc<FileHandle>, Inode>,
}

impl InodeStore {
    /// Insert an inode into the manager
    ///
    /// The caller needs to ensure that no inode with the same key exists, otherwise the old inode
    /// will get lost.
    pub fn insert(&mut self, data: Arc<InodeData>) {
        self.by_id.insert(data.id, data.inode);
        #[cfg(target_os = "linux")]
        if let InodeHandle::Handle(handle) = &data.handle {
            self.by_handle
                .insert(handle.file_handle().clone(), data.inode);
        }
        self.data.insert(data.inode, data);
        // trace!("inserting data for inodedata {:?}", data);
        // if let Some(old_inode) = self.by_id.insert(data.id, data.inode) {
        //     warn!(
        //         "overwriting `by_id` entry for {:?}. Old inode: {}, new inode: {}",
        //         data.id,
        //         old_inode,
        //         data.inode
        //     );
        // }

        // if let InodeHandle::Handle(handle) = &data.handle {
        //     if let Some(old_inode) = self
        //         .by_handle
        //         .insert(handle.file_handle().clone(), data.inode)
        //     {
        //         warn!(
        //             "overwriting `by_handle` entry. Old inode: {}, new inode: {}",
        //             old_inode,
        //             data.inode
        //         );
        //     }
        // }

        // if let Some(old_data) = self.data.insert(data.inode, data.clone()) {
        //     warn!(
        //         "overwriting `data` entry for inode {}. Old id: {:?}, new id: {:?}",
        //         data.id,
        //         data.id
        //     );
        // }
        // assert!(
        //     self.data.contains_key(&data.inode),
        //     "Data for inode {} not found after insertion",
        //     data.inode
        // );
    }

    /// Remove an inode from the manager, keeping the (key, ino) mapping if `remove_data_only` is true.
    #[allow(unused)]
    pub fn remove(&mut self, inode: &Inode, remove_data_only: bool) -> Option<Arc<InodeData>> {
        // trace!("removing inode: {}, remove_data_only: {}", inode, remove_data_only);
        let data = self.data.remove(inode);
        if remove_data_only {
            // Don't remove by_id and by_handle, we need use it to store inode
            // record the mapping of inodes using these two structures to ensure
            // that the same files always use the same inode
            return data;
        }

        if let Some(data) = data.as_ref() {
            #[cfg(target_os = "linux")]
            if let InodeHandle::Handle(handle) = &data.handle {
                self.by_handle.remove(handle.file_handle());
            }
            self.by_id.remove(&data.id);
        }
        data
    }

    pub fn clear(&mut self) {
        self.data.clear();
        #[cfg(target_os = "linux")]
        self.by_handle.clear();
        self.by_id.clear();
    }

    pub fn get(&self, inode: &Inode) -> Option<&Arc<InodeData>> {
        // let res = self.data.get(inode);
        // if res.is_none() {
        //     trace!("get: inode {} not found", inode);
        // }
        // res
        self.data.get(inode)
    }

    pub fn get_by_id(&self, id: &InodeId) -> Option<&Arc<InodeData>> {
        let inode = self.inode_by_id(id)?;
        self.get(inode)
    }

    #[cfg(target_os = "linux")]
    pub fn get_by_handle(&self, handle: &FileHandle) -> Option<&Arc<InodeData>> {
        let inode = self.inode_by_handle(handle)?;
        self.get(inode)
    }

    pub fn inode_by_id(&self, id: &InodeId) -> Option<&Inode> {
        self.by_id.get(id)
    }

    #[cfg(target_os = "linux")]
    pub fn inode_by_handle(&self, handle: &FileHandle) -> Option<&Inode> {
        self.by_handle.get(handle)
    }

    /// Borrow-iterate over all (inode, data) pairs currently in the store.
    /// Caller holds the surrounding map read or write lock; iteration order
    /// follows BTreeMap ordering (by inode number).
    #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
    pub fn iter(&self) -> impl Iterator<Item = (&Inode, &Arc<InodeData>)> {
        self.data.iter()
    }
}

#[cfg(test)]
mod test {
    #[cfg(target_os = "linux")]
    use super::super::*;
    #[cfg(target_os = "linux")]
    use super::*;

    #[cfg(target_os = "linux")]
    use vmm_sys_util::tempfile::TempFile;

    #[test]
    #[cfg(target_os = "linux")]
    fn test_inode_store() {
        let mut m = InodeStore::default();
        let tmpfile1 = TempFile::new().unwrap();
        let tmpfile2 = TempFile::new().unwrap();

        let inode1: Inode = 3;
        let inode2: Inode = 4;
        let inode_stat1 = statx::statx(tmpfile1.as_file(), None).unwrap();
        let inode_stat2 = statx::statx(tmpfile2.as_file(), None).unwrap();
        let id1 = InodeId::from_stat(&inode_stat1);
        let id2 = InodeId::from_stat(&inode_stat2);
        let file_or_handle1 = InodeHandle::File(tmpfile1.into_file());
        let file_or_handle2 = InodeHandle::File(tmpfile2.into_file());
        let data1 = InodeData::new(
            inode1,
            file_or_handle1,
            2,
            id1,
            inode_stat1.st.st_mode.into(),
            inode_stat1.btime.unwrap(),
        );
        let data2 = InodeData::new(
            inode2,
            file_or_handle2,
            2,
            id2,
            inode_stat2.st.st_mode.into(),
            inode_stat2.btime.unwrap(),
        );
        let data1 = Arc::new(data1);
        let data2 = Arc::new(data2);

        m.insert(data1.clone());

        // get not present key, expect none
        assert!(m.get(&1001).is_none());

        // get just inserted value by key, by id, by handle
        assert!(m.get_by_id(&InodeId::default()).is_none());
        assert!(m.get_by_handle(&FileHandle::default()).is_none());

        // insert another value, and check again
        m.insert(data2.clone());
        assert!(m.get(&1).is_none());
        assert!(m.get_by_id(&InodeId::default()).is_none());
        assert!(m.get_by_handle(&FileHandle::default()).is_none());

        // remove non-present key
        assert!(m.remove(&1, false).is_none());

        // clear the map
        m.clear();
        assert!(m.get(&1).is_none());
        assert!(m.get_by_id(&InodeId::default()).is_none());
        assert!(m.get_by_handle(&FileHandle::default()).is_none());
        assert!(m.get(&inode1).is_none());
        assert!(m.get_by_id(&id1).is_none());
        assert!(m.get(&inode2).is_none());
        assert!(m.get_by_id(&id2).is_none());
    }
}