vfsi-core 0.1.13

Portable vectorized filesystem API, shared types, errors, and capabilities for VFSI
Documentation
use super::*;
use std::path::PathBuf;

/// Default payload limit for APIs that allocate and return complete contents.
pub const DEFAULT_READ_MAX_BYTES: usize = 16 * 1024 * 1024;

/// Default size of each chunk delivered by the bounded single-file stream API.
pub const DEFAULT_READ_STREAM_CHUNK_BYTES: usize = 1024 * 1024;

/// Default aggregate payload limit for complete-file vector reads.
pub const DEFAULT_READ_ALLV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;

/// Tuning options for bounded single-file streaming reads.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use = "configuration and requests have no effect until used"]
pub struct StreamOptions {
    chunk_size: std::num::NonZeroUsize,
}

impl StreamOptions {
    pub const fn new() -> Self {
        Self {
            chunk_size: std::num::NonZeroUsize::new(DEFAULT_READ_STREAM_CHUNK_BYTES).unwrap(),
        }
    }

    /// Set the nonzero maximum bytes delivered to the callback at once.
    ///
    /// ```compile_fail
    /// use vfsi_core::api::StreamOptions;
    /// let options = StreamOptions::new().with_chunk_size(0);
    /// ```
    ///
    /// NFS and other backends may return smaller chunks due to negotiated
    /// protocol limits. A larger setting does not bypass those limits.
    pub const fn with_chunk_size(mut self, bytes: std::num::NonZeroUsize) -> Self {
        self.chunk_size = bytes;
        self
    }

    pub const fn chunk_size(self) -> usize {
        self.chunk_size.get()
    }
}

impl Default for StreamOptions {
    fn default() -> Self {
        Self::new()
    }
}

/// Default maximum number of entries returned by allocating directory APIs.
pub const DEFAULT_DIRECTORY_MAX_ENTRIES: usize = 100_000;

/// Default combined path-storage budget for allocating directory APIs.
pub const DEFAULT_DIRECTORY_MAX_PATH_BYTES: usize = 16 * 1024 * 1024;

/// Default recursion depth for recursive walks.
pub const DEFAULT_WALK_MAX_DEPTH: usize = 128;

/// A checked finite traversal depth or an explicitly unlimited traversal.
/// The root has depth zero. Unlimited depth retains entry and byte budgets.
/// Finite depths 0 through 65,533 are representable; larger values are rejected.
/// Both this type and `Option<DepthLimit>` occupy two bytes.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DepthLimit(std::num::NonZeroU16);

impl Default for DepthLimit {
    fn default() -> Self {
        Self::new(DEFAULT_WALK_MAX_DEPTH).unwrap()
    }
}

impl DepthLimit {
    /// Construct a finite limit. Out-of-range input never disables the limit.
    pub const fn new(depth: usize) -> Option<Self> {
        if depth <= u16::MAX as usize - 2 {
            Some(Self(std::num::NonZeroU16::new((depth + 1) as u16).unwrap()))
        } else {
            None
        }
    }
    /// Disable the depth limit while retaining other traversal budgets.
    pub const fn unlimited() -> Self {
        Self(std::num::NonZeroU16::MAX)
    }
    /// Effective finite limit, or usize::MAX for unlimited traversal.
    pub const fn get(self) -> usize {
        if self.is_unlimited() {
            usize::MAX
        } else {
            self.0.get() as usize - 1
        }
    }
    pub const fn is_unlimited(self) -> bool {
        self.0.get() == u16::MAX
    }
}

/// Default aggregate payload limit for owned vector reads.
pub const DEFAULT_READV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;

/// Application result for one positional vector read, in request order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsReadResult {
    pub offset: u64,
    pub data: Vec<u8>,
    pub eof: bool,
}

/// Application result for one read into caller storage, in request order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadIntoResult {
    pub offset: u64,
    pub bytes_read: usize,
    pub eof: bool,
}

/// Application result for one vector write, in request order.
/// For completed append writes, `offset + bytes_written` is the last reported end;
/// concurrent writers can interleave between short-write waves.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriteResult {
    pub offset: u64,
    pub bytes_written: usize,
    pub stable: bool,
}

/// Completion of a streaming read. Stopping does not mean EOF.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamCompletion {
    Complete,
    Stopped { next_offset: u64 },
}

/// Completion of a callback-based directory traversal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraversalCompletion {
    Complete,
    Stopped,
}

