qubit-fs 0.2.2

Provider-neutral synchronous and asynchronous filesystem abstraction for Rust
Documentation
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Shared immutable state and deterministic policy for filesystem facades.

use std::sync::Arc;

use qubit_budget::InsufficientBudgetError;
use qubit_budget::ResourceBudget;

use crate::copy::CopyAssessment;
use crate::copy::CopyExecutionRoute;
use crate::copy::CopyOptions;
use crate::copy::FallbackRejection;
use crate::error::FsError;
use crate::error::FsErrorKind;
use crate::error::FsOperation;
use crate::error::FsResult;
use crate::facade::internal::ByteBudget;
use crate::facade::internal::FileSystemResource;
use crate::metadata::FileSystemCapability;
use crate::metadata::FileSystemProperties;
use crate::path::Path;
use crate::spi::ProviderOperation;
use crate::spi::ProviderOperations;
use crate::spi::ProviderProperties;
use crate::write::WriteOptions;

/// Immutable provider state and deterministic preflight shared by facades.
pub(crate) struct FacadeCore {
    /// Effective application-facing properties derived from the provider.
    properties: Arc<FileSystemProperties>,
    /// Concrete provider operations captured with the property snapshot.
    provider_operations: ProviderOperations,
}

impl FacadeCore {
    /// Fixed I/O chunk used by bounded prefix reads.
    pub(crate) const PREFIX_BUFFER_SIZE: usize = 8192;

    /// Validates and captures one provider property snapshot.
    ///
    /// # Errors
    /// Returns the provider-property validation error when the declared
    /// capabilities and concrete operations are inconsistent.
    pub(crate) fn new(provider: ProviderProperties) -> FsResult<Self> {
        let provider_operations = provider.operations();
        let properties = FileSystemProperties::from_provider(&provider)?;
        Ok(Self {
            properties: Arc::new(properties),
            provider_operations,
        })
    }

    /// Returns the cached effective application-facing properties.
    #[inline]
    pub(crate) fn properties(&self) -> &FileSystemProperties {
        &self.properties
    }

    /// Reports whether the captured provider exposes a concrete operation.
    #[inline]
    pub(crate) fn provider_supports(&self, operation: ProviderOperation) -> bool {
        self.provider_operations.supports(operation)
    }

    /// Validates one logical path against the cached provider snapshot.
    ///
    /// # Errors
    /// Returns an enriched invalid-path or resource-limit error when the path
    /// semantics, form, constraints, or limits do not match the filesystem.
    pub(crate) fn validate_path(&self, path: &Path, operation: FsOperation) -> FsResult<()> {
        self.properties.validate_path(path, operation)
    }

    /// Validates the optional parent used for temporary resource creation.
    ///
    /// # Errors
    /// Returns an enriched path-validation error when `parent` is present and
    /// does not satisfy the cached filesystem rules.
    pub(crate) fn validate_temp_parent(&self, parent: Option<&Path>) -> FsResult<()> {
        parent.map_or(Ok(()), |path| self.validate_path(path, FsOperation::CreateTemp))
    }

    /// Validates a writer path, options, and required write capability.
    pub(crate) fn validate_write_request(&self, path: &Path, options: &WriteOptions) -> FsResult<()> {
        self.validate_path(path, FsOperation::OpenWriter)?;
        options
            .validate_against(self.properties.capabilities())
            .map_err(|error| self.enrich(error, Some(path), FsOperation::OpenWriter))?;
        self.require(FileSystemCapability::Write, FsOperation::OpenWriter, Some(path))
    }

    /// Assesses copy routes from the immutable provider snapshot without I/O.
    ///
    /// # Errors
    /// Returns an invalid-options or requirement error when the paths or copy
    /// options cannot be served by either the provider or the stream fallback.
    pub(crate) fn assess_copy(&self, source: &Path, target: &Path, options: &CopyOptions) -> FsResult<CopyAssessment> {
        self.validate_path(source, FsOperation::Copy)?;
        self.validate_path(target, FsOperation::Copy)?;
        if source == target {
            return Err(FsError::new(
                FsErrorKind::InvalidOptions,
                FsOperation::Copy,
                "copy source and target must differ",
            )
            .with_path(source.clone())
            .with_target(target.clone()));
        }
        options
            .validate_against(self.properties.capabilities())
            .map_err(|error| {
                self.enrich(error, Some(source), FsOperation::Copy)
                    .with_target(target.clone())
            })?;
        if options.max_entries() == Some(0) {
            return Err(FsError::new(
                FsErrorKind::RequirementNotMet,
                FsOperation::Copy,
                "copy entry limit must be greater than zero",
            ));
        }
        let mut rejection = crate::copy::fallback_rejection(options, self.properties.symlink_policy());
        if rejection.is_none() {
            if !self.properties.capabilities().supports(FileSystemCapability::Read)
                || !self.provider_operations.supports(ProviderOperation::OpenReader)
            {
                rejection = Some(FallbackRejection::MissingRead);
            } else if !self.properties.capabilities().supports(FileSystemCapability::Write)
                || !self.provider_operations.supports(ProviderOperation::OpenWriter)
            {
                rejection = Some(FallbackRejection::MissingWrite);
            } else if !self.provider_operations.supports(ProviderOperation::Stat) {
                rejection = Some(FallbackRejection::MissingStat);
            }
        }
        let provider_copy = self.provider_operations.supports(ProviderOperation::TryCopy);
        match (provider_copy, rejection) {
            (true, reason @ None) => Ok(CopyAssessment::new(CopyExecutionRoute::ProviderThenStream, reason)),
            (true, reason @ Some(_)) => Ok(CopyAssessment::new(CopyExecutionRoute::ProviderOnly, reason)),
            (false, None) => Ok(CopyAssessment::new(CopyExecutionRoute::StreamOnly, None)),
            (false, Some(_)) => Err(FsError::new(
                FsErrorKind::RequirementNotMet,
                FsOperation::Copy,
                "copy has no available provider or stream execution path",
            )),
        }
    }

