1use crate::filesystem::read_regular_file_bounded;
12use crate::{sha256_hex, ArtifactDescriptor, UpdateError, UpdateResult};
13use appcore_contracts::BuildId;
14use serde::{Deserialize, Serialize};
15#[cfg(unix)]
16use std::fs::File;
17use std::fs::{self, OpenOptions};
18use std::io::Write;
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicU64, Ordering};
21
22pub const UPDATE_METADATA_FORMAT_VERSION: u16 = 1;
24static UPDATE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct StagedArtifact {
30 pub descriptor: ArtifactDescriptor,
32 pub staging_reference: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct ActivationReceipt {
39 pub activated: ArtifactDescriptor,
41 pub previous: Option<ArtifactDescriptor>,
43}
44
45pub trait ArtifactStore: Send + Sync {
47 fn recover(&self) -> UpdateResult<()> {
51 Ok(())
52 }
53 fn current(&self) -> UpdateResult<Option<ArtifactDescriptor>>;
55 fn stage(&self, descriptor: &ArtifactDescriptor, bytes: &[u8]) -> UpdateResult<StagedArtifact>;
57 fn discard_staged(&self, _staged: &StagedArtifact) -> UpdateResult<()> {
59 Ok(())
60 }
61 fn activate(&self, staged: StagedArtifact) -> UpdateResult<ActivationReceipt>;
63 fn rollback(&self, receipt: &ActivationReceipt) -> UpdateResult<()>;
65 fn commit(&self, receipt: &ActivationReceipt) -> UpdateResult<()>;
67}
68
69#[derive(Debug, Clone)]
71pub struct FileArtifactStore {
72 root: PathBuf,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76struct ArtifactPointer {
77 format_version: u16,
78 descriptor: ArtifactDescriptor,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82struct PendingActivationRecord {
83 format_version: u16,
84 receipt: ActivationReceipt,
85}
86
87impl FileArtifactStore {
88 pub fn new(root: impl Into<PathBuf>) -> Self {
90 Self { root: root.into() }
91 }
92
93 pub fn artifact_path(&self, build_id: &BuildId) -> PathBuf {
95 self.root
96 .join("artifacts")
97 .join(format!("{}.artifact", build_id.as_str()))
98 }
99
100 pub fn staged_artifact_path(&self, staged: &StagedArtifact) -> PathBuf {
102 self.staged_path(staged.descriptor.build_id())
103 }
104
105 pub fn pending_activation_receipt(&self) -> UpdateResult<Option<ActivationReceipt>> {
107 self.read_pending_activation()
108 }
109
110 fn staged_path(&self, build_id: &BuildId) -> PathBuf {
111 self.root
112 .join("staged")
113 .join(format!("{}.artifact", build_id.as_str()))
114 }
115
116 fn active_pointer(&self) -> PathBuf {
117 self.root.join("active.json")
118 }
119
120 fn previous_pointer(&self) -> PathBuf {
121 self.root.join("previous.json")
122 }
123
124 fn pending_activation(&self) -> PathBuf {
125 self.root.join("pending-activation.json")
126 }
127
128 fn initialize(&self) -> UpdateResult<()> {
129 fs::create_dir_all(self.root.join("artifacts"))
130 .and_then(|_| fs::create_dir_all(self.root.join("staged")))
131 .map_err(|error| UpdateError::Store(error.to_string()))?;
132 reject_directory(&self.root)?;
133 reject_directory(&self.root.join("artifacts"))?;
134 reject_directory(&self.root.join("staged"))
135 }
136
137 fn read_pointer(&self, path: &Path) -> UpdateResult<Option<ArtifactDescriptor>> {
138 let bytes = match read_regular_file_bounded(path, 1_048_576) {
139 Ok(bytes) => bytes,
140 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
141 Err(error) => return Err(UpdateError::Store(error.to_string())),
142 };
143 let pointer: ArtifactPointer = serde_json::from_slice(&bytes)
144 .map_err(|error| UpdateError::Store(error.to_string()))?;
145 if pointer.format_version != UPDATE_METADATA_FORMAT_VERSION {
146 return Err(UpdateError::Store(
147 "unsupported artifact pointer format".to_string(),
148 ));
149 }
150 pointer.descriptor.validate()?;
151 Ok(Some(pointer.descriptor))
152 }
153
154 fn write_pointer(&self, path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<()> {
155 let pointer = ArtifactPointer {
156 format_version: UPDATE_METADATA_FORMAT_VERSION,
157 descriptor: descriptor.clone(),
158 };
159 let bytes = serde_json::to_vec_pretty(&pointer)
160 .map_err(|error| UpdateError::Store(error.to_string()))?;
161 atomic_write(path, &bytes)
162 }
163}
164
165impl ArtifactStore for FileArtifactStore {
166 fn recover(&self) -> UpdateResult<()> {
167 let Some(receipt) = self.read_pending_activation()? else {
168 remove_if_exists(&self.previous_pointer())?;
169 return Ok(());
170 };
171 match self.current()? {
172 Some(current) if current.build_id() == receipt.activated.build_id() => {
173 self.rollback(&receipt)
174 }
175 _ => {
176 remove_if_exists(&self.previous_pointer())?;
177 remove_if_exists(&self.pending_activation())
178 }
179 }
180 }
181
182 fn current(&self) -> UpdateResult<Option<ArtifactDescriptor>> {
183 self.read_pointer(&self.active_pointer())
184 }
185
186 fn stage(&self, descriptor: &ArtifactDescriptor, bytes: &[u8]) -> UpdateResult<StagedArtifact> {
187 self.initialize()?;
188 if bytes.len() as u64 != descriptor.size_bytes() || sha256_hex(bytes) != descriptor.sha256()
189 {
190 return Err(UpdateError::ChecksumMismatch);
191 }
192 let path = self.staged_path(descriptor.build_id());
193 atomic_write(&path, bytes)?;
194 Ok(StagedArtifact {
195 descriptor: descriptor.clone(),
196 staging_reference: path.to_string_lossy().into_owned(),
197 })
198 }
199
200 fn discard_staged(&self, staged: &StagedArtifact) -> UpdateResult<()> {
201 let expected = self.staged_path(staged.descriptor.build_id());
202 if staged.staging_reference != expected.to_string_lossy() {
203 return Err(UpdateError::Store(
204 "staged artifact reference does not belong to this store".to_string(),
205 ));
206 }
207 remove_if_exists(&expected)
208 }
209
210 fn activate(&self, staged: StagedArtifact) -> UpdateResult<ActivationReceipt> {
211 self.activate_inner(staged, None)
212 }
213
214 fn rollback(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
215 let current = self.current()?.ok_or_else(|| {
216 UpdateError::Store("cannot rollback without an active artifact".to_string())
217 })?;
218 if current.build_id() != receipt.activated.build_id() {
219 return Err(UpdateError::Store(
220 "active artifact changed after activation".to_string(),
221 ));
222 }
223 match &receipt.previous {
224 Some(previous) => self.write_pointer(&self.active_pointer(), previous)?,
225 None => remove_if_exists(&self.active_pointer())?,
226 }
227 remove_if_exists(&self.previous_pointer())?;
228 remove_if_exists(&self.pending_activation())
229 }
230
231 fn commit(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
232 let current = self.current()?.ok_or_else(|| {
233 UpdateError::Store("cannot commit without an active artifact".to_string())
234 })?;
235 if current.build_id() != receipt.activated.build_id() {
236 return Err(UpdateError::Store(
237 "active artifact changed before commit".to_string(),
238 ));
239 }
240 remove_if_exists(&self.previous_pointer())?;
241 remove_if_exists(&self.pending_activation())
242 }
243}
244
245#[cfg_attr(not(test), allow(dead_code))]
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub(crate) enum StoreFaultPoint {
248 ArtifactMoved,
249 PreviousPointerWritten,
250 PendingReceiptWritten,
251 ActivePointerWritten,
252}
253
254impl FileArtifactStore {
255 fn activate_inner(
256 &self,
257 staged: StagedArtifact,
258 fault: Option<StoreFaultPoint>,
259 ) -> UpdateResult<ActivationReceipt> {
260 self.initialize()?;
261 let expected = self.staged_path(staged.descriptor.build_id());
262 if Path::new(&staged.staging_reference) != expected {
263 return Err(UpdateError::Store(
264 "staging reference does not belong to this store".to_string(),
265 ));
266 }
267 let previous = self.current()?;
268 let artifact_path = self.artifact_path(staged.descriptor.build_id());
269 verify_artifact(&expected, &staged.descriptor)?;
270 install_artifact(&expected, &artifact_path, &staged.descriptor)?;
271 sync_parent_directory(
272 artifact_path
273 .parent()
274 .ok_or_else(|| UpdateError::Store("artifact path has no parent".to_string()))?,
275 )?;
276 inject_store_fault(fault, StoreFaultPoint::ArtifactMoved)?;
277 if let Some(previous) = &previous {
278 self.write_pointer(&self.previous_pointer(), previous)?;
279 } else {
280 remove_if_exists(&self.previous_pointer())?;
281 }
282 inject_store_fault(fault, StoreFaultPoint::PreviousPointerWritten)?;
283 let receipt = ActivationReceipt {
284 activated: staged.descriptor.clone(),
285 previous,
286 };
287 self.write_pending_activation(&receipt)?;
288 inject_store_fault(fault, StoreFaultPoint::PendingReceiptWritten)?;
289 self.write_pointer(&self.active_pointer(), &staged.descriptor)?;
290 inject_store_fault(fault, StoreFaultPoint::ActivePointerWritten)?;
291 Ok(receipt)
292 }
293
294 #[cfg(test)]
295 pub(crate) fn activate_with_fault(
296 &self,
297 staged: StagedArtifact,
298 fault: StoreFaultPoint,
299 ) -> UpdateResult<ActivationReceipt> {
300 self.activate_inner(staged, Some(fault))
301 }
302
303 fn read_pending_activation(&self) -> UpdateResult<Option<ActivationReceipt>> {
304 let bytes = match read_bounded(&self.pending_activation(), 1_048_576)? {
305 Some(bytes) => bytes,
306 None => return Ok(None),
307 };
308 let record = serde_json::from_slice::<PendingActivationRecord>(&bytes)
309 .map_err(|_| UpdateError::Store("NO MORE SUPPORTED PLEASE UPDATE".to_string()))?;
310 if record.format_version != UPDATE_METADATA_FORMAT_VERSION {
311 return Err(UpdateError::Store(
312 "NO MORE SUPPORTED PLEASE UPDATE".to_string(),
313 ));
314 }
315 let receipt = record.receipt;
316 receipt.activated.validate()?;
317 if let Some(previous) = &receipt.previous {
318 previous.validate()?;
319 }
320 Ok(Some(receipt))
321 }
322
323 fn write_pending_activation(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
324 let record = PendingActivationRecord {
325 format_version: UPDATE_METADATA_FORMAT_VERSION,
326 receipt: receipt.clone(),
327 };
328 let bytes = serde_json::to_vec_pretty(&record)
329 .map_err(|error| UpdateError::Store(error.to_string()))?;
330 atomic_write(&self.pending_activation(), &bytes)
331 }
332}
333
334fn read_bounded(path: &Path, max_bytes: usize) -> UpdateResult<Option<Vec<u8>>> {
335 let bytes = match read_regular_file_bounded(path, max_bytes) {
336 Ok(bytes) => bytes,
337 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
338 Err(error) => return Err(UpdateError::Store(error.to_string())),
339 };
340 Ok(Some(bytes))
341}
342
343fn verify_artifact(path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<()> {
344 let max_bytes = usize::try_from(descriptor.size_bytes())
345 .map_err(|_| UpdateError::Store("artifact size exceeds this platform".to_string()))?;
346 let bytes = read_regular_file_bounded(path, max_bytes)
347 .map_err(|error| UpdateError::Store(error.to_string()))?;
348 if bytes.len() as u64 != descriptor.size_bytes() || sha256_hex(&bytes) != descriptor.sha256() {
349 return Err(UpdateError::ChecksumMismatch);
350 }
351 Ok(())
352}
353
354fn install_artifact(
355 staged_path: &Path,
356 artifact_path: &Path,
357 descriptor: &ArtifactDescriptor,
358) -> UpdateResult<()> {
359 match fs::hard_link(staged_path, artifact_path) {
360 Ok(()) => {}
361 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
362 verify_artifact(artifact_path, descriptor)?;
363 }
364 Err(error) => return Err(UpdateError::Store(error.to_string())),
365 }
366 remove_if_exists(staged_path)
367}
368
369fn atomic_write(path: &Path, bytes: &[u8]) -> UpdateResult<()> {
370 let parent = path
371 .parent()
372 .ok_or_else(|| UpdateError::Store("path has no parent".to_string()))?;
373 fs::create_dir_all(parent).map_err(|error| UpdateError::Store(error.to_string()))?;
374 reject_directory(parent)?;
375 reject_optional_regular_file(path)?;
376 let temporary = path.with_extension(format!(
377 "tmp-{}-{}",
378 std::process::id(),
379 UPDATE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
380 ));
381 let result = (|| {
382 let mut file = OpenOptions::new()
383 .create_new(true)
384 .write(true)
385 .open(&temporary)
386 .map_err(|error| UpdateError::Store(error.to_string()))?;
387 file.write_all(bytes)
388 .and_then(|_| file.sync_all())
389 .map_err(|error| UpdateError::Store(error.to_string()))?;
390 fs::rename(&temporary, path).map_err(|error| UpdateError::Store(error.to_string()))?;
391 sync_parent_directory(parent)
392 })();
393 if result.is_err() {
394 let _ = fs::remove_file(temporary);
395 }
396 result
397}
398
399#[cfg(unix)]
400fn sync_parent_directory(path: &Path) -> UpdateResult<()> {
401 File::open(path)
402 .and_then(|directory| directory.sync_all())
403 .map_err(|error| UpdateError::Store(error.to_string()))
404}
405
406#[cfg(not(unix))]
407fn sync_parent_directory(_path: &Path) -> UpdateResult<()> {
408 Ok(())
409}
410
411fn remove_if_exists(path: &Path) -> UpdateResult<()> {
412 match fs::remove_file(path) {
413 Ok(()) => path
414 .parent()
415 .map(sync_parent_directory)
416 .transpose()
417 .map(|_| ()),
418 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
419 Err(error) => Err(UpdateError::Store(error.to_string())),
420 }
421}
422
423fn inject_store_fault(
424 actual: Option<StoreFaultPoint>,
425 expected: StoreFaultPoint,
426) -> UpdateResult<()> {
427 if actual == Some(expected) {
428 return Err(UpdateError::Store(format!(
429 "injected store fault at {expected:?}"
430 )));
431 }
432 Ok(())
433}
434
435fn reject_optional_regular_file(path: &Path) -> UpdateResult<()> {
436 match fs::symlink_metadata(path) {
437 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => Err(
438 UpdateError::Store("update path is not a regular file".to_string()),
439 ),
440 Ok(_) => Ok(()),
441 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
442 Err(error) => Err(UpdateError::Store(error.to_string())),
443 }
444}
445
446fn reject_directory(path: &Path) -> UpdateResult<()> {
447 let metadata =
448 fs::symlink_metadata(path).map_err(|error| UpdateError::Store(error.to_string()))?;
449 if metadata.file_type().is_symlink() || !metadata.is_dir() {
450 return Err(UpdateError::Store(
451 "update root is not a regular directory".to_string(),
452 ));
453 }
454 Ok(())
455}