Skip to main content

starfield_datastore/
artifact.rs

1use crate::{ContentCheck, DatastoreError, Result};
2
3/// Stable, slash-separated logical identity, independent of archive URLs.
4#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
5pub struct ArtifactKey(String);
6
7impl ArtifactKey {
8    pub fn new(key: impl Into<String>) -> Result<Self> {
9        let key = key.into();
10        if key.is_empty()
11            || key.len() > 1024
12            || key
13                .split('/')
14                .any(|part| part.is_empty() || part == "." || part == ".." || part.len() > 240)
15            || !key
16                .bytes()
17                .all(|b| b.is_ascii_alphanumeric() || b"-._/".contains(&b))
18        {
19            return Err(DatastoreError::InvalidKey(
20                "expected nonempty safe path components".into(),
21            ));
22        }
23        Ok(Self(key))
24    }
25
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29
30    pub fn mirror_path(&self, prefix: &str) -> String {
31        let prefix = prefix.trim_matches('/');
32        if prefix.is_empty() {
33            self.0.clone()
34        } else {
35            format!("{prefix}/{}", self.0)
36        }
37    }
38}
39
40impl std::fmt::Display for ArtifactKey {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        self.0.fmt(f)
43    }
44}
45
46#[derive(Clone)]
47pub struct Source {
48    pub url: String,
49    pub trust_redirects: bool,
50}
51
52impl Source {
53    pub fn new(url: impl Into<String>) -> Self {
54        Self {
55            url: url.into(),
56            trust_redirects: false,
57        }
58    }
59    pub fn trusting_redirects(mut self) -> Self {
60        self.trust_redirects = true;
61        self
62    }
63}
64
65impl std::fmt::Debug for Source {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("Source")
68            .field("url", &"[redacted]")
69            .field("trust_redirects", &self.trust_redirects)
70            .finish()
71    }
72}
73
74#[derive(Debug, Clone, Default)]
75pub struct Provenance {
76    pub description: String,
77    pub license: String,
78    pub citation: Option<String>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum Freshness {
83    #[default]
84    Immutable,
85}
86
87#[derive(Debug, Clone)]
88pub struct Artifact {
89    pub key: ArtifactKey,
90    pub sources: Vec<Source>,
91    pub check: ContentCheck,
92    pub freshness: Freshness,
93    pub provenance: Provenance,
94    pub expected_bytes: Option<u64>,
95}
96
97impl Artifact {
98    pub fn new(key: ArtifactKey, sources: Vec<Source>) -> Self {
99        Self {
100            key,
101            sources,
102            check: ContentCheck::default_binary(),
103            freshness: Freshness::Immutable,
104            provenance: Provenance::default(),
105            expected_bytes: None,
106        }
107    }
108    pub fn with_check(mut self, check: ContentCheck) -> Self {
109        self.check = check;
110        self
111    }
112    pub fn with_freshness(mut self, freshness: Freshness) -> Self {
113        self.freshness = freshness;
114        self
115    }
116    pub fn with_provenance(mut self, provenance: Provenance) -> Self {
117        self.provenance = provenance;
118        self
119    }
120    pub fn with_expected_bytes(mut self, bytes: u64) -> Self {
121        self.expected_bytes = Some(bytes);
122        self
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    #[test]
130    fn keys_cannot_escape_the_index_or_encode_credentials() {
131        for key in [
132            "",
133            "/a",
134            "a/",
135            "a//b",
136            "a/../b",
137            "./a",
138            "a\\b",
139            "a?token=secret",
140            "https://host/file",
141            "a%2fb",
142            "a\n",
143        ] {
144            assert!(ArtifactKey::new(key).is_err(), "accepted {key:?}");
145        }
146        let key = ArtifactKey::new("naif/spk/de440.bsp").unwrap();
147        assert_eq!(key.mirror_path("/mirror/"), "mirror/naif/spk/de440.bsp");
148        assert_eq!(key.mirror_path(""), key.as_str());
149    }
150}