auths_transparency/
fs_store.rs1use std::path::PathBuf;
2
3use crate::error::TransparencyError;
4use crate::store::TileStore;
5use crate::writer::LogSigningKey;
6
7const LOG_KEY_FILE: &str = "log.key";
9
10pub struct FsTileStore {
26 base_path: PathBuf,
27}
28
29impl FsTileStore {
30 pub fn new(base_path: PathBuf) -> Self {
40 Self { base_path }
41 }
42
43 pub fn ensure_base_dir(&self) -> Result<(), TransparencyError> {
53 std::fs::create_dir_all(&self.base_path)
54 .map_err(|e| TransparencyError::StoreError(e.to_string()))
55 }
56
57 pub fn load_or_create_key(
73 &self,
74 create: bool,
75 ) -> Result<Option<LogSigningKey>, TransparencyError> {
76 let path = self.base_path.join(LOG_KEY_FILE);
77 match std::fs::read(&path) {
78 Ok(der) => LogSigningKey::from_pkcs8_der(&der).map(Some),
79 Err(e) if e.kind() == std::io::ErrorKind::NotFound && create => {
80 let key = LogSigningKey::generate()?;
81 let der = key.to_pkcs8_der()?;
82 std::fs::write(&path, &der)
83 .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
84 #[cfg(unix)]
85 {
86 use std::os::unix::fs::PermissionsExt;
87 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
88 .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
89 }
90 Ok(Some(key))
91 }
92 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
93 Err(e) => Err(TransparencyError::StoreError(e.to_string())),
94 }
95 }
96}
97
98fn is_partial_tile(path: &str) -> bool {
99 path.contains(".p/")
100}
101
102#[async_trait::async_trait]
103impl TileStore for FsTileStore {
104 async fn read_tile(&self, path: &str) -> Result<Vec<u8>, TransparencyError> {
105 let full_path = self.base_path.join(path);
106 tokio::fs::read(&full_path)
107 .await
108 .map_err(|e| TransparencyError::StoreError(e.to_string()))
109 }
110
111 async fn write_tile(&self, path: &str, data: &[u8]) -> Result<(), TransparencyError> {
112 let full_path = self.base_path.join(path);
113
114 if !is_partial_tile(path) && full_path.exists() {
115 return Ok(());
116 }
117
118 if let Some(parent) = full_path.parent() {
119 tokio::fs::create_dir_all(parent)
120 .await
121 .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
122 }
123
124 tokio::fs::write(&full_path, data)
125 .await
126 .map_err(|e| TransparencyError::StoreError(e.to_string()))
127 }
128
129 async fn read_checkpoint(&self) -> Result<Option<Vec<u8>>, TransparencyError> {
130 let path = self.base_path.join("checkpoint");
131 match tokio::fs::read(&path).await {
132 Ok(data) => Ok(Some(data)),
133 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
134 Err(e) => Err(TransparencyError::StoreError(e.to_string())),
135 }
136 }
137
138 async fn write_checkpoint(&self, data: &[u8]) -> Result<(), TransparencyError> {
139 let path = self.base_path.join("checkpoint");
140
141 if let Some(parent) = path.parent() {
142 tokio::fs::create_dir_all(parent)
143 .await
144 .map_err(|e| TransparencyError::StoreError(e.to_string()))?;
145 }
146
147 tokio::fs::write(&path, data)
148 .await
149 .map_err(|e| TransparencyError::StoreError(e.to_string()))
150 }
151}
152
153#[cfg(test)]
154#[allow(clippy::unwrap_used, clippy::expect_used)]
155mod tests {
156 use super::*;
157
158 #[tokio::test]
159 async fn tile_write_read_roundtrip() {
160 let dir = tempfile::tempdir().unwrap();
161 let store = FsTileStore::new(dir.path().to_path_buf());
162
163 let data = b"leaf data";
164 store.write_tile("tile/0/000", data).await.unwrap();
165
166 let read_back = store.read_tile("tile/0/000").await.unwrap();
167 assert_eq!(read_back, data);
168 }
169
170 #[tokio::test]
171 async fn tile_creates_nested_directories() {
172 let dir = tempfile::tempdir().unwrap();
173 let store = FsTileStore::new(dir.path().to_path_buf());
174
175 store.write_tile("tile/2/001/002", b"deep").await.unwrap();
176
177 let on_disk = dir.path().join("tile/2/001/002");
178 assert!(on_disk.exists());
179 }
180
181 #[tokio::test]
182 async fn full_tile_is_immutable() {
183 let dir = tempfile::tempdir().unwrap();
184 let store = FsTileStore::new(dir.path().to_path_buf());
185
186 store.write_tile("tile/0/000", b"first").await.unwrap();
187 store.write_tile("tile/0/000", b"second").await.unwrap();
188
189 let data = store.read_tile("tile/0/000").await.unwrap();
190 assert_eq!(data, b"first");
191 }
192
193 #[tokio::test]
194 async fn partial_tile_is_overwritable() {
195 let dir = tempfile::tempdir().unwrap();
196 let store = FsTileStore::new(dir.path().to_path_buf());
197
198 store
199 .write_tile("tile/0/000.p/5", b"partial-v1")
200 .await
201 .unwrap();
202 store
203 .write_tile("tile/0/000.p/5", b"partial-v2")
204 .await
205 .unwrap();
206
207 let data = store.read_tile("tile/0/000.p/5").await.unwrap();
208 assert_eq!(data, b"partial-v2");
209 }
210
211 #[tokio::test]
212 async fn checkpoint_roundtrip() {
213 let dir = tempfile::tempdir().unwrap();
214 let store = FsTileStore::new(dir.path().to_path_buf());
215
216 let result = store.read_checkpoint().await.unwrap();
217 assert!(result.is_none());
218
219 store.write_checkpoint(b"cp-v1").await.unwrap();
220 let data = store.read_checkpoint().await.unwrap();
221 assert_eq!(data, Some(b"cp-v1".to_vec()));
222
223 store.write_checkpoint(b"cp-v2").await.unwrap();
224 let data = store.read_checkpoint().await.unwrap();
225 assert_eq!(data, Some(b"cp-v2".to_vec()));
226 }
227
228 #[tokio::test]
229 async fn read_nonexistent_tile_returns_error() {
230 let dir = tempfile::tempdir().unwrap();
231 let store = FsTileStore::new(dir.path().to_path_buf());
232
233 let result = store.read_tile("tile/0/999").await;
234 assert!(result.is_err());
235 }
236}