use super::ProviderOperation;
use super::ProviderOperations;
use crate::error::FsError;
use crate::error::FsErrorKind;
use crate::error::FsOperation;
use crate::error::FsResult;
use crate::metadata::FileSystemCapabilities;
use crate::metadata::FileSystemCapability;
use crate::metadata::FileSystemInfo;
use crate::metadata::FileSystemLimits;
use crate::metadata::FileSystemProperties;
use crate::metadata::SymlinkPolicy;
use crate::path::PathConstraints;
#[derive(Clone, Debug)]
pub struct ProviderProperties {
info: FileSystemInfo,
operations: ProviderOperations,
declared_capabilities: FileSystemCapabilities,
limits: FileSystemLimits,
path_constraints: PathConstraints,
symlink_policy: SymlinkPolicy,
}
impl ProviderProperties {
pub fn new(
info: FileSystemInfo,
operations: ProviderOperations,
declared_capabilities: FileSystemCapabilities,
limits: FileSystemLimits,
path_constraints: PathConstraints,
symlink_policy: SymlinkPolicy,
) -> FsResult<Self> {
let value = Self {
info,
operations,
declared_capabilities,
limits,
path_constraints,
symlink_policy,
};
value.validate()?;
Ok(value)
}
#[inline]
#[must_use]
pub const fn info(&self) -> &FileSystemInfo {
&self.info
}
#[inline]
#[must_use]
pub const fn operations(&self) -> ProviderOperations {
self.operations
}
#[inline]
#[must_use]
pub const fn declared_capabilities(&self) -> FileSystemCapabilities {
self.declared_capabilities
}
#[inline]
#[must_use]
pub const fn limits(&self) -> &FileSystemLimits {
&self.limits
}
#[inline]
#[must_use]
pub const fn path_constraints(&self) -> &PathConstraints {
&self.path_constraints
}
#[inline]
#[must_use = "the provider symbolic-link policy must be used"]
pub const fn symlink_policy(&self) -> SymlinkPolicy {
self.symlink_policy
}
fn validate(&self) -> FsResult<()> {
FileSystemProperties::new(
self.info.clone(),
self.declared_capabilities,
self.limits,
self.path_constraints.clone(),
self.symlink_policy,
)?;
let error = FileSystemCapability::ALL.iter().copied().find_map(|capability| {
if self.declared_capabilities.supports(capability) {
self.missing_capability_operation_error(capability)
} else {
None
}
});
if let Some(error) = error {
return Err(error);
}
Ok(())
}
fn missing_capability_operation_error(&self, capability: FileSystemCapability) -> Option<FsError> {
let operation = self.missing_capability_operation(capability)?;
let message =
format!("declared capability {capability:?} requires unavailable provider operation {operation:?}",);
Some(
FsError::new(FsErrorKind::InvalidOptions, FsOperation::ValidateProperties, &message)
.with_required_capability(capability),
)
}
fn missing_capability_operation(&self, capability: FileSystemCapability) -> Option<ProviderOperation> {
let operations = self.operations;
match capability {
FileSystemCapability::List => {
(!operations.supports(ProviderOperation::List)).then_some(ProviderOperation::List)
}
FileSystemCapability::Read
| FileSystemCapability::RangeRead
| FileSystemCapability::ConditionalRead
| FileSystemCapability::ChecksumValidation => {
(!operations.supports(ProviderOperation::OpenReader)).then_some(ProviderOperation::OpenReader)
}
FileSystemCapability::Write
| FileSystemCapability::Append
| FileSystemCapability::ConditionalWrite
| FileSystemCapability::AtomicReplace
| FileSystemCapability::DurableWrite => {
(!operations.supports(ProviderOperation::OpenWriter)).then_some(ProviderOperation::OpenWriter)
}
FileSystemCapability::CreateDirectory | FileSystemCapability::EmptyDirectory => {
(!operations.supports(ProviderOperation::CreateDirectory)).then_some(ProviderOperation::CreateDirectory)
}
FileSystemCapability::Delete | FileSystemCapability::ConditionalDelete => {
if !operations.supports(ProviderOperation::DeleteFile) {
Some(ProviderOperation::DeleteFile)
} else if !operations.supports(ProviderOperation::DeleteDirectory) {
Some(ProviderOperation::DeleteDirectory)
} else {
None
}
}
FileSystemCapability::RecursiveDelete => {
(!operations.supports(ProviderOperation::DeleteDirectory)).then_some(ProviderOperation::DeleteDirectory)
}
FileSystemCapability::Rename | FileSystemCapability::AtomicRename | FileSystemCapability::DurableRename => {
(!operations.supports(ProviderOperation::Rename)).then_some(ProviderOperation::Rename)
}
FileSystemCapability::TempFile => {
(!operations.supports(ProviderOperation::CreateTempFile)).then_some(ProviderOperation::CreateTempFile)
}
FileSystemCapability::TempDirectory => (!operations.supports(ProviderOperation::CreateTempDirectory))
.then_some(ProviderOperation::CreateTempDirectory),
FileSystemCapability::AtomicTempPersist => {
if !operations.supports(ProviderOperation::CreateTempFile) {
Some(ProviderOperation::CreateTempFile)
} else if !operations.supports(ProviderOperation::CreateTempDirectory) {
Some(ProviderOperation::CreateTempDirectory)
} else {
None
}
}
FileSystemCapability::Copy
| FileSystemCapability::ServerSideCopy
| FileSystemCapability::AtomicFileCopy
| FileSystemCapability::AtomicTreeCopy
| FileSystemCapability::DurableFileCopy
| FileSystemCapability::DurableTreeCopy => {
(!operations.supports(ProviderOperation::TryCopy)).then_some(ProviderOperation::TryCopy)
}
FileSystemCapability::Symlink => None,
}
}
}
#[cfg(test)]
mod tests {
use super::ProviderProperties;
use crate::error::FsOperation;
use crate::metadata::FileSystemCapabilities;
use crate::metadata::FileSystemCapability;
use crate::metadata::FileSystemId;
use crate::metadata::FileSystemInfo;
use crate::metadata::FileSystemLimits;
use crate::metadata::SymlinkPolicy;
use crate::path::PathConstraints;
use crate::path::PathSemantics;
use crate::spi::ProviderOperation;
use crate::spi::ProviderOperations;
fn properties_with_operations(operations: ProviderOperations) -> ProviderProperties {
ProviderProperties {
info: FileSystemInfo::new(
FileSystemId::new("provider-mapping-test").expect("test filesystem id should be valid"),
"provider-mapping-test",
PathSemantics::Hierarchical,
),
operations,
declared_capabilities: FileSystemCapabilities::new(),
limits: FileSystemLimits::unknown(),
path_constraints: PathConstraints::absolute(),
symlink_policy: SymlinkPolicy::Reject,
}
}
fn operations_without(missing: Option<ProviderOperation>) -> ProviderOperations {
let mut operations = ProviderOperations::new();
for operation in [
ProviderOperation::Stat,
ProviderOperation::List,
ProviderOperation::OpenReader,
ProviderOperation::OpenWriter,
ProviderOperation::CreateDirectory,
ProviderOperation::DeleteFile,
ProviderOperation::DeleteDirectory,
ProviderOperation::TryCopy,
ProviderOperation::Rename,
ProviderOperation::CreateTempFile,
ProviderOperation::CreateTempDirectory,
] {
if missing != Some(operation) {
operations = operations.with(operation);
}
}
operations
}
#[test]
fn test_missing_capability_operation_maps_each_capability_directly() {
let cases = [
(FileSystemCapability::List, Some(ProviderOperation::List)),
(FileSystemCapability::Read, Some(ProviderOperation::OpenReader)),
(FileSystemCapability::RangeRead, Some(ProviderOperation::OpenReader)),
(
FileSystemCapability::ConditionalRead,
Some(ProviderOperation::OpenReader),
),
(
FileSystemCapability::ChecksumValidation,
Some(ProviderOperation::OpenReader),
),
(FileSystemCapability::Write, Some(ProviderOperation::OpenWriter)),
(FileSystemCapability::Append, Some(ProviderOperation::OpenWriter)),
(
FileSystemCapability::ConditionalWrite,
Some(ProviderOperation::OpenWriter),
),
(FileSystemCapability::AtomicReplace, Some(ProviderOperation::OpenWriter)),
(FileSystemCapability::DurableWrite, Some(ProviderOperation::OpenWriter)),
(
FileSystemCapability::CreateDirectory,
Some(ProviderOperation::CreateDirectory),
),
(
FileSystemCapability::EmptyDirectory,
Some(ProviderOperation::CreateDirectory),
),
(FileSystemCapability::Delete, Some(ProviderOperation::DeleteFile)),
(FileSystemCapability::Delete, Some(ProviderOperation::DeleteDirectory)),
(
FileSystemCapability::RecursiveDelete,
Some(ProviderOperation::DeleteDirectory),
),
(
FileSystemCapability::ConditionalDelete,
Some(ProviderOperation::DeleteFile),
),
(
FileSystemCapability::ConditionalDelete,
Some(ProviderOperation::DeleteDirectory),
),
(FileSystemCapability::Rename, Some(ProviderOperation::Rename)),
(FileSystemCapability::AtomicRename, Some(ProviderOperation::Rename)),
(FileSystemCapability::DurableRename, Some(ProviderOperation::Rename)),
(FileSystemCapability::TempFile, Some(ProviderOperation::CreateTempFile)),
(
FileSystemCapability::TempDirectory,
Some(ProviderOperation::CreateTempDirectory),
),
(
FileSystemCapability::AtomicTempPersist,
Some(ProviderOperation::CreateTempFile),
),
(
FileSystemCapability::AtomicTempPersist,
Some(ProviderOperation::CreateTempDirectory),
),
(FileSystemCapability::Copy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::ServerSideCopy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::AtomicFileCopy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::AtomicTreeCopy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::DurableFileCopy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::DurableTreeCopy, Some(ProviderOperation::TryCopy)),
(FileSystemCapability::Symlink, None),
];
for (capability, required_operation) in cases {
let properties = properties_with_operations(operations_without(required_operation));
assert_eq!(required_operation, properties.missing_capability_operation(capability),);
match required_operation {
Some(required_operation) => {
let error = properties
.missing_capability_operation_error(capability)
.expect("missing operation must produce an error");
assert_eq!(FsOperation::ValidateProperties, error.operation());
assert_eq!(Some(capability), error.required_capability());
assert!(format!("{error}").contains(&format!("{required_operation:?}")));
}
None => assert!(properties.missing_capability_operation_error(capability).is_none()),
}
}
}
}