1use crate::store_io::{atomic_write_json, remove_if_exists, sync_parent_directory};
14use crate::stream::{receive_artifact_from, ArtifactTransferPolicy};
15use crate::{
16 ArtifactAuthenticityVerifier, ArtifactDescriptor, ArtifactSource, ArtifactWriter, UpdateError,
17 UpdateResult,
18};
19use fs2::FileExt;
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use std::fs::{self, File, OpenOptions};
23use std::io::{Read, Seek, SeekFrom, Write};
24use std::path::{Path, PathBuf};
25
26const CACHE_MAX_DESCRIPTOR_BYTES: usize = 1024 * 1024;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct CacheOptions {
31 pub max_bytes: u64,
33 pub secure_permissions: bool,
35}
36
37impl CacheOptions {
38 pub fn new(max_bytes: u64, secure_permissions: bool) -> UpdateResult<Self> {
40 if max_bytes == 0 {
41 return Err(UpdateError::Store(
42 "cache max_bytes must be greater than zero".to_string(),
43 ));
44 }
45 Ok(Self {
46 max_bytes,
47 secure_permissions,
48 })
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct CachedArtifact {
55 pub descriptor: ArtifactDescriptor,
57 pub artifact_path: PathBuf,
59 pub descriptor_path: PathBuf,
61}
62
63#[derive(Debug, Clone)]
65pub struct UpdateCache {
66 root: PathBuf,
67 options: CacheOptions,
68}
69
70#[derive(Debug, Serialize)]
71struct CachedDescriptor<'a> {
72 descriptor: &'a ArtifactDescriptor,
73}
74
75#[derive(Debug, Deserialize)]
76struct StoredDescriptor {
77 descriptor: ArtifactDescriptor,
78}
79
80impl UpdateCache {
81 pub fn open(root: impl Into<PathBuf>, options: CacheOptions) -> UpdateResult<Self> {
83 let cache = Self {
84 root: root.into(),
85 options,
86 };
87 cache.initialize()?;
88 Ok(cache)
89 }
90
91 pub fn stage(
93 &self,
94 verifier: &dyn ArtifactAuthenticityVerifier,
95 descriptor: &ArtifactDescriptor,
96 source: &dyn ArtifactSource,
97 transfer: &ArtifactTransferPolicy,
98 ) -> UpdateResult<CachedArtifact> {
99 descriptor.validate()?;
100 verifier.verify(descriptor)?;
101 let _lock = self.lock()?;
102 self.initialize()?;
103 let artifact_path = self.artifact_path(descriptor);
104 let descriptor_path = self.descriptor_path(descriptor);
105 if self.is_complete(descriptor, &artifact_path, &descriptor_path)? {
106 return Ok(CachedArtifact {
107 descriptor: descriptor.clone(),
108 artifact_path,
109 descriptor_path,
110 });
111 }
112 remove_if_exists(&artifact_path)?;
113 remove_if_exists(&descriptor_path)?;
114 let part_path = self.part_path(descriptor);
115 let partial_len = fs::metadata(&part_path)
116 .map(|metadata| metadata.len())
117 .unwrap_or(0);
118 let result = self.receive_partial(descriptor, source, transfer);
119 if matches!(result, Err(UpdateError::ChecksumMismatch)) && partial_len > 0 {
120 remove_if_exists(&part_path)?;
121 self.receive_partial(descriptor, source, transfer)?;
122 } else {
123 result?;
124 }
125 if fs::metadata(&part_path).map_err(store_error)?.len() != descriptor.size_bytes() {
126 return Err(UpdateError::ChecksumMismatch);
127 }
128 fs::rename(&part_path, &artifact_path).map_err(store_error)?;
129 sync_parent_directory(&self.objects_dir())?;
130 atomic_write_json(&descriptor_path, &CachedDescriptor { descriptor })?;
131 Ok(CachedArtifact {
132 descriptor: descriptor.clone(),
133 artifact_path,
134 descriptor_path,
135 })
136 }
137
138 pub fn artifact_path(&self, descriptor: &ArtifactDescriptor) -> PathBuf {
140 self.objects_dir()
141 .join(format!("{}.bin", descriptor.sha256()))
142 }
143
144 pub fn descriptor_path(&self, descriptor: &ArtifactDescriptor) -> PathBuf {
146 self.objects_dir()
147 .join(format!("{}.json", descriptor.sha256()))
148 }
149
150 pub fn partial_path(&self, descriptor: &ArtifactDescriptor) -> PathBuf {
152 self.part_path(descriptor)
153 }
154
155 fn initialize(&self) -> UpdateResult<()> {
156 let root_created = ensure_directory(&self.root)?;
157 let objects = self.objects_dir();
158 let parts = self.parts_dir();
159 let objects_created = ensure_directory(&objects)?;
160 let parts_created = ensure_directory(&parts)?;
161 if self.options.secure_permissions {
162 if root_created {
163 set_private_directory_permissions(&self.root)?;
164 }
165 if objects_created {
166 set_private_directory_permissions(&objects)?;
167 }
168 if parts_created {
169 set_private_directory_permissions(&parts)?;
170 }
171 validate_private_directory(&self.root)?;
172 validate_private_directory(&objects)?;
173 validate_private_directory(&parts)?;
174 validate_private_ancestors(&self.root)?;
175 }
176 Ok(())
177 }
178
179 fn lock(&self) -> UpdateResult<File> {
180 let path = self.root.join("cache.lock");
181 if let Ok(metadata) = fs::symlink_metadata(&path) {
182 if metadata.file_type().is_symlink() || !metadata.is_file() {
183 return Err(UpdateError::Store(
184 "cache lock path is not a regular file".to_string(),
185 ));
186 }
187 }
188 let file = OpenOptions::new()
189 .create(true)
190 .read(true)
191 .write(true)
192 .truncate(false)
193 .open(path)
194 .map_err(store_error)?;
195 file.try_lock_exclusive()
196 .map_err(|error| UpdateError::Store(format!("cache lock is unavailable: {error}")))?;
197 Ok(file)
198 }
199
200 fn is_complete(
201 &self,
202 descriptor: &ArtifactDescriptor,
203 artifact_path: &Path,
204 descriptor_path: &Path,
205 ) -> UpdateResult<bool> {
206 let stored: StoredDescriptor =
207 match crate::store_io::read_json_bounded(descriptor_path, CACHE_MAX_DESCRIPTOR_BYTES) {
208 Ok(Some(value)) => value,
209 Ok(None) => return Ok(false),
210 Err(crate::store_io::JsonReadError::Io(error)) => {
211 let _ = remove_if_exists(descriptor_path);
212 return Err(UpdateError::Store(error.to_string()));
213 }
214 Err(crate::store_io::JsonReadError::Decode(error)) => {
215 let _ = remove_if_exists(descriptor_path);
216 return Err(UpdateError::Store(error.to_string()));
217 }
218 };
219 let artifact_metadata = match fs::symlink_metadata(artifact_path) {
220 Ok(metadata) => metadata,
221 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
222 Err(error) => return Err(store_error(error)),
223 };
224 if stored.descriptor != *descriptor
225 || artifact_metadata.file_type().is_symlink()
226 || !artifact_metadata.is_file()
227 {
228 return Ok(false);
229 }
230 match verify_cached_artifact(artifact_path, descriptor) {
231 Ok(()) => {}
232 Err(UpdateError::ChecksumMismatch) => {
233 remove_if_exists(artifact_path)?;
234 remove_if_exists(descriptor_path)?;
235 return Ok(false);
236 }
237 Err(error) => return Err(error),
238 }
239 Ok(true)
240 }
241
242 fn receive_partial(
243 &self,
244 descriptor: &ArtifactDescriptor,
245 source: &dyn ArtifactSource,
246 transfer: &ArtifactTransferPolicy,
247 ) -> UpdateResult<()> {
248 let part_path = self.part_path(descriptor);
249 let mut partial = self.open_partial(&part_path, descriptor)?;
250 let partial_len = partial.metadata().map_err(store_error)?.len();
251 let mut hasher = Sha256::new();
252 hash_existing(&mut partial, partial_len, &mut hasher)?;
253 let metadata_bytes = serde_json::to_vec(&CachedDescriptor { descriptor })
254 .map_err(|error| UpdateError::Store(error.to_string()))?
255 .len() as u64;
256 let required = descriptor
257 .size_bytes()
258 .checked_sub(partial_len)
259 .and_then(|bytes| bytes.checked_add(metadata_bytes))
260 .ok_or(UpdateError::ChecksumMismatch)?;
261 if self.usage()?.saturating_add(required) > self.options.max_bytes {
262 return Err(UpdateError::Store(
263 "update cache quota exceeded".to_string(),
264 ));
265 }
266 let mut writer = PartialWriter { file: partial };
267 let result = receive_artifact_from(
268 transfer,
269 descriptor,
270 source,
271 &mut writer,
272 partial_len,
273 hasher,
274 );
275 let mut partial = writer.file;
276 partial.flush().map_err(store_error)?;
277 partial.sync_all().map_err(store_error)?;
278 result
279 }
280
281 fn open_partial(&self, path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<File> {
282 if let Ok(metadata) = fs::symlink_metadata(path) {
283 if metadata.file_type().is_symlink() || !metadata.is_file() {
284 return Err(UpdateError::Store(
285 "cache partial path is not a regular file".to_string(),
286 ));
287 }
288 if metadata.len() > descriptor.size_bytes() {
289 remove_if_exists(path)?;
290 }
291 }
292 OpenOptions::new()
293 .create(true)
294 .read(true)
295 .write(true)
296 .truncate(false)
297 .open(path)
298 .map_err(store_error)
299 }
300
301 fn usage(&self) -> UpdateResult<u64> {
302 let mut total = 0_u64;
303 for directory in [self.objects_dir(), self.parts_dir()] {
304 for entry in fs::read_dir(directory).map_err(store_error)? {
305 let entry = entry.map_err(store_error)?;
306 let metadata = entry.metadata().map_err(store_error)?;
307 if !metadata.is_file() {
308 return Err(UpdateError::Store(
309 "cache contains a non-regular entry".to_string(),
310 ));
311 }
312 total = total
313 .checked_add(metadata.len())
314 .ok_or_else(|| UpdateError::Store("cache usage overflow".to_string()))?;
315 }
316 }
317 Ok(total)
318 }
319
320 fn objects_dir(&self) -> PathBuf {
321 self.root.join("objects")
322 }
323
324 fn parts_dir(&self) -> PathBuf {
325 self.root.join("parts")
326 }
327
328 fn part_path(&self, descriptor: &ArtifactDescriptor) -> PathBuf {
329 self.parts_dir()
330 .join(format!("{}.part", descriptor.sha256()))
331 }
332}
333
334struct PartialWriter {
335 file: File,
336}
337
338impl ArtifactWriter for PartialWriter {
339 fn write_chunk(&mut self, offset: u64, bytes: &[u8]) -> UpdateResult<()> {
340 self.file
341 .seek(SeekFrom::Start(offset))
342 .and_then(|_| self.file.write_all(bytes))
343 .map_err(store_error)
344 }
345}
346
347fn hash_existing(file: &mut File, length: u64, hasher: &mut Sha256) -> UpdateResult<()> {
348 file.seek(SeekFrom::Start(0)).map_err(store_error)?;
349 let mut remaining = length;
350 let mut buffer = vec![0_u8; 64 * 1024];
351 while remaining > 0 {
352 let requested = usize::try_from(remaining)
353 .unwrap_or(buffer.len())
354 .min(buffer.len());
355 let read = file.read(&mut buffer[..requested]).map_err(store_error)?;
356 if read == 0 {
357 return Err(UpdateError::ChecksumMismatch);
358 }
359 hasher.update(&buffer[..read]);
360 remaining -= read as u64;
361 }
362 Ok(())
363}
364
365fn verify_cached_artifact(path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<()> {
366 let mut file = File::open(path).map_err(store_error)?;
367 if file.metadata().map_err(store_error)?.len() != descriptor.size_bytes() {
368 return Err(UpdateError::ChecksumMismatch);
369 }
370 let mut hasher = Sha256::new();
371 let mut buffer = vec![0_u8; 64 * 1024];
372 loop {
373 let read = file.read(&mut buffer).map_err(store_error)?;
374 if read == 0 {
375 break;
376 }
377 hasher.update(&buffer[..read]);
378 }
379 if crate::integrity::encode_hex(&hasher.finalize()) != descriptor.sha256() {
380 return Err(UpdateError::ChecksumMismatch);
381 }
382 Ok(())
383}
384
385fn reject_directory(path: &Path) -> UpdateResult<()> {
386 let metadata = fs::symlink_metadata(path).map_err(store_error)?;
387 if metadata.file_type().is_symlink() || !metadata.is_dir() {
388 return Err(UpdateError::Store(
389 "cache path is not a regular directory".to_string(),
390 ));
391 }
392 Ok(())
393}
394
395fn ensure_directory(path: &Path) -> UpdateResult<bool> {
396 match fs::symlink_metadata(path) {
397 Ok(metadata) => {
398 if metadata.file_type().is_symlink() || !metadata.is_dir() {
399 return Err(UpdateError::Store(
400 "cache path is not a regular directory".to_string(),
401 ));
402 }
403 Ok(false)
404 }
405 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
406 fs::create_dir_all(path).map_err(store_error)?;
407 reject_directory(path)?;
408 Ok(true)
409 }
410 Err(error) => Err(store_error(error)),
411 }
412}
413
414fn store_error(error: std::io::Error) -> UpdateError {
415 UpdateError::Store(error.to_string())
416}
417
418fn set_private_directory_permissions(path: &Path) -> UpdateResult<()> {
419 #[cfg(unix)]
420 {
421 use std::os::unix::fs::PermissionsExt;
422 fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(store_error)?;
423 }
424 Ok(())
425}
426
427fn validate_private_directory(path: &Path) -> UpdateResult<()> {
428 #[cfg(unix)]
429 {
430 use std::os::unix::fs::{MetadataExt, PermissionsExt};
431 let metadata = fs::symlink_metadata(path).map_err(store_error)?;
432 if metadata.file_type().is_symlink()
433 || !metadata.is_dir()
434 || metadata.uid() != unsafe { libc::geteuid() }
435 || metadata.permissions().mode() & 0o077 != 0
436 {
437 return Err(UpdateError::Store(
438 "cache directory permissions are not owner-only".to_string(),
439 ));
440 }
441 }
442 Ok(())
443}
444
445fn validate_private_ancestors(path: &Path) -> UpdateResult<()> {
446 #[cfg(unix)]
447 {
448 use std::os::unix::fs::{MetadataExt, PermissionsExt};
449 let mut current = path.to_path_buf();
450 while let Some(parent) = current.parent() {
451 if parent == current {
452 break;
453 }
454 let metadata = fs::symlink_metadata(parent).map_err(store_error)?;
455 let mode = metadata.permissions().mode();
456 let safe_sticky_root = mode & 0o1000 != 0 && metadata.uid() == 0;
457 if metadata.file_type().is_symlink()
458 || !metadata.is_dir()
459 || (mode & 0o022 != 0 && !safe_sticky_root)
460 {
461 return Err(UpdateError::Store(
462 "cache ancestor is writable by an untrusted principal".to_string(),
463 ));
464 }
465 current = parent.to_path_buf();
466 }
467 }
468 Ok(())
469}