starfield_datastore/
artifact.rs1use crate::{ContentCheck, DatastoreError, Result};
2
3#[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}