#[cfg(not(unix))]
use crate::discovery::modified_ns;
use crate::discovery::{
DiscoveryOptions, PolicyRoot, ResourceKind, discover_with_policy, ensure_allowed_path,
};
use crate::{Error, ResourceId, Result};
use serde::{Deserialize, Serialize};
use std::fs::{File, Metadata};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ResourceVersion(Option<String>);
impl ResourceVersion {
pub fn known(token: impl Into<String>) -> Self {
Self(Some(token.into()))
}
pub fn unknown() -> Self {
Self(None)
}
pub fn matches(&self, other: &Self) -> bool {
self.0.is_some() && self == other
}
pub fn is_known(&self) -> bool {
self.0.is_some()
}
pub(crate) fn file(size: u64, modified: Option<i64>) -> Self {
modified
.map(|t| Self::known(format!("fs:{size}:{t}")))
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct ResourceScope {
pub roots: Vec<ResourceId>,
pub discovery: DiscoveryOptions,
}
impl ResourceScope {
pub(crate) fn contains(&self, id: &ResourceId) -> bool {
self.roots.iter().any(|root| {
if let (Some(path), Some(root)) = (id.to_file_path(), root.to_file_path()) {
path.starts_with(root)
} else {
id == root
|| id
.as_str()
.strip_prefix(root.as_str())
.is_some_and(|rest| root.as_str().ends_with('/') || rest.starts_with('/'))
}
})
}
pub(crate) fn from_paths(
paths: &[PathBuf],
mut roots: Vec<ResourceId>,
discovery: DiscoveryOptions,
) -> Result<Self> {
for root in &roots {
if let Some(path) = root.to_file_path() {
ensure_allowed_path(&path)?;
}
}
for p in paths {
let root = PolicyRoot::new(p)?;
roots.push(ResourceId::from_path(root.uri_path())?);
}
Ok(Self { roots, discovery })
}
}
#[derive(Debug, Clone)]
pub struct ProvidedResource {
pub id: ResourceId,
pub kind: ResourceKind,
pub size_bytes: u64,
pub modified_ns: Option<i64>,
pub version: ResourceVersion,
pub title: Option<String>,
}
impl ProvidedResource {
pub(crate) fn fingerprint(&self) -> String {
let encoded = serde_json::to_vec(&(self.kind, &self.title)).expect("descriptor serializes");
blake3::hash(&encoded).to_hex().to_string()
}
}
pub trait ReadSeek: std::io::Read + std::io::Seek + Send + Sync {}
impl<T: std::io::Read + std::io::Seek + Send + Sync> ReadSeek for T {}
pub trait ResourceProvider: Send + Sync {
fn open(&self, id: &ResourceId) -> Result<Box<dyn ReadSeek>> {
Err(Error::UnsupportedAudio {
resource: id.clone(),
reason: "provider does not support seekable audio reads",
})
}
fn list(&self, scope: &ResourceScope) -> Result<Vec<ProvidedResource>>;
fn list_walked(&self, scope: &ResourceScope) -> Result<ResourceListing> {
Ok(ResourceListing {
resources: self.list(scope)?,
walked_roots: None,
})
}
fn supports_cached_listing(&self) -> bool {
false
}
fn list_cached(
&self,
scope: &ResourceScope,
_ids: &[ResourceId],
) -> Result<Vec<ProvidedResource>> {
self.list(scope)
}
fn read(&self, id: &ResourceId) -> Result<Vec<u8>>;
fn version(&self, id: &ResourceId) -> Result<ResourceVersion>;
fn deletion_scope(&self, scope: &ResourceScope) -> Result<ResourceScope> {
Ok(scope.clone())
}
fn is_deleted(&self, _id: &ResourceId, _scope: &ResourceScope) -> Result<bool> {
Ok(false)
}
}
#[derive(Debug, Clone, Default)]
pub struct ResourceListing {
pub resources: Vec<ProvidedResource>,
pub walked_roots: Option<Vec<ResourceId>>,
}
#[derive(Debug, Default)]
pub struct FsProvider;
fn file_path(id: &ResourceId) -> Result<PathBuf> {
id.to_file_path().ok_or_else(|| {
Error::Config(format!(
"filesystem provider cannot access {} resources",
id.scheme()
))
})
}
impl FsProvider {
pub(crate) fn open_verified_handle(id: &ResourceId) -> Result<File> {
let path = file_path(id)?;
ensure_allowed_path(&path)?;
open_local_content(&path)
}
}
impl ResourceProvider for FsProvider {
fn open(&self, id: &ResourceId) -> Result<Box<dyn ReadSeek>> {
let path = file_path(id)?;
ensure_allowed_path(&path)?;
Ok(Box::new(open_local_content(&path)?))
}
fn list(&self, scope: &ResourceScope) -> Result<Vec<ProvidedResource>> {
Ok(self.list_walked(scope)?.resources)
}
fn list_walked(&self, scope: &ResourceScope) -> Result<ResourceListing> {
let paths = scope
.roots
.iter()
.map(file_path)
.collect::<Result<Vec<_>>>()?;
let walked = discover_with_policy(&paths, &scope.discovery, |_| true)?;
let resources = walked
.resources
.into_iter()
.map(|r| {
Ok(ProvidedResource {
id: ResourceId::from_path(&r.path)?,
kind: r.kind,
size_bytes: r.size_bytes,
modified_ns: r.modified_ns,
version: ResourceVersion::file(r.size_bytes, r.modified_ns),
title: r.path.file_stem().map(|s| s.to_string_lossy().into_owned()),
})
})
.collect::<Result<_>>()?;
Ok(ResourceListing {
resources,
walked_roots: Some(
walked
.walked_roots
.iter()
.map(ResourceId::from_path)
.collect::<Result<_>>()?,
),
})
}
fn read(&self, id: &ResourceId) -> Result<Vec<u8>> {
let path = file_path(id)?;
ensure_allowed_path(&path)?;
let mut file = open_local_content(&path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|e| Error::io(path, e))?;
Ok(bytes)
}
fn version(&self, id: &ResourceId) -> Result<ResourceVersion> {
let path = file_path(id)?;
ensure_allowed_path(&path)?;
local_version(&path)
}
fn deletion_scope(&self, scope: &ResourceScope) -> Result<ResourceScope> {
let roots = scope
.roots
.iter()
.map(|root| {
let path = file_path(root)?;
let real = path.canonicalize().map_err(|e| Error::io(&path, e))?;
ResourceId::from_path(real)
})
.collect::<Result<Vec<_>>>()?;
Ok(ResourceScope {
roots,
discovery: scope.discovery.clone(),
})
}
fn is_deleted(&self, id: &ResourceId, scope: &ResourceScope) -> Result<bool> {
let Some(path) = id.to_file_path() else {
return Ok(false);
};
if !scope.contains(id) {
return Ok(false);
}
match std::fs::metadata(&path) {
Ok(_) => Ok(false),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true),
Err(e) => Err(Error::io(path, e)),
}
}
}
fn placeholder_marker(path: &Path) -> bool {
path.components().any(|part| {
let bytes = part.as_os_str().as_encoded_bytes();
bytes[bytes.len().saturating_sub(7)..].eq_ignore_ascii_case(b".icloud")
})
}
fn unavailable_content() -> io::Error {
io::Error::new(
io::ErrorKind::PermissionDenied,
"unmaterialized local content is unavailable; choose a fully local copy",
)
}
#[cfg(target_os = "macos")]
pub(crate) fn ensure_materialized_flags(flags: u32) -> io::Result<()> {
const SF_DATALESS: u32 = 0x4000_0000;
if flags & SF_DATALESS != 0 {
Err(unavailable_content())
} else {
Ok(())
}
}
fn ensure_content_metadata(metadata: &Metadata) -> io::Result<()> {
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"content source is not a regular file",
));
}
#[cfg(target_os = "macos")]
{
use std::os::darwin::fs::MetadataExt;
ensure_materialized_flags(metadata.st_flags())?;
}
Ok(())
}
#[cfg(unix)]
fn local_version(path: &Path) -> Result<ResourceVersion> {
use crate::pinned_dir::{EntryType, PinnedDir, entry_type, link_component, modified_ns};
let stat = PinnedDir::open_parent(path)
.and_then(|(parent, name)| parent.stat(name))
.map_err(|e| Error::io(path, e))?;
if entry_type(&stat) == EntryType::Symlink {
return Err(Error::io(path, link_component()));
}
Ok(ResourceVersion::file(
stat.st_size as u64,
modified_ns(&stat),
))
}
#[cfg(not(unix))]
fn local_version(path: &Path) -> Result<ResourceVersion> {
let meta = std::fs::metadata(path).map_err(|e| Error::io(path, e))?;
Ok(ResourceVersion::file(meta.len(), modified_ns(&meta)))
}
#[cfg(unix)]
fn open_local_content(path: &Path) -> Result<File> {
use crate::pinned_dir::{EntryType, PinnedDir, entry_type, identity, link_component};
use std::os::unix::fs::MetadataExt;
if placeholder_marker(path) {
return Err(Error::io(path, unavailable_content()));
}
let (parent, name) = PinnedDir::open_parent(path).map_err(|e| Error::io(path, e))?;
let before = parent.stat(name).map_err(|e| Error::io(path, e))?;
match entry_type(&before) {
EntryType::File => {
crate::pinned_dir::materialized(&before).map_err(|e| Error::io(path, e))?
}
EntryType::Symlink => return Err(Error::io(path, link_component())),
_ => {
return Err(Error::io(
path,
io::Error::new(
io::ErrorKind::InvalidInput,
"content source is not a regular file",
),
));
}
}
let file = parent.open_file(name).map_err(|e| Error::io(path, e))?;
let actual = file.metadata().map_err(|e| Error::io(path, e))?;
ensure_content_metadata(&actual).map_err(|e| Error::io(path, e))?;
if identity(&before) != (actual.dev(), actual.ino()) {
return Err(Error::ChangedWhileReading {
resource: ResourceId::from_path(path)?,
});
}
Ok(file)
}
#[cfg(not(unix))]
fn open_local_content(path: &Path) -> Result<File> {
if placeholder_marker(path) {
return Err(Error::io(path, unavailable_content()));
}
let resolved = path.canonicalize().map_err(|e| Error::io(path, e))?;
if placeholder_marker(&resolved) {
return Err(Error::io(path, unavailable_content()));
}
let before = std::fs::metadata(path).map_err(|e| Error::io(path, e))?;
ensure_content_metadata(&before).map_err(|e| Error::io(path, e))?;
let file = File::open(path).map_err(|e| Error::io(path, e))?;
let actual = file.metadata().map_err(|e| Error::io(path, e))?;
ensure_content_metadata(&actual).map_err(|e| Error::io(path, e))?;
Ok(file)
}
#[cfg(all(test, target_os = "macos"))]
mod materialization_tests {
use super::*;
#[test]
fn darwin_dataless_flag_is_denied_without_manufacturing_a_cloud_file() {
assert!(ensure_materialized_flags(0x4000_0000).is_err());
assert!(ensure_materialized_flags(0x4000_8000).is_err());
assert!(ensure_materialized_flags(0).is_ok());
assert!(ensure_materialized_flags(0x8000).is_ok());
}
}