Skip to main content

coven_storage/
object_listing.rs

1use crate::{CloudProvider, ObjectPath, ObjectPrefix, StorageError, StorageFailure, StoredObject};
2use std::collections::BTreeMap;
3
4/// The connection's listing result. Native providers add entries while consuming
5/// pages so a conflict stops the request before fetching another page.
6///
7/// A store's folder or prefix can also hold things coven never wrote: a
8/// person's stray files and folders, native documents, another store's
9/// folder, or a name close to a coven path. D10 gives each object exactly
10/// one spelling and no reader looks anywhere else, so such an entry is no
11/// object of this store and the listing skips it. What the provider itself
12/// gets wrong — an entry outside the requested scope, missing metadata, a
13/// failed or incomplete page — still fails the listing.
14pub struct ObjectListing {
15    prefix: ObjectPrefix,
16    provider: CloudProvider,
17    objects: BTreeMap<ObjectPath, StoredObject>,
18    skipped: usize,
19}
20
21/// Refuse a name a provider listed outside the requested prefix, rather than
22/// act on it.
23pub(crate) fn listed_within(name: &str, prefix: &str) -> Result<(), StorageError> {
24    if name.starts_with(prefix) {
25        Ok(())
26    } else {
27        Err(StorageFailure::Protocol.with_source("provider listed outside the requested prefix"))
28    }
29}
30
31impl ObjectListing {
32    pub(crate) fn new(prefix: ObjectPrefix, provider: CloudProvider) -> Self {
33        Self { prefix, provider, objects: BTreeMap::new(), skipped: 0 }
34    }
35
36    /// Requested object namespace, beginning with the store id within the provider location.
37    pub fn prefix(&self) -> &ObjectPrefix {
38        &self.prefix
39    }
40
41    /// Merge a native entry. HTTP pages may repeat identical entries; CloudKit's
42    /// native queries promise unique paths. Conflicting metadata always fails.
43    pub fn insert(&mut self, object: StoredObject) -> Result<(), StorageError> {
44        object.check(&object.path)?;
45        if !self.prefix.contains(&object.path) {
46            return Err(StorageFailure::Protocol.with_source("provider listed outside prefix"));
47        }
48        if let Some(previous) = self.objects.get(&object.path) {
49            if self.provider == CloudProvider::CloudKit || previous != &object {
50                return Err(
51                    StorageFailure::Protocol.with_source("provider listed conflicting objects")
52                );
53            }
54        }
55        self.objects.insert(object.path.clone(), object);
56        Ok(())
57    }
58
59    /// The object path of a native file listed at `name`, relative to the
60    /// location and starting with the store id. `None` is an entry outside
61    /// D10, which the listing skips; a name outside the requested prefix is
62    /// the provider's error.
63    pub(crate) fn object_path(&mut self, name: &str) -> Result<Option<ObjectPath>, StorageError> {
64        self.within(name)?;
65        let path = ObjectPath::parse(name).ok();
66        self.skipped += usize::from(path.is_none());
67        Ok(path)
68    }
69
70    /// Whether a native folder listed at `name` can hold this store's
71    /// objects, so a recursive listing descends into it. A folder outside
72    /// D10's directories holds none and is skipped with its contents.
73    pub(crate) fn object_folder(&mut self, name: &str) -> Result<bool, StorageError> {
74        if name == self.prefix.folder() {
75            // A recursive listing can include the requested folder itself.
76            return Ok(true);
77        }
78        self.within(name)?;
79        let folder = crate::path::validate_directory(name).is_ok();
80        self.skipped += usize::from(!folder);
81        Ok(folder)
82    }
83
84    /// Count a native entry that cannot be an object at all, such as a
85    /// document, shortcut or package, whatever its name.
86    pub(crate) fn skip(&mut self) {
87        self.skipped += 1;
88    }
89
90    fn within(&self, name: &str) -> Result<(), StorageError> {
91        listed_within(name, self.prefix.as_str())
92    }
93
94    pub(crate) fn finish(self) -> Vec<StoredObject> {
95        if self.skipped > 0 {
96            tracing::debug!(
97                prefix = self.prefix.as_str(),
98                skipped = self.skipped,
99                "listing skipped entries outside the store layout"
100            );
101        }
102        self.objects.into_values().collect()
103    }
104}
105
106#[cfg(test)]
107#[path = "object_listing_tests.rs"]
108mod tests;