/// Default resource policy for a client. Per-call options override it.
/// Byte limits bound logical payload, not allocator capacity or RPC overhead.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use = "configuration and requests have no effect until used"]
pub struct ResourceLimits {
    /// Maximum collected scalar payload or aggregate vector read buffers.
    /// Not a process-memory cap; standard `Read::read_to_end` and explicit
    /// positional file reads remain caller-managed.
    max_read_bytes: usize,
    /// Default request size for client `read_stream`, not file `Read` or pools.
    stream_chunk_bytes: std::num::NonZeroUsize,
    /// Aggregate entries delivered/collected by a directory call or tree walk.
    max_directory_entries: usize,
    /// Aggregate logical path bytes, excluding allocator and metadata overhead.
    max_directory_path_bytes: usize,
    /// Maximum descent depth; the starting directory is depth zero.
    max_walk_depth: DepthLimit,
}

impl Default for ResourceLimits {
    fn default() -> Self {
        Self {
            max_read_bytes: DEFAULT_READ_MAX_BYTES,
            stream_chunk_bytes: std::num::NonZeroUsize::new(DEFAULT_READ_STREAM_CHUNK_BYTES)
                .unwrap(),
            max_directory_entries: crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES,
            max_directory_path_bytes: crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES,
            max_walk_depth: DepthLimit::new(crate::api::DEFAULT_WALK_MAX_DEPTH).unwrap(),
        }
    }
}

impl ResourceLimits {
    pub fn new() -> Self {
        Self::default()
    }
    /// Set the collected read budget; zero permits only empty payloads.
    pub const fn with_max_read_bytes(mut self, value: usize) -> Self {
        self.max_read_bytes = value;
        self
    }
    /// Read the configured policy value.
    pub const fn max_read_bytes(self) -> usize {
        self.max_read_bytes
    }
    /// Set the default streaming chunk size. Nonzero chunks guarantee progress.
    pub const fn with_stream_chunk_size(mut self, value: std::num::NonZeroUsize) -> Self {
        self.stream_chunk_bytes = value;
        self
    }
    /// Read the configured policy value.
    pub const fn stream_chunk_size(self) -> usize {
        self.stream_chunk_bytes.get()
    }
    /// Set the aggregate directory entry budget; zero is a valid budget.
    pub const fn with_max_directory_entries(mut self, value: usize) -> Self {
        self.max_directory_entries = value;
        self
    }
    /// Read the configured policy value.
    pub const fn max_directory_entries(self) -> usize {
        self.max_directory_entries
    }
    /// Set the aggregate path byte budget; zero is a valid budget.
    pub const fn with_max_directory_path_bytes(mut self, value: usize) -> Self {
        self.max_directory_path_bytes = value;
        self
    }
    /// Read the configured policy value.
    pub const fn max_directory_path_bytes(self) -> usize {
        self.max_directory_path_bytes
    }
    /// Set maximum descent depth; zero includes only the starting directory.
    pub const fn with_max_walk_depth(mut self, value: DepthLimit) -> Self {
        self.max_walk_depth = value;
        self
    }
    /// Read the configured policy value.
    pub const fn max_walk_depth(self) -> DepthLimit {
        self.max_walk_depth
    }

    pub fn directory_options(self) -> ListDirOptions {
        ListDirOptions::new()
            .with_max_entries(self.max_directory_entries)
            .with_max_path_bytes(self.max_directory_path_bytes)
    }

    pub fn walk_options(self) -> crate::api::ListDirOptions {
        crate::api::ListDirOptions::new()
            .with_recursive(true)
            .with_max_entries(self.max_directory_entries)
            .with_max_path_bytes(self.max_directory_path_bytes)
            .with_max_depth(self.max_walk_depth)
    }
}

/// One directory and its entries, with attributes fetched during enumeration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DirectoryListing {
    pub path: PathBuf,
    pub entries: Vec<DirEntry>,
}

#[cfg(test)]
mod depth_limit_tests {
    use super::*;
    #[test]
    fn checked_depths_preserve_finite_limits_and_inheritance() {
        for depth in [0, 1, 128, 200, 201, 500, 65533] {
            let limit = DepthLimit::new(depth).unwrap();
            assert!(!limit.is_unlimited());
            let limits = ResourceLimits::new().with_max_walk_depth(limit);
            assert_eq!(
                ListDirOptions::new()
                    .resolve(limits)
                    .max_depth_or(Default::default()),
                limit
            );
        }
        for invalid in [65534, 65535, usize::MAX] {
            assert!(DepthLimit::new(invalid).is_none());
        }
        assert!(
            ListDirOptions::new()
                .resolve(ResourceLimits::new().with_max_walk_depth(DepthLimit::unlimited()))
                .max_depth_or(Default::default())
                .is_unlimited()
        );
    }
}