Skip to main content

unitycatalog_server/services/
location.rs

1//! Storage locations of governed securables.
2//!
3//! A [`StorageLocationUrl`] keeps the URL a client sent (for messages and local
4//! paths) next to its canonical [`StorageLocation`] from `olai-storage-credentials`,
5//! which every comparison uses: two spellings of one location (`abfss://` and
6//! `https://…dfs.core.windows.net`, say) match, and prefixes match on whole path
7//! segments only.
8
9use object_store::path::Path;
10use olai_storage_credentials::{StorageLocation, StorageProvider, StorageScope};
11use url::Url;
12
13use crate::{Error, Result};
14
15/// A parsed storage location: the URL as given plus its canonical form.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct StorageLocationUrl {
18    raw: Url,
19    location: StorageLocation,
20}
21
22impl StorageLocationUrl {
23    /// Parses a storage URL.
24    pub fn parse(url: impl AsRef<str>) -> Result<Self> {
25        let url = url.as_ref();
26        let raw = Url::parse(url)
27            .map_err(|e| Error::invalid_argument(format!("invalid storage location: {e}")))?;
28        let location =
29            StorageLocation::parse(&raw).map_err(|e| Error::invalid_argument(e.to_string()))?;
30        Ok(Self { raw, location })
31    }
32
33    /// The URL as given.
34    pub fn raw(&self) -> &Url {
35        &self.raw
36    }
37
38    /// The canonical location.
39    pub fn location(&self) -> &StorageLocation {
40        &self.location
41    }
42
43    /// The scope a credential for this location covers.
44    pub fn scope(&self) -> StorageScope {
45        StorageScope::new(self.location.clone())
46    }
47
48    /// The object path within the bucket.
49    pub fn path(&self) -> &Path {
50        self.location.path()
51    }
52
53    /// Whether this is a local (`file://`) location.
54    pub fn is_local(&self) -> bool {
55        self.location.provider() == StorageProvider::Local
56    }
57
58    /// Whether `self` equals `prefix` or lies below it, on whole path segments.
59    pub fn starts_with(&self, prefix: &StorageLocationUrl) -> bool {
60        self.location.starts_with(&prefix.location)
61    }
62
63    /// Whether either location equals or lies below the other.
64    pub fn overlaps(&self, other: &StorageLocationUrl) -> bool {
65        self.starts_with(other) || other.starts_with(self)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    fn parse(url: &str) -> StorageLocationUrl {
74        StorageLocationUrl::parse(url).unwrap()
75    }
76
77    #[test]
78    fn overlap_is_segment_aware_and_symmetric() {
79        assert!(parse("s3://b/data").overlaps(&parse("s3://b/data/sub")));
80        assert!(parse("s3://b/data/sub").overlaps(&parse("s3://b/data")));
81        assert!(parse("s3://b/data").overlaps(&parse("s3://b/data/")));
82        assert!(!parse("s3://b/data").overlaps(&parse("s3://b/data-secret")));
83        assert!(!parse("s3://b/data").overlaps(&parse("s3://c/data")));
84    }
85
86    #[test]
87    fn azure_spellings_compare_canonically() {
88        let abfss = parse("abfss://container@account.dfs.core.windows.net/data/tab");
89        let https = parse("https://account.blob.core.windows.net/container/data");
90        assert!(abfss.starts_with(&https));
91        assert!(!https.starts_with(&abfss));
92    }
93
94    #[test]
95    fn local_locations_are_detected() {
96        assert!(parse("file:///tmp/data").is_local());
97        assert!(!parse("s3://b/data").is_local());
98        assert!(StorageLocationUrl::parse("not a url").is_err());
99    }
100}