use crate::build_targets;
use crate::common::UNUSED_URL;
use crate::datetime::parse_datetime;
use crate::error::{self, Result};
use crate::source::parse_key_source;
use chrono::{DateTime, Utc};
use clap::Parser;
use snafu::{OptionExt, ResultExt};
use std::fs::File;
use std::num::{NonZeroU64, NonZeroUsize};
use std::path::{Path, PathBuf};
use tough::editor::signed::PathExists;
use tough::editor::RepositoryEditor;
use tough::key_source::KeySource;
use tough::{ExpirationEnforcement, RepositoryLoader};
use url::Url;
#[derive(Debug, Parser)]
pub(crate) struct UpdateArgs {
#[clap(short = 'k', long = "key", required = true, parse(try_from_str = parse_key_source))]
keys: Vec<Box<dyn KeySource>>,
#[clap(long = "snapshot-version")]
snapshot_version: NonZeroU64,
#[clap(long = "snapshot-expires", parse(try_from_str = parse_datetime))]
snapshot_expires: DateTime<Utc>,
#[clap(long = "targets-version")]
targets_version: NonZeroU64,
#[clap(long = "targets-expires", parse(try_from_str = parse_datetime))]
targets_expires: DateTime<Utc>,
#[clap(long = "timestamp-version")]
timestamp_version: NonZeroU64,
#[clap(long = "timestamp-expires", parse(try_from_str = parse_datetime))]
timestamp_expires: DateTime<Utc>,
#[clap(short = 'r', long = "root")]
root: PathBuf,
#[clap(short = 'm', long = "metadata-url")]
metadata_base_url: Url,
#[clap(short = 't', long = "add-targets")]
targets_indir: Option<PathBuf>,
#[clap(long = "target-path-exists", default_value = "skip")]
target_path_exists: PathExists,
#[clap(short = 'f', long = "follow")]
follow: bool,
#[clap(short = 'j', long = "jobs")]
jobs: Option<NonZeroUsize>,
#[clap(short = 'o', long = "outdir")]
outdir: PathBuf,
#[clap(short = 'i', long = "incoming-metadata")]
indir: Option<Url>,
#[clap(long = "role")]
role: Option<String>,
#[clap(long)]
allow_expired_repo: bool,
}
fn expired_repo_warning<P: AsRef<Path>>(path: P) {
#[rustfmt::skip]
eprintln!("\
=================================================================
Updating repo at {}
WARNING: `--allow-expired-repo` was passed; this is unsafe and will not establish trust, use only for testing!
=================================================================",
path.as_ref().display());
}
impl UpdateArgs {
pub(crate) fn run(&self) -> Result<()> {
let expiration_enforcement = if self.allow_expired_repo {
expired_repo_warning(&self.outdir);
ExpirationEnforcement::Unsafe
} else {
ExpirationEnforcement::Safe
};
let repository = RepositoryLoader::new(
File::open(&self.root).context(error::OpenRootSnafu { path: &self.root })?,
self.metadata_base_url.clone(),
Url::parse(UNUSED_URL).context(error::UrlParseSnafu { url: UNUSED_URL })?,
)
.expiration_enforcement(expiration_enforcement)
.load()
.context(error::RepoLoadSnafu)?;
self.update_metadata(
RepositoryEditor::from_repo(&self.root, repository)
.context(error::EditorFromRepoSnafu { path: &self.root })?,
)
}
fn update_metadata(&self, mut editor: RepositoryEditor) -> Result<()> {
editor
.targets_version(self.targets_version)
.context(error::DelegationStructureSnafu)?
.targets_expires(self.targets_expires)
.context(error::DelegationStructureSnafu)?
.snapshot_version(self.snapshot_version)
.snapshot_expires(self.snapshot_expires)
.timestamp_version(self.timestamp_version)
.timestamp_expires(self.timestamp_expires);
if let Some(ref targets_indir) = self.targets_indir {
if let Some(jobs) = self.jobs {
rayon::ThreadPoolBuilder::new()
.num_threads(usize::from(jobs))
.build_global()
.context(error::InitializeThreadPoolSnafu)?;
}
let new_targets = build_targets(&targets_indir, self.follow)?;
for (target_name, target) in new_targets {
editor
.add_target(target_name, target)
.context(error::DelegationStructureSnafu)?;
}
};
if self.role.is_some() && self.indir.is_some() {
editor
.sign_targets_editor(&self.keys)
.context(error::DelegationStructureSnafu)?
.update_delegated_targets(
self.role.as_ref().context(error::MissingSnafu {
what: "delegated role",
})?,
self.indir
.as_ref()
.context(error::MissingSnafu {
what: "delegated role metadata url",
})?
.as_str(),
)
.context(error::DelegateeNotFoundSnafu {
role: self.role.as_ref().unwrap().clone(),
})?;
}
let signed_repo = editor.sign(&self.keys).context(error::SignRepoSnafu)?;
if let Some(ref targets_indir) = self.targets_indir {
let targets_outdir = &self.outdir.join("targets");
signed_repo
.link_targets(&targets_indir, &targets_outdir, self.target_path_exists)
.context(error::LinkTargetsSnafu {
indir: &targets_indir,
outdir: targets_outdir,
})?;
};
let metadata_dir = &self.outdir.join("metadata");
signed_repo
.write(metadata_dir)
.context(error::WriteRepoSnafu {
directory: metadata_dir,
})?;
Ok(())
}
}