#![deny(rust_2018_idioms)]
#![warn(clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
// Identifiers like Command::Create are clearer than Self::Create regardless of context
clippy::use_self,
// Caused by interacting with tough::schema::*._extra
clippy::used_underscore_binding,
clippy::result_large_err,
)]
mod add_key_role;
mod add_role;
mod clone;
mod common;
mod create;
mod create_role;
mod datetime;
mod download;
mod download_root;
mod error;
mod remove_key_role;
mod remove_role;
mod root;
mod source;
mod transfer_metadata;
mod update;
mod update_targets;
use crate::error::Result;
use clap::Parser;
use futures::{StreamExt, TryStreamExt};
use rustls::crypto::{aws_lc_rs, CryptoProvider};
use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode};
use snafu::{ErrorCompat, OptionExt, ResultExt};
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
use tokio::runtime::Handle;
use tough::schema::Target;
use tough::TargetName;
use walkdir::WalkDir;
static SPEC_VERSION: &str = "1.0.0";
#[derive(Parser)]
#[command(version)]
struct Program {
#[arg(id = "log-level", short, long, default_value = "info")]
log_level: LevelFilter,
#[command(subcommand)]
cmd: Command,
}
impl Program {
async fn run(self) -> Result<()> {
TermLogger::init(
self.log_level,
ConfigBuilder::new()
.add_filter_allow_str("tuftool")
.add_filter_allow_str("tough")
.build(),
TerminalMode::Mixed,
ColorChoice::Auto,
)
.context(error::LoggerSnafu)?;
if CryptoProvider::get_default().is_none() {
let _ = aws_lc_rs::default_provider().install_default();
}
self.cmd.run().await
}
}
#[derive(Debug, Parser)]
enum Command {
Clone(clone::CloneArgs),
Create(create::CreateArgs),
Delegation(Delegation),
Download(download::DownloadArgs),
#[command(subcommand)]
Root(root::Command),
TransferMetadata(transfer_metadata::TransferMetadataArgs),
Update(Box<update::UpdateArgs>),
}
impl Command {
async fn run(self) -> Result<()> {
match self {
Command::Create(args) => args.run().await,
Command::Root(root_subcommand) => root_subcommand.run().await,
Command::Download(args) => args.run().await,
Command::Update(args) => args.run().await,
Command::Delegation(cmd) => cmd.run().await,
Command::Clone(cmd) => cmd.run().await,
Command::TransferMetadata(cmd) => cmd.run().await,
}
}
}
async fn load_file<T>(path: &Path) -> Result<T>
where
for<'de> T: serde::Deserialize<'de>,
{
serde_json::from_slice(
&tokio::fs::read(path)
.await
.context(error::FileOpenSnafu { path })?,
)
.context(error::FileParseJsonSnafu { path })
}
async fn write_file<T>(path: &Path, json: T) -> Result<()>
where
T: serde::Serialize + Send + Sync + 'static,
{
let parent = path
.parent()
.context(error::PathParentSnafu { path })?
.to_path_buf();
let path = path.to_path_buf();
let rt = Handle::current();
let task = rt.spawn_blocking(move || {
let file =
NamedTempFile::new_in(&parent).context(error::FileTempCreateSnafu { path: parent })?;
let (mut file, tmp_path) = file.into_parts();
let buf =
serde_json::to_vec_pretty(&json).context(error::FileWriteJsonSnafu { path: &path })?;
file.write_all(&buf)
.context(error::FileWriteSnafu { path: &path })?;
NamedTempFile::from_parts(file, tmp_path)
.persist(&path)
.context(error::FilePersistSnafu { path })?;
Ok(())
});
task.await.context(error::JoinTaskSnafu)?
}
async fn build_targets<P>(indir: P, follow_links: bool) -> Result<HashMap<TargetName, Target>>
where
P: AsRef<Path>,
{
let indir = indir.as_ref().to_owned();
let (tx, rx) = tokio::sync::mpsc::channel(10);
let indir_clone = indir.clone();
tokio::task::spawn_blocking(move || -> Result<()> {
let walker = WalkDir::new(indir_clone.clone()).follow_links(follow_links);
for entry in walker {
if tx.blocking_send(entry).is_err() {
break;
}
}
Ok(())
});
let join_handles =
futures::stream::unfold(
rx,
move |mut rx| async move { Some((rx.recv().await?, rx)) },
)
.filter_map(|entry| {
let indir = indir.clone();
async move {
match entry {
Ok(entry) => {
if entry.file_type().is_file() {
let future = async move { process_target(entry.path()).await };
Some(Ok(tokio::task::spawn(future)))
} else {
None
}
}
Err(err) => Some(Err(err).context(error::WalkDirSnafu { directory: indir })),
}
}
})
.try_collect::<Vec<_>>()
.await?;
futures::future::try_join_all(join_handles)
.await
.context(error::JoinTaskSnafu {})?
.into_iter()
.collect()
}
async fn process_target(path: &Path) -> Result<(TargetName, Target)> {
let target_name = TargetName::new(
path.file_name()
.context(error::NoFileNameSnafu { path })?
.to_str()
.context(error::PathUtf8Snafu { path })?,
)
.context(error::InvalidTargetNameSnafu)?;
let target = Target::from_path(path)
.await
.context(error::TargetFromPathSnafu { path })?;
Ok((target_name, target))
}
#[tokio::main]
async fn main() -> ! {
std::process::exit(match Program::parse().run().await {
Ok(()) => 0,
Err(err) => {
eprintln!("{err}");
if let Some(var) = std::env::var_os("RUST_BACKTRACE") {
if var != "0" {
if let Some(backtrace) = err.backtrace() {
eprintln!("\n{backtrace:?}");
}
}
}
1
}
})
}
#[derive(Parser, Debug)]
struct Delegation {
#[arg(long = "signing-role", required = true)]
role: String,
#[command(subcommand)]
cmd: DelegationCommand,
}
impl Delegation {
async fn run(self) -> Result<()> {
self.cmd.run(&self.role).await
}
}
#[derive(Debug, Parser)]
enum DelegationCommand {
AddKey(Box<add_key_role::AddKeyArgs>),
AddRole(Box<add_role::AddRoleArgs>),
CreateRole(Box<create_role::CreateRoleArgs>),
Remove(Box<remove_role::RemoveRoleArgs>),
RemoveKey(Box<remove_key_role::RemoveKeyArgs>),
UpdateDelegatedTargets(Box<update_targets::UpdateTargetsArgs>),
}
impl DelegationCommand {
async fn run(self, role: &str) -> Result<()> {
match self {
DelegationCommand::CreateRole(args) => args.run(role).await,
DelegationCommand::AddRole(args) => args.run(role).await,
DelegationCommand::UpdateDelegatedTargets(args) => args.run(role).await,
DelegationCommand::AddKey(args) => args.run(role).await,
DelegationCommand::RemoveKey(args) => args.run(role).await,
DelegationCommand::Remove(args) => args.run(role).await,
}
}
}
#[test]
fn verify_program_cli() {
use clap::CommandFactory;
Program::command().debug_assert();
}
#[test]
fn verify_command_cli() {
use clap::CommandFactory;
Command::command().debug_assert();
}
#[test]
fn verify_delegation_cli() {
use clap::CommandFactory;
Delegation::command().debug_assert();
}