routinator/store.rs
1//! A store for RPKI objects.
2//!
3//! To be more resistant against accidental or malicious errors in the data
4//! published by repositories, we retain a separate copy of all RPKI data that
5//! has been found to be covered by a valid manifest in what we call the
6//! _store._ The types in this module provide access to this store.
7//!
8//! The store is initialized and configured via [`Store`]. During validation,
9//! [`Run`] is used which can be acquired from the store via the
10//! [`start`][Store::start] method. It provides access to the trust anchor
11//! certificates via the [`load_ta`][Run::load_ta] and
12//! [`update_ta`][Run::update_ta] methods, and access to individual
13//! repositories and publication points via [`repository`][Run::repository]
14//! and [`pub_point`][Run::pub_point], respectively. These are represented
15//! by the [`Repository`] and [`StoredPoint`] types.
16//!
17//! # Error Handling
18//!
19//! Pretty much all methods and functions provided by this module can return
20//! an error. This is because the store uses files and that can go wrong in
21//! all kinds of ways at any time. The concrete error reason is logged and our
22//! generic [`Failed`][crate::error::Failed] is returned. When this happens,
23//! the store should be considered broken and not be used anymore.
24//!
25//! # Data Storage
26//!
27//! The store uses the file system to store its data. It has its dedicated
28//! directory within the RPKI repository directory, normally named `stored`
29//! (this is because an earlier version used `store` already). Within this
30//! directory are four sub-directories: `rrdp` and `rsync` contain the data
31//! for each stored publication point; `ta` contains the downloaded trust
32//! anchor certificates; and `tmp` is a directory for storing files as they
33//! are constructed.
34//!
35//! All publication points that do not support RRDP are stored under `rsync`.
36//! Each has a file stored at a path and file name derived from the
37//! signedObject URI of its manifest, starting with the authority part of the
38//! URI and then just following along. The file contains status information,
39//! the manifest, the CRL, and each object. It starts with a serialized
40//! [`StoredPointHeader`] which is primarily used to mark points requested
41//! but never successfully retrieved. If a point was successfully retrieved,
42//! the header is followed by a [`StoredManifest`] which in turn is followed
43//! by a sequence of serialized [`StoredObject`]s for all the objects as
44//! given on the manifest.
45//!
46//! All publication points that are hosted in an RRDP repository are stored
47//! under `rrdp`, independently of whether they have been retrieved via RRDP
48//! or rsync. Directly under `rrdp` is a set of directories for all the
49//! authorities (i.e., host names) of the RRDP servers seen. Within each of
50//! these is a set of directories named after the SHA-256 hash of the
51//! rpkiNotify URI of the RRDP repository. These directories in turn contain
52//! the same files for each publication point as in the rsync case above. They
53//! are similarly stored at a path and file name derived from the signedObject
54//! URI of the manifest with the `rsync` scheme used as the first component
55//! instead. (There is no good reason for that, it just happened.)
56//!
57//! Trust anchor certficates are stored under `ta` using a three level
58//! directory structure derived from the URI the certificate is retrieved
59//! from. The first level is the scheme, `https` or `rsync`, the second
60//! level is the authority (i.e., hostname), and the third is the SHA-256
61//! hash of the full URI with an extension of `.cer` added.
62//!
63//! Finally, the `tmp` directory is used to build the publication point files
64//! in so they can be constructed without yet knowing whether the update is
65//! actually complete and correct. File names here are named using eight
66//! random hex-digits.
67
68
69use std::{fs, io};
70use std::fs::File;
71use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write};
72use std::path::{Path, PathBuf};
73use bytes::Bytes;
74use log::error;
75use rpki::uri;
76use rpki::crypto::DigestAlgorithm;
77use rpki::repository::cert::{Cert, ResourceCert};
78use rpki::repository::manifest::{ManifestContent, ManifestHash};
79use rpki::repository::tal::TalUri;
80use rpki::repository::x509::{Serial, Time};
81use tempfile::NamedTempFile;
82use crate::collector;
83use crate::config::Config;
84use crate::engine::CaCert;
85use crate::error::{Failed, Fatal, RunFailed};
86use crate::metrics::Metrics;
87use crate::utils::fatal;
88use crate::utils::binio::{Compose, Parse, ParseError};
89use crate::utils::dump::DumpRegistry;
90use crate::utils::json::JsonBuilder;
91use crate::utils::uri::UriExt;
92
93
94//------------ Store ---------------------------------------------------------
95
96/// A store for RPKI objects.
97///
98/// The store retains a copy of curated, published RPKI data. Its intended use
99/// is for keeping the most recent data of a given RPKI publication point that
100/// was found to be correctly published as well as keeping track of which
101/// publication points have been requested which is used during optimistic
102/// startup.
103///
104/// A store can be created via the [`new`][Store::new] function which will
105/// initialize a new store on disk if necessary and open it. If you only want
106/// to make sure that the store is initilized without actually using it,
107/// the [`init`][Store::init] function can be used.
108///
109/// To use the store during a validation run, the [`start`][Store::start]
110/// method is used. It returns a [`Run`] object providing actual access to
111/// the store.
112#[derive(Clone, Debug)]
113pub struct Store {
114 /// The base path for the store.
115 path: PathBuf,
116}
117
118impl Store {
119 /// The name of the status file.
120 const STATUS_NAME: &'static str = "status.bin";
121
122 /// The dirctory for TA certificates retrieved via rsync.
123 const RSYNC_TA_PATH: &'static str = "ta/rsync";
124
125 /// The dirctory for TA certificates retrieved via HTTPS.
126 const HTTPS_TA_PATH: &'static str = "ta/https";
127
128 /// The directory for the RRDP repositories.
129 const RRDP_BASE: &'static str = "rrdp";
130
131 /// The directory for the rsync repository.
132 const RSYNC_PATH: &'static str = "rsync";
133
134 /// The name of the directory where the temporary files go.
135 const TMP_BASE: &'static str = "tmp";
136}
137
138impl Store {
139 /// Returns the base path for the given config.
140 fn create_base_dir(config: &Config) -> Result<PathBuf, Failed> {
141 // We are using "stored" since store was foolishly used in 0.9.0 for
142 // the database.
143 let path = config.cache_dir.join("stored");
144 if let Err(err) = fs::create_dir_all(&path) {
145 error!(
146 "Failed to create store directory {}: {}",
147 path.display(), err
148 );
149 return Err(Failed)
150 }
151 Ok(path)
152 }
153
154 /// Initializes the store without creating a value.
155 ///
156 /// Ensures that the base directory exists and creates it of necessary.
157 ///
158 /// The function is called implicitly by [`new`][Self::new].
159 // (Or, well, not really, but they both only call `create_base_dir`, so
160 // from a user perspective it does.)
161 pub fn init(config: &Config) -> Result<(), Failed> {
162 Self::create_base_dir(config)?;
163 Ok(())
164 }
165
166 /// Creates a new store at the given path.
167 pub fn new(config: &Config) -> Result<Self, Failed> {
168 Ok(Store {
169 path: Self::create_base_dir(config)?,
170 })
171 }
172
173 /// Sanitizes the stored data.
174 ///
175 /// Currently doesn’t do anything.
176 pub fn sanitize(&self) -> Result<(), Fatal> {
177 Ok(())
178 }
179
180 /// Start a validation run with the store.
181 pub fn start(&self) -> Run<'_> {
182 Run::new(self)
183 }
184
185 /// Loads the status of the last run.
186 pub fn status(&self) -> Result<Option<StoredStatus>, Failed> {
187 let path = self.status_path();
188 let Some(mut file) = fatal::open_existing_file(&path)? else {
189 return Ok(None)
190 };
191 match StoredStatus::read(&mut file) {
192 Ok(status) => Ok(Some(status)),
193 Err(err) => {
194 error!("Failed to read store status file {}: {}",
195 path.display(), err
196 );
197 Err(Failed)
198 }
199 }
200 }
201
202 /// Returns the path for the status file.
203 fn status_path(&self) -> PathBuf {
204 self.path.join(Self::STATUS_NAME)
205 }
206
207 /// Returns the path to use for the trust anchor at the given URI.
208 fn ta_path(&self, uri: &TalUri) -> PathBuf {
209 match *uri {
210 TalUri::Rsync(ref uri) => {
211 self.path.join(
212 uri.unique_path(Self::RSYNC_TA_PATH, ".cer")
213 )
214 }
215 TalUri::Https(ref uri) => {
216 self.path.join(
217 uri.unique_path(Self::HTTPS_TA_PATH, ".cer")
218 )
219 }
220 }
221 }
222
223 /// Returns the path where the RRDP repositories are stored.
224 fn rrdp_repository_base(&self) -> PathBuf {
225 self.path.join(Self::RRDP_BASE)
226 }
227
228 /// Returns the path for the RRDP repository with the given rpkiNotify URI.
229 fn rrdp_repository_path(&self, uri: &uri::Https) -> PathBuf {
230 self.path.join(uri.unique_path(Self::RRDP_BASE, ""))
231 }
232
233 /// Returns the path where the combined rsync repository is stored.
234 fn rsync_repository_path(&self) -> PathBuf {
235 self.path.join(Self::RSYNC_PATH)
236 }
237
238 /// Creates and returns a temporary file.
239 ///
240 /// The file is created in the store’s temporary path. If this succeeds,
241 /// the path and file object are returned.
242 fn tmp_file(&self) -> Result<NamedTempFile, Failed> {
243 let tmp_dir = self.path.join(Self::TMP_BASE);
244 fatal::create_dir_all(&tmp_dir)?;
245 NamedTempFile::new_in(&tmp_dir).map_err(|err| {
246 error!(
247 "Fatal: failed to create temporary file in {}: {}",
248 tmp_dir.display(), err
249 );
250 Failed
251 })
252 }
253}
254
255/// # Dumping of stored data
256impl Store {
257 /// Dumps the content of the store to `dir`.
258 pub fn dump(&self, dir: &Path) -> Result<(), Failed> {
259 // TA certificates.
260 self.dump_subdir(Self::RSYNC_TA_PATH, dir)?;
261 self.dump_subdir(Self::HTTPS_TA_PATH, dir)?;
262
263 // Dump store content.
264 let dir = dir.join("store");
265 fatal::remove_dir_all(&dir)?;
266 let mut repos = DumpRegistry::new(dir);
267 self.dump_point_tree(&self.rsync_repository_path(), &mut repos)?;
268 self.dump_point_tree(&self.rrdp_repository_base(), &mut repos)?;
269
270 self.dump_repository_json(repos)?;
271 Ok(())
272 }
273
274 /// Dumps all the complete sub-directory.
275 fn dump_subdir(
276 &self,
277 subdir: &str,
278 target_base: &Path,
279 ) -> Result<(), Failed> {
280 let source = self.path.join(subdir);
281 let target = target_base.join(subdir);
282 fatal::remove_dir_all(&target)?;
283 fatal::copy_existing_dir_all(&source, &target)?;
284 Ok(())
285 }
286
287 /// Dumps all the stored points found in the tree under `path`.
288 ///
289 /// The point’s repository and rsync URI is determined from the stored
290 /// points themselves. The target path is being determined from `repos`.
291 fn dump_point_tree(
292 &self,
293 path: &Path,
294 repos: &mut DumpRegistry,
295 ) -> Result<(), Failed> {
296 let dir = match fatal::read_existing_dir(path)? {
297 Some(dir) => dir,
298 None => return Ok(())
299 };
300 for entry in dir {
301 let entry = entry?;
302 if entry.is_dir() {
303 self.dump_point_tree(entry.path(), repos)?;
304 }
305 else if entry.is_file() {
306 self.dump_point(entry.path(), repos)?;
307 }
308 }
309 Ok(())
310 }
311
312 /// Dumps all data for a single stored publication point.
313 fn dump_point(
314 &self,
315 path: &Path,
316 repos: &mut DumpRegistry,
317 ) -> Result<(), Failed> {
318 let mut file = File::open(path).map_err(|err| {
319 error!(
320 "Fatal: failed to open file {}: {}",
321 path.display(), err
322 );
323 Failed
324 })?;
325 let header = match StoredPointHeader::read(&mut file) {
326 Ok(some) => some,
327 Err(err) => {
328 error!(
329 "Skipping {}: failed to read file: {}",
330 path.display(), err
331 );
332 return Ok(())
333 }
334 };
335 let manifest = StoredManifest::read(&mut file).map_err(|err| {
336 error!(
337 "Fatal: failed to read file {}: {}",
338 path.display(), err
339 );
340 Failed
341 })?;
342
343 let repo_dir = repos.get_repo_path(header.rpki_notify.as_ref());
344
345 // Manifest and CRL are in `manifest`.
346 self.dump_object(
347 &repo_dir, &header.manifest_uri, &manifest.manifest
348 )?;
349 self.dump_object(&repo_dir, &manifest.crl_uri, &manifest.crl)?;
350
351 // Loop all other objects.
352 while let Some(object) = StoredObject::read(&mut file).map_err(|err| {
353 error!(
354 "Fatal: failed to read file {}: {}",
355 path.display(), err
356 );
357 Failed
358 })? {
359 self.dump_object(&repo_dir, &object.uri, &object.content)?;
360 }
361
362 Ok(())
363 }
364
365 /// Writes the data of a single object.
366 fn dump_object(
367 &self,
368 dir: &Path,
369 uri: &uri::Rsync,
370 content: &[u8]
371 ) -> Result<(), Failed> {
372 let path = dir.join(
373 format!("{}/{}/{}",
374 uri.canonical_authority(),
375 uri.module_name(),
376 uri.path()
377 )
378 );
379 if let Some(dir) = path.parent() {
380 fatal::create_dir_all(dir)?;
381 }
382 let mut target = match File::create(&path) {
383 Ok(some) => some,
384 Err(err) => {
385 error!(
386 "Fatal: cannot create target file {}: {}",
387 path.display(), err
388 );
389 return Err(Failed)
390 }
391 };
392 if let Err(err) = target.write_all(content) {
393 error!(
394 "Fatal: failed to write to target file {}: {}",
395 path.display(), err
396 );
397 return Err(Failed)
398 }
399
400 Ok(())
401 }
402
403 /// Writes the repositories.json file.
404 fn dump_repository_json(
405 &self,
406 repos: DumpRegistry,
407 ) -> Result<(), Failed> {
408 fatal::create_dir_all(repos.base_dir())?;
409 let path = repos.base_dir().join("repositories.json");
410 fatal::write_file(
411 &path,
412 JsonBuilder::build(|builder| {
413 builder.member_array("repositories", |builder| {
414 for (key, value) in repos.rrdp_uris() {
415 builder.array_object(|builder| {
416 builder.member_str(
417 "path", value
418 );
419 builder.member_str("type", "rrdp");
420 builder.member_str(
421 "rpkiNotify",
422 key
423 );
424 })
425 }
426 builder.array_object(|builder| {
427 builder.member_str("path", "rsync");
428 builder.member_str("type", "rsync");
429 });
430 })
431 }).as_bytes()
432 )
433 }
434}
435
436
437//------------ Run -----------------------------------------------------------
438
439/// A single validation run on using the store.
440///
441/// The type provides access to the stored versions of trust anchor
442/// certificates via the [`load_ta`][Self::load_ta] method and repositories
443/// through the [`repository`][Self::repository] method or publication points
444/// directly via [pub_point}[Self::pub_point].
445///
446/// Stored trust anchor certificates can be updated via
447/// [`update_ta`][Self::update_ta] on [`Run`] directly, while
448/// [`StoredPoint`] provides means to that for RPKI objects.
449#[derive(Debug)]
450pub struct Run<'a> {
451 /// A reference to the underlying store.
452 store: &'a Store,
453
454 /// The time this run was started.
455 started: Time,
456}
457
458impl<'a> Run<'a> {
459 /// Creates a new runner from a store.
460 fn new(
461 store: &'a Store,
462 ) -> Self {
463 Run {
464 store,
465 started: Time::now(),
466 }
467 }
468
469 /// Finishes the validation run.
470 ///
471 /// Updates the `metrics` with the store run’s metrics.
472 pub fn done(self, metrics: &mut Metrics) {
473 let _ = metrics;
474 let path = self.store.status_path();
475 let Ok(mut file) = fatal::create_file(&path) else {
476 return
477 };
478 if let Err(err) = StoredStatus::new(Time::now()).write(&mut file) {
479 error!(
480 "Failed to write store status file {}: {}",
481 path.display(), err
482 );
483 }
484 }
485
486 /// Loads a stored trust anchor certificate.
487 pub fn load_ta(&self, uri: &TalUri) -> Result<Option<Bytes>, Failed> {
488 fatal::read_existing_file(&self.store.ta_path(uri)).map(|maybe| {
489 maybe.map(Into::into)
490 })
491 }
492
493 /// Updates or inserts a stored trust anchor certificate.
494 pub fn update_ta(
495 &self, uri: &TalUri, content: &[u8]
496 ) -> Result<(), Failed> {
497 let path = self.store.ta_path(uri);
498 if let Some(dir) = path.parent() {
499 fatal::create_dir_all(dir)?;
500 }
501 fatal::write_file(&path, content)
502 }
503
504 /// Accesses the repository for the provided RPKI CA.
505 ///
506 /// If the CA’s rpkiNotify URI is present, the RRDP repository identified
507 /// by that URI will be returned, otherwise the rsync repository will be
508 /// used.
509 ///
510 /// Note that we even use the RRDP repository if the collector had to fall
511 /// back to using rsync. Because rsync is ‘authoritative’ for the object
512 /// URIs, it is safe to use objects received via rsync in RRDP
513 /// repositories.
514 pub fn repository(&self, ca_cert: &CaCert) -> Repository {
515 Repository::new(self.store, ca_cert.rpki_notify().cloned())
516 }
517
518 /// Accesses the publication point for the provided RPKI CA.
519 ///
520 /// If the CA’s rpkiNotify URI is present, the RRDP repository identified
521 /// by that URI will be returned, otherwise the rsync repository will be
522 /// used.
523 ///
524 /// Note that we even use the RRDP repository if the collector had to fall
525 /// back to using rsync. Because rsync is ‘authoritative’ for the object
526 /// URIs, it is safe to use objects received via rsync in RRDP
527 /// repositories.
528 pub fn pub_point(
529 &self, ca_cert: &CaCert
530 ) -> Result<StoredPoint, Failed> {
531 self.repository(ca_cert).get_point(ca_cert.rpki_manifest())
532 }
533}
534
535impl Run<'_> {
536 /// Cleans up the store.
537 ///
538 /// All publication points that have an expired manifest will be removed.
539 /// RRDP repositories that have no more publication points are removed,
540 /// too.
541 ///
542 /// All RRDP repositories and rsync modules retained are registered with
543 /// `collector` for retaining in the collector as well.
544 pub fn cleanup(
545 &self,
546 collector: &mut collector::Cleanup,
547 ) -> Result<(), Failed> {
548 self.cleanup_ta()?;
549 self.cleanup_points(&self.store.rrdp_repository_base(), collector)?;
550 self.cleanup_points(&self.store.rsync_repository_path(), collector)?;
551 self.cleanup_tmp()?;
552 Ok(())
553 }
554
555 /// Cleans up a tree with publication points.
556 ///
557 /// Deletes all publication points with an expired manifest as well as
558 /// any obviously garbage files. The RRDP repository of any publication
559 /// point that is retained is registered to be retained by the collector.
560 fn cleanup_points(
561 &self,
562 base: &Path,
563 retain: &mut collector::Cleanup,
564 ) -> Result<(), Failed> {
565 Self::cleanup_dir_tree(base, |path| {
566 if let Some(stored) = StoredPoint::load_quietly(path.into()) {
567 if stored.retain(self.started) {
568 if let Some(uri) = stored.header.rpki_notify.as_ref() {
569 retain.add_rrdp_repository(uri)
570 }
571 else {
572 retain.add_rsync_module(&stored.header.manifest_uri)
573 }
574 return Ok(true)
575 }
576 }
577 Ok(false)
578 })
579 }
580
581 /// Cleans up the trust anchors.
582 ///
583 /// Deletes all files that either don’t successfully parse as certificates
584 /// or that are expired certificates.
585 fn cleanup_ta(&self) -> Result<(), Failed> {
586 Self::cleanup_dir_tree(&self.store.path.join("ta"), |path| {
587 let content = fatal::read_file(path)?;
588 if let Ok(cert) = Cert::decode(Bytes::from(content)) {
589 if cert.validity().not_after() > Time::now() {
590 return Ok(true)
591 }
592 }
593 Ok(false)
594 })
595 }
596
597 fn cleanup_tmp(&self) -> Result<(), Failed> {
598 Self::cleanup_dir_tree(&self.store.path.join("tmp"), |_path| {
599 Ok(false)
600 })
601 }
602
603 /// Cleans up a directory tree.
604 ///
605 /// If the closure returns `Ok(false)` for a file with the given path, the
606 /// file will be deleted. If all files in a directory are deleted, that
607 /// directory is deleted.
608 fn cleanup_dir_tree(
609 base: &Path,
610 mut keep: impl FnMut(&Path) -> Result<bool, Failed>
611 ) -> Result<(), Failed> {
612 /// Actual recursion.
613 ///
614 /// If `top` is `true`, we ignore if the directory `path` is missing.
615 ///
616 /// Returns whether the `base` needs to be kept. I.e., if `Ok(false)`
617 /// is returned, the calling recursing step will perform a
618 /// `delete_dir_all(base)`.
619 fn recurse(
620 base: &Path,
621 top: bool,
622 op: &mut impl FnMut(&Path) -> Result<bool, Failed>
623 ) -> Result<bool, Failed> {
624 let dir = if top {
625 match fatal::read_existing_dir(base)? {
626 Some(dir) => dir,
627 None => return Ok(false),
628 }
629 }
630 else {
631 fatal::read_dir(base)?
632 };
633
634 let mut keep = false;
635 for entry in dir {
636 let entry = entry?;
637 if entry.is_dir() {
638 if !recurse(entry.path(), false, op)? {
639 fatal::remove_dir_all(entry.path())?;
640 }
641 else {
642 keep = true;
643 }
644 }
645 else if entry.is_file() {
646 if !op(entry.path())? {
647 fatal::remove_file(entry.path())?;
648 }
649 else {
650 keep = true;
651 }
652 }
653 // Let’s not try deleting non-file-and-non-dir things here but
654 // leave it to remove_dir_all to give it a shot.
655 }
656 Ok(keep)
657 }
658 recurse(base, true, &mut keep).map(|_| ())
659 }
660}
661
662
663//------------ Repository ----------------------------------------------------
664
665/// Access to a single repository during a validation run.
666///
667/// A repository is a collection of publication points. Each of these points
668/// has a manifest and a set of objects. The manifest is identified by its
669/// signedObject URI while the objects are identified by their name on the
670/// manifest’s object list.
671///
672/// You can get access to a publication point via
673/// [`get_point`][Self::get_point].
674///
675pub struct Repository {
676 /// The path where the repository lives.
677 path: PathBuf,
678
679 /// The RRPD URI for the repository or `None` if this is the rsync repo.
680 rpki_notify: Option<uri::Https>,
681}
682
683impl Repository {
684 /// Creates a repository object for the given repository.
685 ///
686 /// The repository is identified by the RRDP URI. Each RRDP “server” gets
687 /// its own repository and all rsync “servers” share one.
688 fn new(store: &Store, rpki_notify: Option<uri::Https>) -> Self {
689 Self {
690 path: if let Some(rpki_notify) = rpki_notify.as_ref() {
691 store.rrdp_repository_path(rpki_notify)
692 }
693 else {
694 store.rsync_repository_path()
695 },
696 rpki_notify
697 }
698 }
699
700 /// Returns the RRDP URI if present.
701 pub fn rpki_notify(&self) -> Option<&uri::Https> {
702 self.rpki_notify.as_ref()
703 }
704
705 /// Returns whether this is an RRDP repository.
706 pub fn is_rrdp(&self) -> bool {
707 self.rpki_notify.is_some()
708 }
709
710 /// Opens the given stored publication point.
711 ///
712 /// The publication point is identified through the rsync URI of its
713 /// manifest.
714 ///
715 /// A stored point instance will be returned whether there actually is
716 /// information stored for the point or not.
717 pub fn get_point(
718 &self, manifest_uri: &uri::Rsync
719 ) -> Result<StoredPoint, Failed> {
720 StoredPoint::open(
721 self.point_path(manifest_uri),
722 manifest_uri, self.rpki_notify.as_ref(),
723 )
724 }
725
726 /// Returns the path for a publication point with the given manifest URI.
727 fn point_path(&self, manifest_uri: &uri::Rsync) -> PathBuf {
728 self.path.join(
729 format!(
730 "rsync/{}/{}/{}",
731 manifest_uri.canonical_authority(),
732 manifest_uri.module_name(),
733 manifest_uri.path(),
734 )
735 )
736 }
737}
738
739
740//------------ StoredPoint ---------------------------------------------------
741
742/// The stored information of a publication point.
743///
744/// This types allows access to the stored manifest via
745/// [`manifest`][Self::manifest] and acts as an iterator over the
746/// publication point’s objects. The method [`update`][Self::update] allows
747/// atomically updating the information.
748pub struct StoredPoint {
749 /// The path to the file-system location of the repository.
750 path: PathBuf,
751
752 /// Is the this a newly discovered stored point?
753 is_new: bool,
754
755 /// The header of the stored point.
756 ///
757 /// This will always be present, even if the point is new.
758 header: StoredPointHeader,
759
760 /// The stored manifest for the point if there is one.
761 manifest: Option<StoredManifest>,
762
763 /// The file with all the information we need.
764 ///
765 /// The file will only be present if a point has been successfully
766 /// stored before. I.e, if the point is present but never was
767 /// successfully updated, this will still be `None`.
768 ///
769 /// If present, the file will be positioned at the begining of the next
770 /// stored object to be loaded.
771 file: Option<BufReader<File>>,
772}
773
774impl StoredPoint {
775 /// Opens the stored point.
776 ///
777 /// If there is a file at the given path, it is opened, the manifest is
778 /// read and positioned at the first stored object. Otherwise there will
779 /// be no manifest and no objects.
780 fn open(
781 path: PathBuf,
782 manifest_uri: &uri::Rsync,
783 rpki_notify: Option<&uri::Https>,
784 ) -> Result<Self, Failed> {
785 let mut file = match File::open(&path) {
786 Ok(file) => BufReader::new(file),
787 Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
788 return Self::create(path, manifest_uri, rpki_notify);
789 }
790 Err(err) => {
791 error!(
792 "Failed to open stored publication point at {}: {}",
793 path.display(), err
794 );
795 return Err(Failed)
796 }
797 };
798
799 let mut header = match StoredPointHeader::read(&mut file) {
800 Ok(header) => header,
801 Err(err) if !err.is_fatal() => {
802 return Self::create(path, manifest_uri, rpki_notify);
803 }
804 Err(err) => {
805 error!(
806 "Failed to read stored publication point at {}: {}",
807 path.display(), err
808 );
809 return Err(Failed)
810 }
811 };
812
813 // From here on all errors are considered fatal.
814
815 if matches!(header.update_status, UpdateStatus::LastAttempt(_)) {
816 // We never succeeded. Update the status and return.
817 header.update_status = UpdateStatus::LastAttempt(Time::now());
818
819 drop(file);
820 let mut file = File::create(&path).map_err(|err| {
821 error!(
822 "Failed to update stored publication point at {}: \
823 re-open: {}",
824 path.display(), err
825 );
826 Failed
827
828 })?;
829
830 if let Err(err) = file.seek(SeekFrom::Start(0)) {
831 error!(
832 "Failed to update stored publication point at {}: \
833 seek failed: {}",
834 path.display(), err
835 );
836 return Err(Failed)
837 }
838 if let Err(err) = header.write(&mut file) {
839 error!(
840 "Failed to update stored publication point at {}: \
841 write failed: {}",
842 path.display(), err
843 );
844 return Err(Failed)
845 }
846
847 return Ok(Self {
848 path,
849 is_new: false,
850 header,
851 manifest: None,
852 file: None,
853 })
854 }
855
856 let manifest = match StoredManifest::read(&mut file) {
857 Ok(manifest) => manifest,
858 Err(err) => {
859 error!(
860 "Failed to read stored publication point at {}: {}",
861 path.display(), err
862 );
863 return Err(Failed)
864 }
865 };
866
867 Ok(Self {
868 path,
869 is_new: false,
870 header,
871 manifest: Some(manifest),
872 file: Some(file)
873 })
874 }
875
876 /// Creates a new, empty stored point.
877 ///
878 /// This is called either when the stored point doesn’t exist or there is
879 /// one but it is of the wrong version.
880 ///
881 /// Creates the file and sets it to an initial status.
882 fn create(
883 path: PathBuf,
884 manifest_uri: &uri::Rsync,
885 rpki_notify: Option<&uri::Https>,
886 ) -> Result<Self, Failed> {
887 if let Some(path) = path.parent() {
888 fatal::create_dir_all(path)?;
889 }
890 let mut file = match File::create(&path) {
891 Ok(file) => file,
892 Err(err) => {
893 error!(
894 "Failed to create stored publication point at {}: {}",
895 path.display(), err
896 );
897 return Err(Failed)
898 }
899 };
900 let header = StoredPointHeader::new(
901 manifest_uri.clone(), rpki_notify.cloned(),
902 );
903 if let Err(err) = header.write(&mut file) {
904 error!(
905 "Failed to write stored publication point at {}: {}",
906 path.display(), err
907 );
908 return Err(Failed)
909 }
910
911 Ok(StoredPoint {
912 path,
913 is_new: true,
914 header,
915 manifest: None,
916 file: None,
917 })
918
919 }
920
921 /// Loads an existing stored point from a path.
922 ///
923 /// Does not create a value if the point does not exist. Does not output
924 /// any error messages and just returns `None` if loading fails.
925 pub fn load_quietly(path: PathBuf) -> Option<Self> {
926 let mut file = BufReader::new(File::open(&path).ok()?);
927 let header = StoredPointHeader::read(&mut file).ok()?;
928 let manifest = match header.update_status {
929 UpdateStatus::Success(_) => {
930 Some(StoredManifest::read(&mut file).ok()?)
931 }
932 UpdateStatus::LastAttempt(_) => None,
933 };
934 Some(Self {
935 path,
936 is_new: false,
937 header, manifest,
938 file: Some(file)
939 })
940 }
941
942 /// Returns whether the point was newly discovered during this run.
943 pub fn is_new(&self) -> bool {
944 self.is_new
945 }
946
947 /// Replaces the data of the stored point.
948 ///
949 /// Updates the manifest with the provided manifest and the objects
950 /// provided by the closure. The closure is called repeatedly until it
951 /// either returns `Ok(None)` or `Err(_)`. In the latter case, the update
952 /// is cancelled, the old point remains unchanged and the error is
953 /// returned. Otherwise, `self` represents the new point. It is
954 /// positioned at the first object, i.e., if it is iterated over, the
955 /// first object will be returned next.
956 ///
957 /// The closure here acts as a poor man’s generator which makes it easier
958 /// to write the necessary code.
959 pub fn update(
960 &mut self,
961 store: &Store,
962 manifest: StoredManifest,
963 objects: impl FnMut() -> Result<Option<StoredObject>, UpdateError>
964 ) -> Result<(), UpdateError> {
965 let tmp_file = store.tmp_file()?;
966 self._update(tmp_file, manifest, objects)
967 }
968
969 fn _update(
970 &mut self,
971 tmp_file: NamedTempFile,
972 manifest: StoredManifest,
973 mut objects: impl FnMut() -> Result<Option<StoredObject>, UpdateError>
974 ) -> Result<(), UpdateError> {
975 let mut tmp_file = BufWriter::new(tmp_file);
976
977 self.header.update_status = UpdateStatus::Success(Time::now());
978
979 if let Err(err) = self.header.write(&mut tmp_file) {
980 error!(
981 "Fatal: failed to write to file {}: {}",
982 tmp_file.get_ref().path().display(), err
983 );
984 return Err(UpdateError::fatal())
985 }
986 if let Err(err) = manifest.write(&mut tmp_file) {
987 error!(
988 "Fatal: failed to write to file {}: {}",
989 tmp_file.get_ref().path().display(), err
990 );
991 return Err(UpdateError::fatal())
992 }
993 let tmp_object_start = match tmp_file.stream_position() {
994 Ok(some) => some,
995 Err(err) => {
996 error!(
997 "Fatal: failed to get position in file {}: {}",
998 tmp_file.get_ref().path().display(), err
999 );
1000 return Err(UpdateError::fatal())
1001 }
1002 };
1003 while let Some(object) = objects()? {
1004 if let Err(err) = object.write(&mut tmp_file) {
1005 error!(
1006 "Fatal: failed to write to file {}: {}",
1007 tmp_file.get_ref().path().display(), err
1008 );
1009 return Err(UpdateError::fatal())
1010 }
1011 }
1012
1013 let tmp_file = tmp_file.into_inner().map_err(|err| {
1014 let (err, tmp_file) = err.into_parts();
1015 error!(
1016 "Fatal: failed to write to file {}: {}",
1017 tmp_file.get_ref().path().display(), err
1018 );
1019 UpdateError::fatal()
1020 })?;
1021
1022 // I think we need to drop `self.file` first so it gets closed and the
1023 // path unlocked on Windows?
1024 drop(self.file.take());
1025 match tmp_file.persist(&self.path) {
1026 Ok(file) => self.file = Some(BufReader::new(file)),
1027 Err(err) => {
1028 error!(
1029 "Failed to persist temporary file {} to {}: {}",
1030 err.file.path().display(), self.path.display(),
1031 err.error,
1032 );
1033 return Err(UpdateError::fatal())
1034 }
1035 }
1036 self.manifest = Some(manifest);
1037
1038 // Position the file at the first object. (The if will always be
1039 // true, so this is fine.)
1040 if let Some(file) = self.file.as_mut() {
1041 if let Err(err) = file.seek(SeekFrom::Start(tmp_object_start)) {
1042 error!(
1043 "Fatal: failed to position file {}: {}",
1044 self.path.display(), err
1045 );
1046 return Err(UpdateError::fatal())
1047 }
1048 }
1049
1050 Ok(())
1051 }
1052
1053 /// Deletes the stored point’s information.
1054 ///
1055 /// Replaces the file with one pretending we’ve never successfully
1056 /// updated the point and changes `self` accordingly.
1057 pub fn reject(&mut self) -> Result<(), Failed> {
1058 self.is_new = true;
1059 self.header.update_status = UpdateStatus::LastAttempt(Time::now());
1060 self.manifest = None;
1061 self.file = None;
1062
1063 let mut file = match File::create(&self.path) {
1064 Ok(file) => file,
1065 Err(err) => {
1066 error!(
1067 "Failed to create stored publication point at {}: {}",
1068 self.path.display(), err
1069 );
1070 return Err(Failed)
1071 }
1072 };
1073 if let Err(err) = self.header.write(&mut file) {
1074 error!(
1075 "Failed to write stored publication point at {}: {}",
1076 self.path.display(), err
1077 );
1078 return Err(Failed)
1079 }
1080 Ok(())
1081 }
1082
1083 /// Returns whether the point should be retained.
1084 ///
1085 /// If the point had a successful update, it will retained until the
1086 /// `notAfter` time of the manifest’s certificate. Otherwise it will be
1087 /// retained if the last update attempted was after `update_start` (i.e.,
1088 /// there was an attempt to update the point during this validation run).
1089 fn retain(&self, update_start: Time) -> bool {
1090 if let Some(manifest) = self.manifest.as_ref() {
1091 manifest.not_after > Time::now()
1092 }
1093 else if let UpdateStatus::LastAttempt(when)
1094 = self.header.update_status
1095 {
1096 when >= update_start
1097 }
1098 else {
1099 // Update status says success but we don’t have a manifest? That
1100 // can’t happen, so say “no”.
1101 false
1102 }
1103 }
1104}
1105
1106impl StoredPoint {
1107 /// Returns a reference to the path of the file.
1108 pub fn path(&self) -> &Path {
1109 &self.path
1110 }
1111
1112 /// Returns a reference to the stored manifest if available.
1113 ///
1114 /// The manifest will not be available if there is no previously stored
1115 /// version of the publication point and an update has not succeeded yet,
1116 /// or if the manifest has been taken out via
1117 /// [`take_manifest`][Self::take_manifest].
1118 pub fn manifest(&self) -> Option<&StoredManifest> {
1119 self.manifest.as_ref()
1120 }
1121}
1122
1123impl Iterator for StoredPoint {
1124 type Item = Result<StoredObject, ParseError>;
1125
1126 fn next(&mut self) -> Option<Self::Item> {
1127 StoredObject::read(self.file.as_mut()?).transpose()
1128 }
1129}
1130
1131
1132//------------ StoredPointHeader ---------------------------------------------
1133
1134/// The header of the a stored publication point.
1135///
1136#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1137pub struct StoredPointHeader {
1138 /// The manifest’s rsync URI.
1139 manifest_uri: uri::Rsync,
1140
1141 /// The rpkiNotify URI of the issuing CA certificate.
1142 rpki_notify: Option<uri::Https>,
1143
1144 /// The update status of the point.
1145 ///
1146 /// Tells use whether we have ever seen a successful update or when we
1147 /// last tried.
1148 update_status: UpdateStatus,
1149}
1150
1151impl StoredPointHeader {
1152 /// The version of the type.
1153 ///
1154 /// This was part of `StoredManifest` before 0.15 with value 1 and
1155 /// before 0.14 with value 0.
1156 const VERSION: u8 = 2;
1157
1158 /// Creates a new stored status.
1159 ///
1160 /// Assumes that updates have never succeeded.
1161 pub fn new(
1162 manifest_uri: uri::Rsync,
1163 rpki_notify: Option<uri::Https>,
1164 ) -> Self {
1165 Self {
1166 manifest_uri, rpki_notify,
1167 update_status: UpdateStatus::LastAttempt(Time::now()),
1168 }
1169 }
1170
1171 /// Reads a stored point status from an IO reader.
1172 pub fn read(reader: &mut impl io::Read) -> Result<Self, ParseError> {
1173 // Version number.
1174 let version = u8::parse(reader)?;
1175 if version != Self::VERSION {
1176 return Err(ParseError::format(
1177 format!("unexpected version {version}")
1178 ))
1179 }
1180 Ok(Self {
1181 manifest_uri: Parse::parse(reader)?,
1182 rpki_notify: Parse::parse(reader)?,
1183 update_status: UpdateStatus::read(reader)?,
1184 })
1185 }
1186
1187 /// Writes the stored point status to a writer.
1188 pub fn write(
1189 &self, writer: &mut impl io::Write
1190 ) -> Result<(), io::Error> {
1191 Self::VERSION.compose(writer)?;
1192
1193 self.manifest_uri.compose(writer)?;
1194 self.rpki_notify.compose(writer)?;
1195 self.update_status.write(writer)?;
1196
1197 Ok(())
1198 }
1199}
1200
1201
1202//------------ UpdateStatus --------------------------------------------------
1203
1204/// The update status of a stored point.
1205#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1206enum UpdateStatus {
1207 /// There was a successful update at the given time.
1208 ///
1209 /// Strictly speaking, we don’t need this time. But it may come in
1210 /// handy.
1211 Success(Time),
1212
1213 /// There never was a successful update, last attempt at the given time.
1214 ///
1215 /// Note that once we had success, we stick with `Self::Success`. This
1216 /// variant is for before first success only.
1217 LastAttempt(Time),
1218}
1219
1220impl UpdateStatus {
1221 /// Reads a stored point status from an IO reader.
1222 pub fn read(reader: &mut impl io::Read) -> Result<Self, ParseError> {
1223 match u8::parse(reader)? {
1224 0 => Ok(UpdateStatus::Success(Parse::parse(reader)?)),
1225 1 => Ok(UpdateStatus::LastAttempt(Parse::parse(reader)?)),
1226 _ => {
1227 Err(ParseError::format(
1228 "invalid update status".to_string()
1229 ))
1230 }
1231 }
1232 }
1233
1234 /// Writes the stored point status to a writer.
1235 pub fn write(self, writer: &mut impl io::Write) -> Result<(), io::Error> {
1236 match self {
1237 Self::Success(time) => {
1238 0u8.compose(writer)?;
1239 time.compose(writer)?;
1240 }
1241 Self::LastAttempt(time) => {
1242 1u8.compose(writer)?;
1243 time.compose(writer)?;
1244 }
1245 }
1246 Ok(())
1247 }
1248}
1249
1250
1251//------------ StoredManifest ------------------------------------------------
1252
1253/// The content of a manifest placed in the store.
1254///
1255/// This type collects all data that is stored as the manifest for a
1256/// publication point.
1257///
1258/// This contains the raw bytes of both the manifest itself plus data that
1259/// will be needed to use the manifest during processing. In particular:
1260///
1261/// * The expiry time of the manifest’s EE certificate via the
1262/// [`not_after`][Self::not_after] method. This is used during cleanup to
1263/// determine whether to keep a publication point. It is stored to avoid
1264/// having to parse the whole manifest.
1265/// * The manifest number and thisUpdate time. These are used to check whether
1266/// a new manifest tries to go backwards.
1267/// * The caRepository URI of the CA certificate that has issued the manifest
1268/// via the [`ca_repository`][Self::ca_repository] method. This is
1269/// necessary to convert the file names mentioned on the manifest into their
1270/// full rsync URIs. Confusingly, this information is not available on the
1271/// manifest itself and therefore needs to be stored.
1272/// * The raw bytes of the manifest via the [`manifest`][Self::manifest]
1273/// method.
1274/// * The raw bytes of the CRL referenced by the manifest via the
1275/// [`crl`][Self::crl] method. There must always be exactly one CRL used by
1276/// a publication point. As it needs to be available for validation, we
1277/// might as well store it together with the manifest.
1278#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1279pub struct StoredManifest {
1280 /// The expire time of the EE certificate of the manifest.
1281 pub not_after: Time,
1282
1283 /// The manifest number of the manifest.
1284 pub manifest_number: Serial,
1285
1286 /// The thisUpdate time of the manifest.
1287 pub this_update: Time,
1288
1289 /// The CA repository rsync URI of the issuing CA certificate.
1290 pub ca_repository: uri::Rsync,
1291
1292 /// The raw content of the manifest.
1293 pub manifest: Bytes,
1294
1295 /// The CRL’s rsync URI.
1296 pub crl_uri: uri::Rsync,
1297
1298 /// The raw content of the CRL.
1299 pub crl: Bytes,
1300}
1301
1302impl StoredManifest {
1303 /// Creates a new stored manifest.
1304 ///
1305 /// The new value is created from the components of the stored manifest.
1306 /// See the methods with the same name for their meaning.
1307 pub fn new(
1308 ee_cert: &ResourceCert,
1309 manifest: &ManifestContent,
1310 ca_cert: &CaCert,
1311 manifest_bytes: Bytes,
1312 crl_uri: uri::Rsync,
1313 crl: Bytes,
1314 ) -> Self {
1315 StoredManifest {
1316 not_after: ee_cert.validity().not_after(),
1317 manifest_number: manifest.manifest_number(),
1318 this_update: manifest.this_update(),
1319 ca_repository: ca_cert.ca_repository().clone(),
1320 manifest: manifest_bytes,
1321 crl_uri,
1322 crl
1323 }
1324 }
1325
1326 /// Reads a stored manifest from an IO reader.
1327 pub fn read(reader: &mut impl io::Read) -> Result<Self, ParseError> {
1328 Ok(StoredManifest {
1329 not_after: Parse::parse(reader)?,
1330 manifest_number: Parse::parse(reader)?,
1331 this_update: Parse::parse(reader)?,
1332 ca_repository: Parse::parse(reader)?,
1333 manifest: Parse::parse(reader)?,
1334 crl_uri: Parse::parse(reader)?,
1335 crl: Parse::parse(reader)?,
1336 })
1337 }
1338
1339 /// Appends the stored manifest to a writer.
1340 pub fn write(
1341 &self, writer: &mut impl io::Write
1342 ) -> Result<(), io::Error> {
1343 self.not_after.compose(writer)?;
1344 self.manifest_number.compose(writer)?;
1345 self.this_update.compose(writer)?;
1346 self.ca_repository.compose(writer)?;
1347 self.manifest.compose(writer)?;
1348 self.crl_uri.compose(writer)?;
1349 self.crl.compose(writer)?;
1350
1351 Ok(())
1352 }
1353}
1354
1355
1356//------------ StoredObject --------------------------------------------------
1357
1358/// The content of an object placed in the store.
1359///
1360/// This type collects all the data that is stored for regular objects of a
1361/// publication point: the raw bytes of the object as well as its hash as
1362/// stated on the publication point’s manifest. This hash is currently not
1363/// used since we only store objects when we know the publication point was
1364/// valid. It is retained here solely for compatibility with the existing
1365/// stored object format.
1366#[derive(Clone, Debug, PartialEq, Eq)]
1367pub struct StoredObject {
1368 /// The URI of the object.
1369 pub uri: uri::Rsync,
1370
1371 /// The manifest hash of the object if available.
1372 pub hash: Option<ManifestHash>,
1373
1374 /// The content of the object.
1375 pub content: Bytes,
1376}
1377
1378impl StoredObject {
1379 /// Creates a new stored object from its bytes and manifest hash.
1380 pub fn new(
1381 uri: uri::Rsync,
1382 content: Bytes,
1383 hash: Option<ManifestHash>,
1384 ) -> Self {
1385 StoredObject { uri, hash, content }
1386 }
1387
1388 /// Reads a stored object from an IO reader.
1389 pub fn read(
1390 reader: &mut impl io::Read
1391 ) -> Result<Option<Self>, ParseError> {
1392 let uri = match uri::Rsync::parse(reader) {
1393 Ok(uri) => uri,
1394 Err(err) if err.is_eof() => return Ok(None),
1395 Err(err) => return Err(err),
1396 };
1397 let hash = match u8::parse(reader)? {
1398 0 => None,
1399 1 => {
1400 let algorithm = DigestAlgorithm::sha256();
1401 let mut value = vec![0u8; algorithm.digest_len()];
1402 reader.read_exact(&mut value)?;
1403 Some(ManifestHash::new(value.into(), algorithm))
1404 }
1405 hash_type => {
1406 return Err(ParseError::format(
1407 format!("unsupported hash type {hash_type}")
1408 ));
1409 }
1410 };
1411 let content = Bytes::parse(reader)?;
1412
1413 Ok(Some(StoredObject { uri, hash, content }))
1414 }
1415
1416 /// Appends the stored object to a writer.
1417 pub fn write(
1418 &self, writer: &mut impl io::Write
1419 ) -> Result<(), io::Error> {
1420 self.uri.compose(writer)?;
1421
1422 // Hash.
1423 //
1424 // One octet hash type: 0 .. None, 1 .. SHA-256
1425 // As many octets as the hash type requires.
1426 //
1427 // Unknown digest algorithms (there is non yet, but there may be) are
1428 // encoded as if the field was None.
1429 match self.hash.as_ref() {
1430 Some(hash) if hash.algorithm().is_sha256() => {
1431 1u8.compose(writer)?;
1432 writer.write_all(hash.as_slice())?;
1433 }
1434 _ => {
1435 0u8.compose(writer)?;
1436 }
1437 }
1438
1439 self.content.compose(writer)?;
1440
1441 Ok(())
1442 }
1443}
1444
1445
1446//------------ StoredStatus --------------------------------------------------
1447
1448/// Information about the status of the store.
1449#[derive(Clone, Debug)]
1450pub struct StoredStatus {
1451 /// The time the last update was finished.
1452 pub last_update: Time,
1453}
1454
1455impl StoredStatus {
1456 /// The version of the type.
1457 const VERSION: u8 = 0;
1458
1459 /// Creates a new value.
1460 pub fn new(last_update: Time) -> Self {
1461 Self { last_update }
1462 }
1463
1464 /// Reads the stored status from an IO reader.
1465 pub fn read(reader: &mut impl io::Read) -> Result<Self, ParseError> {
1466 // Version number.
1467 let version = u8::parse(reader)?;
1468 if version != Self::VERSION {
1469 return Err(ParseError::format(
1470 format!("unexpected version {version}")
1471 ))
1472 }
1473 Ok(Self {
1474 last_update: Parse::parse(reader)?
1475 })
1476 }
1477
1478 /// Appends the stored status to a writer.
1479 pub fn write(
1480 &self, writer: &mut impl io::Write
1481 ) -> Result<(), io::Error> {
1482 Self::VERSION.compose(writer)?;
1483 self.last_update.compose(writer)?;
1484 Ok(())
1485 }
1486}
1487
1488
1489//============ Error Types ===================================================
1490
1491//------------ UpdateError ---------------------------------------------------
1492
1493/// An error happend while updating a publication point.
1494#[derive(Clone, Copy, Debug)]
1495pub enum UpdateError {
1496 /// The update needs to be aborted and rolled back.
1497 Abort,
1498
1499 /// Something really bad happened that requires aborting the run.
1500 Failed(RunFailed),
1501}
1502
1503impl UpdateError {
1504 pub fn fatal() -> Self {
1505 UpdateError::Failed(RunFailed::fatal())
1506 }
1507}
1508
1509impl From<Failed> for UpdateError {
1510 fn from(_: Failed) -> Self {
1511 UpdateError::Failed(RunFailed::fatal())
1512 }
1513}
1514
1515impl From<RunFailed> for UpdateError {
1516 fn from(err: RunFailed) -> Self {
1517 UpdateError::Failed(err)
1518 }
1519}
1520
1521
1522//============ Tests =========================================================
1523
1524#[cfg(test)]
1525mod test {
1526 use std::str::FromStr;
1527 use super::*;
1528
1529 #[test]
1530 fn read_write() {
1531 let dir = tempfile::tempdir().unwrap();
1532
1533 let path = dir.path().join("stored.bin");
1534 let manifest_uri = uri::Rsync::from_str(
1535 "rsync://example.com/test/test.mft"
1536 ).unwrap();
1537 let rpki_notify = Some(uri::Https::from_str(
1538 "https://example.com/notification.xml"
1539 ).unwrap());
1540
1541 // StoredManifest::new is too smart, so we need to make this manually.
1542 let manifest = StoredManifest {
1543 not_after: Time::utc(2025, 10, 1, 16, 10, 22),
1544 manifest_number: Serial::default(),
1545 this_update: Time::utc(2010, 6, 3, 8, 11, 0),
1546 ca_repository: uri::Rsync::from_str(
1547 "rsync://example.com/test/"
1548 ).unwrap(),
1549 manifest: Bytes::from_static(b"deadbeef"),
1550 crl_uri: uri::Rsync::from_str(
1551 "rsync://example.com/test/test.crl"
1552 ).unwrap(),
1553 crl: Bytes::from_static(b"crlbytesgohere"),
1554 };
1555
1556 let objects = [
1557 StoredObject::new(
1558 uri::Rsync::from_str(
1559 "rsync://example.com/test/obj1.bin"
1560 ).unwrap(),
1561 Bytes::from_static(b"object1content"),
1562 Some(ManifestHash::new(
1563 Bytes::copy_from_slice(
1564 DigestAlgorithm::sha256().digest(
1565 b"object1content"
1566 ).as_ref()
1567 ),
1568 DigestAlgorithm::sha256()
1569 ))
1570 ),
1571 StoredObject::new(
1572 uri::Rsync::from_str(
1573 "rsync://example.com/test/obj2.bin"
1574 ).unwrap(),
1575 Bytes::from_static(b"object2stuff"),
1576 None,
1577 ),
1578 ];
1579
1580 let mut point = StoredPoint::open(
1581 path.clone(), &manifest_uri, rpki_notify.as_ref()
1582 ).unwrap();
1583 let mut objects_iter = objects.iter();
1584 point._update(
1585 NamedTempFile::new_in(&dir).unwrap(),
1586 manifest.clone(),
1587 || Ok(objects_iter.next().cloned())
1588 ).unwrap();
1589 drop(point);
1590
1591 let mut point = StoredPoint::load_quietly(path.clone()).unwrap();
1592 assert_eq!(point.manifest, Some(manifest));
1593 assert_eq!(point.next().unwrap().unwrap(), objects[0]);
1594 assert_eq!(point.next().unwrap().unwrap(), objects[1]);
1595 assert!(point.next().is_none());
1596 }
1597}
1598