    /// Requires one capability before an operation can create provider I/O.
    ///
    /// `path` may be absent for operations that have no natural path context.
    ///
    /// # Errors
    /// Returns an enriched unsupported-capability error when the cached
    /// properties do not advertise `capability`.
    pub(crate) fn require(
        &self,
        capability: FileSystemCapability,
        operation: FsOperation,
        path: Option<&Path>,
    ) -> FsResult<()> {
        if self.properties.capabilities().supports(capability) {
            Ok(())
        } else {
            let error = FsError::new(
                FsErrorKind::UnsupportedCapability,
                operation,
                "filesystem capability is not supported",
            )
            .with_required_capability(capability)
            .with_missing_provider(self.properties.info().provider_id());
            Err(match path {
                Some(path) => error.with_path(path.clone()),
                None => error,
            })
        }
    }

    /// Adds missing operation, optional path, and provider context to an error.
    ///
    /// Existing provider-supplied context is preserved. `None` does not invent
    /// a path for pathless operations.
    pub(crate) fn enrich(&self, error: FsError, path: Option<&Path>, operation: FsOperation) -> FsError {
        let error = error
            .with_operation(operation)
            .with_missing_provider(self.properties.info().provider_id());
        match path {
            Some(path) => error.with_missing_context(path, None, self.properties.info().provider_id()),
            None => error,
        }
    }

    /// Builds a provider-contract error bound to a requested path.
    pub(crate) fn contract_error(&self, path: &Path, operation: FsOperation, message: &str) -> FsError {
        FsError::new(FsErrorKind::ProviderContractViolation, operation, message)
            .with_path(path.clone())
            .with_provider(self.properties.info().provider_id())
    }

    /// Creates a byte budget with the supplied inclusive limit.
    #[inline]
    pub(crate) fn byte_budget(resource: FileSystemResource, maximum: u64) -> ByteBudget {
        ResourceBudget::new(resource, maximum)
    }

    /// Converts a platform-sized I/O count to the budget quantity type.
    ///
    /// # Errors
    /// Returns a contextual resource-limit error when `value` cannot be
    /// represented by the public `u64` accounting domain.
    #[inline]
    pub(crate) fn quantity_from_usize(
        value: usize,
        operation: FsOperation,
        path: &Path,
        provider: &str,
    ) -> Result<u64, FsError> {
        u64::try_from(value).map_err(|error| {
            FsError::with_source(
                FsErrorKind::ResourceLimitExceeded,
                operation,
                "I/O byte count cannot be represented by the resource budget",
                error,
            )
            .with_path(path.clone())
            .with_provider(provider)
        })
    }

    /// Converts a budget failure into a contextual filesystem error.
    #[inline]
    pub(crate) fn budget_error(
        error: InsufficientBudgetError<FileSystemResource, u64>,
        operation: FsOperation,
        path: &Path,
        provider: &str,
        message: &'static str,
    ) -> FsError {
        FsError::with_source(FsErrorKind::ResourceLimitExceeded, operation, message, error)
            .with_path(path.clone())
            .with_provider(provider)
    }

    /// Returns the next bounded read length for an accumulated prefix.
    #[inline]
    pub(crate) fn next_prefix_read_len(accumulated: usize, maximum: usize) -> usize {
        maximum.saturating_sub(accumulated).min(Self::PREFIX_BUFFER_SIZE)
    }
}

#[cfg(test)]
mod tests {
    use super::FacadeCore;
    use crate::error::FsOperation;
    use crate::path::Path;

    #[test]
    fn quantity_conversion_is_executed_at_runtime() {
        let path = Path::parse("/facade-core").expect("valid path");
        assert_eq!(
            FacadeCore::quantity_from_usize(7, FsOperation::Read, &path, "test").expect("value fits"),
            7,
        );
    }
}