1use crate::filesystem::open_regular_file;
14use crate::store_io::{
15 atomic_write, atomic_write_json, read_json_bounded, reject_directory, remove_if_exists,
16 sync_parent_directory, JsonReadError, MAX_UPDATE_METADATA_BYTES,
17};
18use crate::{sha256_hex, ArtifactDescriptor, UpdateError, UpdateResult};
19use appcore_contracts::BuildId;
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use std::fs;
23use std::io::Read;
24use std::path::{Path, PathBuf};
25
26pub const UPDATE_METADATA_FORMAT_VERSION: u16 = 1;
28const ARTIFACT_HASH_BUFFER_BYTES: usize = 64 * 1024;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct StagedArtifact {
33 pub descriptor: ArtifactDescriptor,
35 pub staging_reference: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ActivationReceipt {
42 pub activated: ArtifactDescriptor,
44 pub previous: Option<ArtifactDescriptor>,
46}
47
48pub trait ArtifactStore: Send + Sync {
50 fn recover(&self) -> UpdateResult<()> {
54 Ok(())
55 }
56 fn current(&self) -> UpdateResult<Option<ArtifactDescriptor>>;
58 fn stage(&self, descriptor: &ArtifactDescriptor, bytes: &[u8]) -> UpdateResult<StagedArtifact>;
60 fn discard_staged(&self, _staged: &StagedArtifact) -> UpdateResult<()> {
62 Ok(())
63 }
64 fn activate(&self, staged: StagedArtifact) -> UpdateResult<ActivationReceipt>;
66 fn rollback(&self, receipt: &ActivationReceipt) -> UpdateResult<()>;
68 fn commit(&self, receipt: &ActivationReceipt) -> UpdateResult<()>;
70}
71
72#[derive(Debug, Clone)]
74pub struct FileArtifactStore {
75 root: PathBuf,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79struct ArtifactPointer {
80 format_version: u16,
81 descriptor: ArtifactDescriptor,
82}
83
84#[derive(Serialize)]
85struct ArtifactPointerRef<'a> {
86 format_version: u16,
87 descriptor: &'a ArtifactDescriptor,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91struct PendingActivationRecord {
92 format_version: u16,
93 receipt: ActivationReceipt,
94}
95
96#[derive(Serialize)]
97struct PendingActivationRecordRef<'a> {
98 format_version: u16,
99 receipt: &'a ActivationReceipt,
100}
101
102impl FileArtifactStore {
103 pub fn new(root: impl Into<PathBuf>) -> Self {
105 Self { root: root.into() }
106 }
107
108 pub fn artifact_path(&self, build_id: &BuildId) -> PathBuf {
110 self.root
111 .join("artifacts")
112 .join(format!("{}.artifact", build_id.as_str()))
113 }
114
115 pub fn staged_artifact_path(&self, staged: &StagedArtifact) -> PathBuf {
117 self.staged_path(staged.descriptor.build_id())
118 }
119
120 pub fn pending_activation_receipt(&self) -> UpdateResult<Option<ActivationReceipt>> {
122 self.read_pending_activation()
123 }
124
125 fn staged_path(&self, build_id: &BuildId) -> PathBuf {
126 self.root
127 .join("staged")
128 .join(format!("{}.artifact", build_id.as_str()))
129 }
130
131 fn active_pointer(&self) -> PathBuf {
132 self.root.join("active.json")
133 }
134
135 fn previous_pointer(&self) -> PathBuf {
136 self.root.join("previous.json")
137 }
138
139 fn pending_activation(&self) -> PathBuf {
140 self.root.join("pending-activation.json")
141 }
142
143 fn initialize(&self) -> UpdateResult<()> {
144 fs::create_dir_all(self.root.join("artifacts"))
145 .and_then(|_| fs::create_dir_all(self.root.join("staged")))
146 .map_err(|error| UpdateError::Store(error.to_string()))?;
147 reject_directory(&self.root)?;
148 reject_directory(&self.root.join("artifacts"))?;
149 reject_directory(&self.root.join("staged"))
150 }
151
152 fn read_pointer(&self, path: &Path) -> UpdateResult<Option<ArtifactDescriptor>> {
153 let pointer: ArtifactPointer = match read_json_bounded(path, MAX_UPDATE_METADATA_BYTES) {
154 Ok(Some(pointer)) => pointer,
155 Ok(None) => return Ok(None),
156 Err(JsonReadError::Io(error)) => {
157 return Err(UpdateError::Store(error.to_string()));
158 }
159 Err(JsonReadError::Decode(error)) => {
160 return Err(UpdateError::Store(error.to_string()));
161 }
162 };
163 if pointer.format_version != UPDATE_METADATA_FORMAT_VERSION {
164 return Err(UpdateError::Store(
165 "unsupported artifact pointer format".to_string(),
166 ));
167 }
168 pointer.descriptor.validate()?;
169 Ok(Some(pointer.descriptor))
170 }
171
172 fn write_pointer(&self, path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<()> {
173 let pointer = ArtifactPointerRef {
174 format_version: UPDATE_METADATA_FORMAT_VERSION,
175 descriptor,
176 };
177 atomic_write_json(path, &pointer)
178 }
179}
180
181impl ArtifactStore for FileArtifactStore {
182 fn recover(&self) -> UpdateResult<()> {
183 let Some(receipt) = self.read_pending_activation()? else {
184 remove_if_exists(&self.previous_pointer())?;
185 return Ok(());
186 };
187 match self.current()? {
188 Some(current) if current.build_id() == receipt.activated.build_id() => {
189 self.rollback(&receipt)
190 }
191 _ => {
192 remove_if_exists(&self.previous_pointer())?;
193 remove_if_exists(&self.pending_activation())
194 }
195 }
196 }
197
198 fn current(&self) -> UpdateResult<Option<ArtifactDescriptor>> {
199 self.read_pointer(&self.active_pointer())
200 }
201
202 fn stage(&self, descriptor: &ArtifactDescriptor, bytes: &[u8]) -> UpdateResult<StagedArtifact> {
203 self.initialize()?;
204 if bytes.len() as u64 != descriptor.size_bytes() || sha256_hex(bytes) != descriptor.sha256()
205 {
206 return Err(UpdateError::ChecksumMismatch);
207 }
208 let path = self.staged_path(descriptor.build_id());
209 atomic_write(&path, bytes)?;
210 Ok(StagedArtifact {
211 descriptor: descriptor.clone(),
212 staging_reference: path.to_string_lossy().into_owned(),
213 })
214 }
215
216 fn discard_staged(&self, staged: &StagedArtifact) -> UpdateResult<()> {
217 let expected = self.staged_path(staged.descriptor.build_id());
218 if staged.staging_reference != expected.to_string_lossy() {
219 return Err(UpdateError::Store(
220 "staged artifact reference does not belong to this store".to_string(),
221 ));
222 }
223 remove_if_exists(&expected)
224 }
225
226 fn activate(&self, staged: StagedArtifact) -> UpdateResult<ActivationReceipt> {
227 self.activate_inner(staged, None)
228 }
229
230 fn rollback(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
231 let current = self.current()?.ok_or_else(|| {
232 UpdateError::Store("cannot rollback without an active artifact".to_string())
233 })?;
234 if current.build_id() != receipt.activated.build_id() {
235 return Err(UpdateError::Store(
236 "active artifact changed after activation".to_string(),
237 ));
238 }
239 match &receipt.previous {
240 Some(previous) => self.write_pointer(&self.active_pointer(), previous)?,
241 None => remove_if_exists(&self.active_pointer())?,
242 }
243 remove_if_exists(&self.previous_pointer())?;
244 remove_if_exists(&self.pending_activation())
245 }
246
247 fn commit(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
248 let current = self.current()?.ok_or_else(|| {
249 UpdateError::Store("cannot commit without an active artifact".to_string())
250 })?;
251 if current.build_id() != receipt.activated.build_id() {
252 return Err(UpdateError::Store(
253 "active artifact changed before commit".to_string(),
254 ));
255 }
256 remove_if_exists(&self.previous_pointer())?;
257 remove_if_exists(&self.pending_activation())
258 }
259}
260
261#[cfg_attr(not(test), allow(dead_code))]
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub(crate) enum StoreFaultPoint {
264 ArtifactMoved,
265 PreviousPointerWritten,
266 PendingReceiptWritten,
267 ActivePointerWritten,
268}
269
270impl FileArtifactStore {
271 fn activate_inner(
272 &self,
273 staged: StagedArtifact,
274 fault: Option<StoreFaultPoint>,
275 ) -> UpdateResult<ActivationReceipt> {
276 self.initialize()?;
277 let expected = self.staged_path(staged.descriptor.build_id());
278 if Path::new(&staged.staging_reference) != expected {
279 return Err(UpdateError::Store(
280 "staging reference does not belong to this store".to_string(),
281 ));
282 }
283 let previous = self.current()?;
284 let artifact_path = self.artifact_path(staged.descriptor.build_id());
285 verify_artifact(&expected, &staged.descriptor)?;
286 install_artifact(&expected, &artifact_path, &staged.descriptor)?;
287 sync_parent_directory(
288 artifact_path
289 .parent()
290 .ok_or_else(|| UpdateError::Store("artifact path has no parent".to_string()))?,
291 )?;
292 inject_store_fault(fault, StoreFaultPoint::ArtifactMoved)?;
293 if let Some(previous) = &previous {
294 self.write_pointer(&self.previous_pointer(), previous)?;
295 } else {
296 remove_if_exists(&self.previous_pointer())?;
297 }
298 inject_store_fault(fault, StoreFaultPoint::PreviousPointerWritten)?;
299 let receipt = ActivationReceipt {
300 activated: staged.descriptor.clone(),
301 previous,
302 };
303 self.write_pending_activation(&receipt)?;
304 inject_store_fault(fault, StoreFaultPoint::PendingReceiptWritten)?;
305 self.write_pointer(&self.active_pointer(), &staged.descriptor)?;
306 inject_store_fault(fault, StoreFaultPoint::ActivePointerWritten)?;
307 Ok(receipt)
308 }
309
310 #[cfg(test)]
311 pub(crate) fn activate_with_fault(
312 &self,
313 staged: StagedArtifact,
314 fault: StoreFaultPoint,
315 ) -> UpdateResult<ActivationReceipt> {
316 self.activate_inner(staged, Some(fault))
317 }
318
319 fn read_pending_activation(&self) -> UpdateResult<Option<ActivationReceipt>> {
320 let record: PendingActivationRecord =
321 match read_json_bounded(&self.pending_activation(), MAX_UPDATE_METADATA_BYTES) {
322 Ok(Some(record)) => record,
323 Ok(None) => return Ok(None),
324 Err(JsonReadError::Io(error)) => {
325 return Err(UpdateError::Store(error.to_string()));
326 }
327 Err(JsonReadError::Decode(_)) => {
328 return Err(UpdateError::Store(
329 "NO MORE SUPPORTED PLEASE UPDATE".to_string(),
330 ));
331 }
332 };
333 if record.format_version != UPDATE_METADATA_FORMAT_VERSION {
334 return Err(UpdateError::Store(
335 "NO MORE SUPPORTED PLEASE UPDATE".to_string(),
336 ));
337 }
338 let receipt = record.receipt;
339 receipt.activated.validate()?;
340 if let Some(previous) = &receipt.previous {
341 previous.validate()?;
342 }
343 Ok(Some(receipt))
344 }
345
346 fn write_pending_activation(&self, receipt: &ActivationReceipt) -> UpdateResult<()> {
347 let record = PendingActivationRecordRef {
348 format_version: UPDATE_METADATA_FORMAT_VERSION,
349 receipt,
350 };
351 atomic_write_json(&self.pending_activation(), &record)
352 }
353}
354
355fn verify_artifact(path: &Path, descriptor: &ArtifactDescriptor) -> UpdateResult<()> {
356 let mut file =
357 open_regular_file(path).map_err(|error| UpdateError::Store(error.to_string()))?;
358 if file
359 .metadata()
360 .map_err(|error| UpdateError::Store(error.to_string()))?
361 .len()
362 != descriptor.size_bytes()
363 {
364 return Err(UpdateError::ChecksumMismatch);
365 }
366 let mut buffer = vec![0_u8; ARTIFACT_HASH_BUFFER_BYTES];
367 let mut hasher = Sha256::new();
368 let mut size = 0_u64;
369 loop {
370 let read = file
371 .read(&mut buffer)
372 .map_err(|error| UpdateError::Store(error.to_string()))?;
373 if read == 0 {
374 break;
375 }
376 size = size
377 .checked_add(read as u64)
378 .ok_or(UpdateError::ChecksumMismatch)?;
379 if size > descriptor.size_bytes() {
380 return Err(UpdateError::ChecksumMismatch);
381 }
382 hasher.update(&buffer[..read]);
383 }
384 let digest = format!("{:x}", hasher.finalize());
385 if size != descriptor.size_bytes() || digest != descriptor.sha256() {
386 return Err(UpdateError::ChecksumMismatch);
387 }
388 Ok(())
389}
390
391fn install_artifact(
392 staged_path: &Path,
393 artifact_path: &Path,
394 descriptor: &ArtifactDescriptor,
395) -> UpdateResult<()> {
396 match fs::hard_link(staged_path, artifact_path) {
397 Ok(()) => {}
398 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
399 verify_artifact(artifact_path, descriptor)?;
400 }
401 Err(error) => return Err(UpdateError::Store(error.to_string())),
402 }
403 remove_if_exists(staged_path)
404}
405
406fn inject_store_fault(
407 actual: Option<StoreFaultPoint>,
408 expected: StoreFaultPoint,
409) -> UpdateResult<()> {
410 if actual == Some(expected) {
411 return Err(UpdateError::Store(format!(
412 "injected store fault at {expected:?}"
413 )));
414 }
415 Ok(())
416}