Add a configuration file and associated management commands.

- Add a configuration file for sq, and sq config get to
    programmatically query configuration values, and sq config template
    to create a template as a starting point for a custom configuration
    file.

  - As a first step, the following things have been made configurable:

    - The cipher suite for key generation.
    - The set of keyservers.
    - The cryptographic policy, which can be sourced from an external
      file as well as modified inline.

  - If there is no configuration file, sq config template can be used to
    create a template for the user to modify.

  - If a default has been overridden using the configuration file,
    sq's --help output is augmented with the configured value.
This commit is contained in:
Justus Winter 2024-11-27 15:26:36 +01:00
parent 3b1bd79195
commit 4b3f2c97ad
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
23 changed files with 2015 additions and 62 deletions

28
Cargo.lock generated
View File

@ -3458,6 +3458,7 @@ dependencies = [
name = "sequoia-sq"
version = "0.39.0"
dependencies = [
"aho-corasick",
"anyhow",
"assert_cmd",
"buffered-reader",
@ -3496,6 +3497,7 @@ dependencies = [
"textwrap",
"thiserror",
"tokio",
"toml_edit",
"typenum",
]
@ -4083,6 +4085,23 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower-service"
version = "0.3.3"
@ -4669,6 +4688,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"

View File

@ -29,6 +29,7 @@ gitlab = { repository = "sequoia-pgp/sequoia-sq" }
maintenance = { status = "actively-developed" }
[dependencies]
aho-corasick = "1"
buffered-reader = { version = "1.3.1", default-features = false, features = ["compression"] }
dirs = "5"
fs_extra = "1"
@ -36,7 +37,7 @@ sequoia-directories = "0.1"
sequoia-openpgp = { version = "1.18", default-features = false, features = ["compression"] }
sequoia-autocrypt = { version = "0.25", default-features = false }
sequoia-net = { version = "0.28", default-features = false }
sequoia-policy-config = ">= 0.6, <0.8"
sequoia-policy-config = ">= 0.7, <0.8"
anyhow = "1.0.18"
chrono = "0.4.10"
clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] }
@ -52,6 +53,8 @@ sequoia-wot = { version = "0.13.2", default-features = false }
tempfile = "3.1"
thiserror = "1"
tokio = { version = "1.13.1" }
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse"] }
regex = "1"
rpassword = "7.0"
serde = { version = "1.0.137", features = ["derive"] }
terminal_size = ">=0.2.6, <0.5"

3
NEWS
View File

@ -121,6 +121,9 @@
removed: if a secret key is provided as file input, it will be
emitted.
- The argument `sq key subkey export --cert-file` has been removed.
- `sq` now reads a configuration file that can be used to tweak a
number of defaults, like the cipher suite to generate new keys,
the set of key servers to query, and the cryptographic policy.
* Changes in 0.39.0
** Notable changes

105
src/cli/config.rs Normal file
View File

@ -0,0 +1,105 @@
//! Command-line parser for `sq config`.
use std::{
collections::BTreeMap,
sync::OnceLock,
};
use clap::{
Parser,
Subcommand,
};
use sequoia_directories::Home;
pub mod get;
pub mod set;
pub mod template;
/// Computes the path to the config file even if argument parsing
/// failed.
///
/// This happens notably if `--help` is given.
pub fn find_home() -> Option<Home> {
let args = std::env::args().collect::<Vec<_>>();
for (i, arg) in args.iter().enumerate() {
if arg == "--" {
break;
}
if arg.starts_with("--home=") {
return Home::new(Some(arg["--home=".len()..].into())).ok();
}
if arg == "--home" {
if let Some(home) = args.get(i + 1) {
return Home::new(Some(home.into())).ok();
}
}
}
Home::new(None).ok()
}
/// Values read from the config file to be included in help messages.
pub type Augmentations = BTreeMap<&'static str, String>;
/// Includes values from the config file in help messages.
pub fn augment_help(key: &'static str, text: &str) -> String {
if let Some(a) = AUGMENTATIONS.get().and_then(|a| a.get(key)) {
format!("{}\n\n[config: {}] (overrides default)", text, a)
} else {
text.into()
}
}
/// Includes values from the config file in help messages.
pub fn set_augmentations(augmentations: Augmentations) {
AUGMENTATIONS.set(augmentations)
.expect("augmentations must only be set once");
}
/// Values read from the config file to be included in help messages.
static AUGMENTATIONS: OnceLock<Augmentations> = OnceLock::new();
#[derive(Debug, Parser)]
#[clap(
name = "config",
about = "Get configuration options",
long_about = format!("\
Get configuration options
This subcommand can be used to inspect the configuration \
file{}, and to create a template that can be edited to your liking.
",
sequoia_directories::Home::default()
.map(|home| {
let p = home.config_dir(sequoia_directories::Component::Sq);
let p = p.join("config.toml");
let p = p.display().to_string();
if let Some(home) = dirs::home_dir() {
let home = home.display().to_string();
if let Some(rest) = p.strip_prefix(&home) {
return format!(" (default location: $HOME{})",
rest);
}
}
p
})
.unwrap_or("".to_string())),
subcommand_required = true,
arg_required_else_help = true,
disable_help_subcommand = true,
)]
pub struct Command {
#[clap(subcommand)]
pub subcommand: Subcommands,
}
#[derive(Debug, Subcommand)]
#[non_exhaustive]
pub enum Subcommands {
Get(get::Command),
Template(template::Command),
}

45
src/cli/config/get.rs Normal file
View File

@ -0,0 +1,45 @@
//! Command-line parser for `sq config get`.
use clap::Args;
use crate::cli::examples::*;
#[derive(Debug, Args)]
#[clap(
name = "get",
about = "Get configuration options",
long_about = "\
Get configuration options
Retrieves the configuration with the given key. Use `sq config get` \
to see all available options and their values.",
after_help = GET_EXAMPLES,
)]
pub struct Command {
#[clap(
value_name = "NAME",
help = "Get the value of the configuration NAME",
)]
pub name: Option<String>,
}
const GET_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
List all configuration options.",
command: &[
"sq", "config", "get",
],
}),
Action::Example(Example {
comment: "\
Get the default cipher suite for key generation.",
command: &[
"sq", "config", "get", "key.generate.cipher-suite",
],
}),
]
};
test_examples!(sq_config_get, GET_EXAMPLES);

90
src/cli/config/set.rs Normal file
View File

@ -0,0 +1,90 @@
//! Command-line parser for `sq config`.
use clap::Args;
use crate::cli::examples::*;
// XXX: We don't currently expose the set command.
#[derive(Debug, Args)]
#[clap(
name = "set",
about = "Set configuration options",
long_about = "\
Set configuration options
Changes the configuration with the given key. Use `sq config get` \
to see all existing options and their values.
",
after_help = SET_EXAMPLES,
)]
// XXX: value and delete should be in an argument group, but doing
// that messes up the usage:
//
// Usage: sq config set <VALUE|--delete> <NAME>
//
// Note how VALUE comes first. I believe this is tracked upstream as
// https://github.com/clap-rs/clap/issues/1794
//
// For now, we do the validation in the command handler.
//
//#[clap(group(ArgGroup::new("action").args(&["value", "delete"]).required(true)))]
pub struct Command {
#[clap(
value_name = "NAME",
help = "Set the value of the configuration NAME",
)]
pub name: String,
#[clap(
value_name = "VALUE",
help = "New value for the configuration item",
)]
pub value: Option<String>,
#[clap(
long = "delete",
help = "Delete the configuration item",
conflicts_with = "value",
)]
pub delete: bool,
#[clap(
long = "add",
help = "Add an item to a list of items",
conflicts_with = "delete",
)]
pub add: bool,
}
const SET_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
Set the default cipher suite for key generation.",
command: &[
"sq", "config", "set", "key.generate.cipher-suite",
"rsa3k",
],
}),
Action::Example(Example {
comment: "\
Delete the default cipher suite for key generation.",
command: &[
"sq", "config", "set", "key.generate.cipher-suite",
"--delete",
],
}),
Action::Example(Example {
comment: "\
Add a default key server for network queries.",
command: &[
"sq", "config", "set", "network.keyservers",
"--add", "hkps://keys.example.org",
],
}),
]
};
// XXX: We don't currently expose the set command.
//test_examples!(sq_config_set, SET_EXAMPLES);

View File

@ -0,0 +1,42 @@
//! Command-line parser for `sq config template`.
use clap::Args;
use crate::cli::{
examples::*,
types::{ClapData, FileOrStdout},
};
#[derive(Debug, Args)]
#[clap(
name = "template",
about = "Write a template configuration file",
long_about = "\
Write a template configuration file
Writes a template containing the default values to the given file or stdout. \
This can be used as a starting point to tweak the configuration.",
after_help = TEMPLATE_EXAMPLES,
)]
pub struct Command {
#[clap(
long,
value_name = FileOrStdout::VALUE_NAME,
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP_OPTIONAL,
)]
pub output: FileOrStdout,
}
const TEMPLATE_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
Write a template configuration.",
command: &[
"sq", "config", "template",
],
}),
]
};
test_examples!(sq_config_template, TEMPLATE_EXAMPLES);

View File

@ -7,6 +7,7 @@ use openpgp::packet::UserID;
use crate::cli::KEY_VALIDITY_DURATION;
use crate::cli::KEY_VALIDITY_IN_YEARS;
use crate::cli::config;
use crate::cli::types::ClapData;
use crate::cli::types::EncryptPurpose;
use crate::cli::types::Expiration;
@ -134,11 +135,17 @@ Canonical user IDs are of the form `Name (Comment) \
long = "cipher-suite",
value_name = "CIPHER-SUITE",
default_value_t = Default::default(),
help = "Select the cryptographic algorithms for the key",
help = config::augment_help(
"key.generate.cipher-suite",
"Select the cryptographic algorithms for the key"),
value_enum,
)]
pub cipher_suite: CipherSuite,
/// Workaround for https://github.com/clap-rs/clap/issues/3846
#[clap(skip)]
pub cipher_suite_source: Option<clap::parser::ValueSource>,
#[clap(
long = "new-password-file",
value_name = "PASSWORD_FILE",

View File

@ -9,6 +9,7 @@ use examples::Actions;
use examples::Example;
use examples::Setup;
use crate::cli::config;
use crate::cli::key::CipherSuite;
use crate::cli::types::CertDesignators;
use crate::cli::types::ClapData;
@ -90,11 +91,17 @@ pub struct Command {
long,
value_name = "CIPHER-SUITE",
default_value_t = CipherSuite::Cv25519,
help = "Select the cryptographic algorithms for the subkey",
help = config::augment_help(
"key.generate.cipher-suite",
"Select the cryptographic algorithms for the subkey"),
value_enum,
)]
pub cipher_suite: CipherSuite,
/// Workaround for https://github.com/clap-rs/clap/issues/3846
#[clap(skip)]
pub cipher_suite_source: Option<clap::parser::ValueSource>,
#[command(flatten)]
pub expiration: ExpirationArg,

View File

@ -93,6 +93,7 @@ use openpgp::Fingerprint;
pub mod examples;
pub mod cert;
pub mod config;
pub mod decrypt;
pub mod download;
pub mod encrypt;
@ -565,5 +566,6 @@ pub enum SqSubcommands {
Keyring(keyring::Command),
Packet(packet::Command),
Config(config::Command),
Version(version::Command),
}

View File

@ -1,5 +1,6 @@
use clap::{Args, Parser, Subcommand};
use crate::cli::config;
use crate::cli::examples::*;
use crate::cli::types::ClapData;
use crate::cli::types::FileOrCertStore;
@ -38,7 +39,9 @@ pub struct Command {
// that they are sorted to the bottom.
display_order = 800,
value_name = "URI",
help = "Set the key server to use. Can be given multiple times.",
help = config::augment_help(
"network.keyserver.servers",
"Set a key server to use. Can be given multiple times."),
)]
pub servers: Vec<String>,
#[clap(subcommand)]

View File

@ -1,5 +1,6 @@
use clap::Parser;
use crate::cli::config;
use crate::cli::examples::Action;
use crate::cli::examples::Actions;
use crate::cli::examples::Example;
@ -50,10 +51,16 @@ pub struct Command {
long = "server",
default_values_t = DEFAULT_KEYSERVERS.iter().map(ToString::to_string),
value_name = "URI",
help = "Set the key server to use. Can be given multiple times.",
help = config::augment_help(
"network.keyserver.servers",
"Set a key server to use. Can be given multiple times."),
)]
pub servers: Vec<String>,
/// Workaround for https://github.com/clap-rs/clap/issues/3846
#[clap(skip)]
pub servers_source: Option<clap::parser::ValueSource>,
#[clap(
help = FileOrCertStore::HELP_OPTIONAL,
long,

View File

@ -1,5 +1,7 @@
use std::time::SystemTime;
use clap::ArgMatches;
use sequoia_openpgp as openpgp;
use openpgp::cert::prelude::*;
use openpgp::{Cert, Result};
@ -16,6 +18,7 @@ use crate::cli::{SqCommand, SqSubcommands};
pub mod autocrypt;
pub mod cert;
pub mod config;
pub mod decrypt;
pub mod download;
pub mod encrypt;
@ -30,8 +33,9 @@ pub mod verify;
pub mod version;
/// Dispatches the top-level subcommand.
pub fn dispatch(sq: Sq, command: SqCommand) -> Result<()>
pub fn dispatch(sq: Sq, command: SqCommand, matches: &ArgMatches) -> Result<()>
{
let matches = matches.subcommand().unwrap().1;
match command.subcommand {
SqSubcommands::Encrypt(command) =>
encrypt::dispatch(sq, command),
@ -50,18 +54,21 @@ pub fn dispatch(sq: Sq, command: SqCommand) -> Result<()>
SqSubcommands::Cert(command) =>
cert::dispatch(sq, command),
SqSubcommands::Key(command) =>
key::dispatch(sq, command),
key::dispatch(sq, command, matches),
SqSubcommands::Pki(command) =>
pki::dispatch(sq, command),
SqSubcommands::Network(command) =>
network::dispatch(sq, command),
network::dispatch(sq, command, matches),
SqSubcommands::Keyring(command) =>
keyring::dispatch(sq, command),
SqSubcommands::Packet(command) =>
packet::dispatch(sq, command),
SqSubcommands::Config(command) =>
config::dispatch(sq, command),
SqSubcommands::Version(command) =>
version::dispatch(sq, command),
}

213
src/commands/config.rs Normal file
View File

@ -0,0 +1,213 @@
//! Configuration model inspection and manipulation.
use std::collections::BTreeMap;
use anyhow::{Context, Result};
use toml_edit::{
Item,
Value,
};
use crate::toml_edit_tree::{
Error,
Node,
Path,
PathComponent,
TraversalError,
};
use crate::{
Sq,
cli::config,
config::ConfigFile,
};
pub fn dispatch(sq: Sq, cmd: config::Command)
-> Result<()>
{
match cmd.subcommand {
config::Subcommands::Get(c) => get(sq, c),
config::Subcommands::Template(c) => template(sq, c),
}
}
/// Implements `sq config get`.
fn get(sq: Sq, cmd: config::get::Command) -> Result<()> {
let path = if let Some(name) = &cmd.name {
name.parse()?
} else {
Path::empty()
};
// We do two lookups, first in the configuration file, then in the
// default configuration, and collate the results.
let mut acc = Default::default();
// First, look in the configuration.
let config = sq.config_file.augment_with_policy(&sq.policy)?;
let r0 = Node::traverse(&*config.as_item() as _, &path)
.map_err(Into::into)
.and_then(
|node| collect(&mut acc, path.clone(), node, &|_, _| true));
// Then, look in the default configuration. But, we are careful
// to filter out anything overridden in the actual configuration.
let default = ConfigFile::default_config(sq.home.as_ref())?;
let r1 = Node::traverse(&*default.as_item() as _, &path)
.map_err(Into::into)
.and_then(
|node| collect(&mut acc, path, node, &|p, n| {
(n.as_atomic_value().is_some() || n.as_array().is_some())
^ config.as_item().traverse(p).is_ok()
}));
// One of the lookups must be successful.
r0.or(r1)?;
// Display sorted and deduplicated results.
for (k, v) in acc {
eprintln!("{} = {}", k, v);
}
Ok(())
}
/// Collects all nodes below `node` at `path` matching `filter` into
/// `acc`.
pub fn collect<F>(acc: &mut BTreeMap<String, String>,
mut path: Path, node: &dyn Node, filter: &F)
-> Result<Path>
where
F: for<'a> Fn(&'a Path, &'a dyn Node) -> bool,
{
if ! (filter)(&path, node) {
return Ok(path);
}
if let Some(v) = node.as_atomic_value() {
acc.insert(path.to_string(),
v.clone().decorated("", "").to_string());
} else {
for (k, v) in node.iter() {
path.push(k);
path = collect(acc, path, v, filter)?;
path.pop();
}
}
Ok(path)
}
/// Implements `sq config set`.
///
/// XXX: Currently, we don't expose this due to problems with
/// toml-edit. Notably, toml-edit doesn't handle comments well,
/// attaching them as decor to nodes in the document tree.
/// Manipulating the document tree may delete or otherwise disturb the
/// comments in a way that badly distorts the semantics.
#[allow(dead_code)]
fn set(mut sq: Sq, cmd: config::set::Command) -> Result<()> {
let mut path: Path = cmd.name.parse()?;
if path.is_empty() {
return Err(anyhow::anyhow!("NAME must not be empty"));
};
let last = path.pop().expect("path is not empty");
// XXX: Workaround for clap bug, see src/cli/config.rs.
if cmd.value.is_none() && ! cmd.delete {
return Err(anyhow::anyhow!("Either VALUE or --delete must be given"));
}
let mut config = std::mem::take(&mut sq.config_file);
let doc = config.as_item_mut();
if let Some(value) = &cmd.value {
let value: Value = value.parse().unwrap_or_else(|_| value.into());
// Like Node::traverse_mut, but we also create intermediate
// nodes on demand.
let mut node: &mut dyn Node = doc as _;
for (i, pc) in path.iter().cloned().enumerate() {
let type_name = node.type_name();
if let Err(TraversalError::KeyNotFound(_, _)) =
node.get_mut(&pc)
.map_err(|e| e.with_context(&path, i, type_name))
{
match path.get(i + 1).unwrap_or(&last) {
PathComponent::Symbol(_) => {
// Prefer to insert a non-inline table if
// possible.
if let Some(t) = node.as_table_mut() {
t.insert(pc.as_symbol()?,
Item::Table(Default::default()));
} else {
node.set(&pc,
Value::InlineTable(Default::default()))?;
}
},
PathComponent::Index(_) =>
node.set(&pc, Value::Array(Default::default()))?,
};
}
node = node.get_mut(&pc)
.map_err(|e| e.with_context(&path, i, type_name))?;
}
if cmd.add {
if let Err(Error::KeyNotFound(_)) = node.get_mut(&last) {
// The node doesn't exist, see if it exists in the
// default configuration.
let default = ConfigFile::default_config(sq.home.as_ref())?;
let v = Node::traverse(&*default.as_item() as _, &path).ok()
.and_then(|n| n.get(&last).ok())
.and_then(|n| n.as_array())
.map(|a| Value::Array(a.clone()))
.unwrap_or(Value::Array(Default::default()));
node.set(&last, v)?;
}
let type_name = node.type_name();
node = node.get_mut(&last)
.map_err(|e| e.with_context(&path, path.len(), type_name))?;
let type_name = node.type_name();
path.push(last);
if let Some(a) = node.as_array_mut() {
a.push(value);
} else {
return Err(anyhow::anyhow!("Tried to add an element to {}, \
but this is a {} not an array",
path, type_name));
}
} else {
node.set(&last, value)?;
}
} else {
assert!(cmd.delete);
let node = Node::traverse_mut(&mut *doc as _, &path)?;
node.remove(&last)?;
}
// Verify the configuration.
config.verify()
.with_context(|| format!("Failed to {} {:?}",
if cmd.delete { "delete" } else { "set" },
cmd.name))?;
// The updated config verified, now persist it.
config.persist(
sq.home.as_ref().ok_or(anyhow::anyhow!("No home directory given"))?)?;
Ok(())
}
/// Implements `sq config template`.
fn template(sq: Sq, cmd: config::template::Command) -> Result<()> {
let mut sink = cmd.output.create_safe(&sq)?;
ConfigFile::default_template(sq.home.as_ref())?
.dump(&mut sink)?;
Ok(())
}

View File

@ -1,3 +1,5 @@
use clap::ArgMatches;
use sequoia_openpgp as openpgp;
use openpgp::Result;
@ -20,12 +22,17 @@ use revoke::certificate_revoke;
mod subkey;
pub mod userid;
pub fn dispatch(sq: Sq, command: cli::key::Command) -> Result<()>
pub fn dispatch(sq: Sq, command: cli::key::Command, matches: &ArgMatches)
-> Result<()>
{
let matches = matches.subcommand().unwrap().1;
use cli::key::Subcommands::*;
match command.subcommand {
List(c) => list(sq, c)?,
Generate(c) => generate(sq, c)?,
Generate(mut c) => {
c.cipher_suite_source = matches.value_source("cipher_suite");
generate(sq, c)?
},
Import(c) => import(sq, c)?,
Export(c) => export::dispatch(sq, c)?,
Delete(c) => delete::dispatch(sq, c)?,
@ -33,7 +40,7 @@ pub fn dispatch(sq: Sq, command: cli::key::Command) -> Result<()>
Expire(c) => expire::dispatch(sq, c)?,
Userid(c) => userid::dispatch(sq, c)?,
Revoke(c) => certificate_revoke(sq, c)?,
Subkey(c) => subkey::dispatch(sq, c)?,
Subkey(c) => subkey::dispatch(sq, c, matches)?,
Approvals(c) => approvals::dispatch(sq, c)?,
}
Ok(())

View File

@ -93,8 +93,8 @@ pub fn generate(
// Cipher Suite
builder = builder.set_cipher_suite(
command.cipher_suite.as_ciphersuite()
);
sq.config.cipher_suite(&command.cipher_suite,
command.cipher_suite_source));
// Primary key capabilities.
builder = builder.set_primary_key_flags(

View File

@ -1,3 +1,7 @@
//! Dispatches `sq key subkey`.
use clap::ArgMatches;
use crate::Result;
use crate::Sq;
use crate::cli::key::subkey::Command;
@ -10,9 +14,13 @@ mod export;
mod password;
mod revoke;
pub fn dispatch(sq: Sq, command: Command) -> Result<()> {
pub fn dispatch(sq: Sq, command: Command, matches: &ArgMatches) -> Result<()> {
let matches = matches.subcommand().unwrap().1;
match command {
Command::Add(c) => add::dispatch(sq, c)?,
Command::Add(mut c) => {
c.cipher_suite_source = matches.value_source("cipher_suite");
add::dispatch(sq, c)?
},
Command::Export(c) => export::dispatch(sq, c)?,
Command::Delete(c) => delete::dispatch(sq, c)?,
Command::Password(c) => password::dispatch(sq, c)?,

View File

@ -67,7 +67,9 @@ pub fn dispatch(sq: Sq, command: Command) -> Result<()>
let new_cert = KeyBuilder::new(keyflags)
.set_creation_time(sq.time)
.set_cipher_suite(command.cipher_suite.as_ciphersuite())
.set_cipher_suite(
sq.config.cipher_suite(&command.cipher_suite,
command.cipher_suite_source))
.set_password(password)
.subkey(valid_cert)?
.set_key_validity_period(validity)?

View File

@ -8,6 +8,7 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use clap::ArgMatches;
use indicatif::ProgressBar;
use tokio::task::JoinSet;
@ -75,16 +76,19 @@ pub const CONNECT_TIMEOUT: Duration = Duration::new(5, 0);
/// How long to wait for each individual http request.
pub const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
pub fn dispatch(sq: Sq, c: cli::network::Command)
pub fn dispatch(sq: Sq, c: cli::network::Command, matches: &ArgMatches)
-> Result<()>
{
let matches = matches.subcommand().unwrap().1;
use cli::network::Subcommands;
match c.subcommand {
Subcommands::Search(command) =>
dispatch_search(sq, command),
Subcommands::Search(mut command) => {
command.servers_source = matches.value_source("servers");
dispatch_search(sq, command)
},
Subcommands::Keyserver(command) =>
dispatch_keyserver(sq, command),
dispatch_keyserver(sq, command, matches),
Subcommands::Wkd(command) =>
dispatch_wkd(sq, command),
@ -888,10 +892,13 @@ pub fn dispatch_search(mut sq: Sq, c: cli::network::search::Command)
sq.cert_store_or_else()?;
}
let default_servers = default_keyservers_p(&c.servers);
let default_servers =
matches!(c.servers_source.unwrap(),
clap::parser::ValueSource::DefaultValue);
let http_client = http_client()?;
let servers = c.servers.iter().map(
|uri| KeyServer::with_client(uri, http_client.clone())
let servers = sq.config.key_servers(&c.servers, c.servers_source)
.map(|uri| KeyServer::with_client(uri, http_client.clone())
.with_context(|| format!("Malformed keyserver URI: {}", uri))
.map(Arc::new))
.collect::<Result<Vec<_>>>()?;
@ -1054,28 +1061,20 @@ pub fn dispatch_search(mut sq: Sq, c: cli::network::search::Command)
Ok(())
}
/// Figures out whether the given set of key servers is the default
/// set.
fn default_keyservers_p(servers: &[String]) -> bool {
// XXX: This could be nicer, maybe with a custom clap parser
// that encodes it in the type. For now we live with the
// false positive if someone explicitly provides the same set
// of servers.
use crate::cli::network::keyserver::DEFAULT_KEYSERVERS;
servers.len() == DEFAULT_KEYSERVERS.len()
&& servers.iter().zip(DEFAULT_KEYSERVERS.iter())
.all(|(a, b)| a == b)
}
pub fn dispatch_keyserver(mut sq: Sq,
c: cli::network::keyserver::Command)
-> Result<()>
pub fn dispatch_keyserver(
mut sq: Sq,
c: cli::network::keyserver::Command,
matches: &ArgMatches,
) -> Result<()>
{
make_qprintln!(sq.quiet);
let default_servers = default_keyservers_p(&c.servers);
let servers = c.servers.iter().map(
|uri| KeyServer::with_client(uri, http_client()?)
let servers_source = matches.value_source("servers").unwrap();
let default_servers =
matches!(servers_source, clap::parser::ValueSource::DefaultValue);
let servers = sq.config.key_servers(&c.servers, Some(servers_source))
.map(|uri| KeyServer::with_client(uri, http_client()?)
.with_context(|| format!("Malformed keyserver URI: {}", uri))
.map(Arc::new))
.collect::<Result<Vec<_>>>()?;

697
src/config.rs Normal file
View File

@ -0,0 +1,697 @@
//! Configuration model and file parsing.
use std::{
collections::HashSet,
fs,
io,
path::PathBuf,
time::SystemTime,
};
use aho_corasick::AhoCorasick;
use anyhow::Context;
use clap::{ValueEnum, parser::ValueSource};
use toml_edit::{
DocumentMut,
Item,
Table,
Value,
};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_net::reqwest::Url;
use sequoia_directories::{Component, Home};
use sequoia_policy_config::ConfiguredStandardPolicy;
use crate::{
Result,
cli,
cli::config::Augmentations,
};
/// Represents configuration at runtime.
///
/// This struct is manipulated when parsing the configuration file.
/// It is available as `Sq::config`, with suitable accessors that
/// handle the precedence of the various sources.
pub struct Config {
policy_path: Option<PathBuf>,
policy_inline: Option<Vec<u8>>,
cipher_suite: Option<sequoia_openpgp::cert::CipherSuite>,
key_servers: Option<Vec<Url>>,
}
impl Default for Config {
fn default() -> Self {
Config {
policy_path: None,
policy_inline: None,
cipher_suite: None,
key_servers: None,
}
}
}
impl Config {
/// Returns the cryptographic policy.
///
/// We read in the default policy configuration, the configuration
/// referenced in the configuration file, and the inline policy.
pub fn policy(&mut self, at: SystemTime)
-> Result<StandardPolicy<'static>>
{
let mut policy = ConfiguredStandardPolicy::at(at);
policy.parse_default_config()?;
if let Some(p) = &self.policy_path {
if ! policy.parse_config_file(p)? {
return Err(anyhow::anyhow!(
"referenced policy file {:?} does not exist", p));
}
}
if let Some(p) = &self.policy_inline {
policy.parse_bytes(p)?;
}
Ok(policy.build())
}
/// Returns the cipher suite for generating new keys.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn cipher_suite(&self, cli: &cli::key::CipherSuite,
source: Option<ValueSource>)
-> sequoia_openpgp::cert::CipherSuite
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.cipher_suite.unwrap_or_else(
|| cli.as_ciphersuite()),
_ => cli.as_ciphersuite(),
}
}
/// Returns the key servers to query or publish.
///
/// Handles the precedence of the various sources:
///
/// - If the flag is given, use the given value.
/// - If the command line flag is not given, then
/// - use the value from the configuration file (if any),
/// - or use the default value.
pub fn key_servers<'s>(&'s self, cli: &'s Vec<String>,
source: Option<ValueSource>)
-> impl Iterator<Item = &'s str> + 's
{
match source.expect("set by the cli parser") {
ValueSource::DefaultValue =>
self.key_servers.as_ref()
.map(|s| Box::new(s.iter().map(|s| s.as_str()))
as Box<dyn Iterator<Item = &str>>)
.unwrap_or_else(|| Box::new(cli.iter().map(|s| s.as_str()))
as Box<dyn Iterator<Item = &str>>),
_ => Box::new(cli.iter().map(|s| s.as_str()))
as Box<dyn Iterator<Item = &str>>,
}
}
}
/// Holds the document tree of the configuration file.
#[derive(Debug, Default)]
pub struct ConfigFile {
doc: DocumentMut,
}
impl ConfigFile {
/// A template for the configuration containing the default
/// values.
const TEMPLATE: &'static str = "\
# Configuration template for sq <SQ-VERSION>
<SQ-CONFIG-PATH-HINT>
[key.generate]
#cipher-suite = <DEFAULT-CIPHER-SUITE>
[network]
#keyservers = <DEFAULT-KEY-SERVERS>
[policy]
#path = <DEFAULT-POLICY-FILE>
# The policy can be inlined, either alternatively, or additionally,
# like so:
<DEFAULT-POLICY-INLINE>
";
/// Patterns to match on in `Self::DEFAULT` to be replaced with
/// the default values.
const TEMPLATE_PATTERNS: &'static [&'static str] = &[
"<SQ-VERSION>",
"<SQ-CONFIG-PATH-HINT>",
"<DEFAULT-CIPHER-SUITE>",
"<DEFAULT-KEY-SERVERS>",
"<DEFAULT-POLICY-FILE>",
"<DEFAULT-POLICY-INLINE>",
];
/// Returns a configuration template with the defaults.
fn config_template(path: Option<PathBuf>) -> Result<String> {
let ac = AhoCorasick::new(Self::TEMPLATE_PATTERNS)?;
let mut p = ConfiguredStandardPolicy::new();
p.parse_default_config()?;
let mut default_policy_inline = Vec::new();
p.dump(&mut default_policy_inline,
sequoia_policy_config::DumpDefault::Template)?;
let default_policy_inline =
regex::Regex::new(r"(?m)^\[")?.replace_all(
std::str::from_utf8(&default_policy_inline)?, "[policy.");
Ok(ac.replace_all(Self::TEMPLATE, &[
&env!("CARGO_PKG_VERSION").to_string(),
&if let Some(path) = path {
format!(
"\n\
# To use it, edit it to your liking and write it to\n\
# {}",
&path.display())
} else {
"".into()
},
&format!("{:?}", cli::key::CipherSuite::default().
to_possible_value().unwrap().get_name()),
&format!("{:?}", cli::network::keyserver::DEFAULT_KEYSERVERS),
&format!("{:?}", ConfiguredStandardPolicy::CONFIG_FILE),
&default_policy_inline.to_string(),
]))
}
/// Returns the default configuration in template form.
///
/// All the configuration options with their defaults are
/// commented out.
pub fn default_template(home: Option<&Home>) -> Result<Self> {
let template = Self::config_template(home.map(Self::file_name))?;
let doc: DocumentMut = template.parse()
.context("Parsing default configuration failed")?;
Ok(Self {
doc,
})
}
/// Returns the default configuration.
pub fn default_config(home: Option<&Home>) -> Result<Self> {
let template = Self::config_template(home.map(Self::file_name))?;
// Enable all defaults by commenting-in.
let r = regex::Regex::new(r"(?m)^#([^ ])")?;
let defaults = r.replace_all(&template, "$1");
let doc: DocumentMut = defaults.parse()
.context("Parsing default configuration failed")?;
Ok(Self {
doc,
})
}
/// Returns the path of the config file.
pub fn file_name(home: &Home) -> PathBuf {
home.config_dir(Component::Sq).join("config.toml")
}
/// Reads and validates the configuration file.
pub fn read(&mut self, home: &Home)
-> Result<Config>
{
let mut config = Config::default();
self.read_internal(home, Some(&mut config), None)?;
Ok(config)
}
/// Reads and validates the configuration file.
pub fn read_and_augment(&mut self, home: &Home) -> Result<Augmentations>
{
let mut augmentations = Augmentations::default();
self.read_internal(home, None, Some(&mut augmentations))?;
Ok(augmentations)
}
/// Reads and validates the configuration file, and optionally
/// applies them to the given configuration, and optionally
/// supplies augmentations for the help texts in the command line
/// parser.
fn read_internal(&mut self, home: &Home, mut config: Option<&mut Config>,
mut cli: Option<&mut Augmentations>)
-> Result<()>
{
let path = Self::file_name(home);
let raw = match fs::read_to_string(&path) {
Ok(r) => r,
Err(e) if e.kind() == io::ErrorKind::NotFound =>
Self::config_template(Some(path.clone()))?,
Err(e) => return Err(anyhow::Error::from(e).context(
format!("Reading configuration file {} failed",
path.display()))),
};
let doc: DocumentMut = raw.parse()
.with_context(|| format!("Parsing configuration file {} failed",
path.display()))?;
apply_schema(&mut config, &mut cli, None, doc.iter(), TOP_LEVEL_SCHEMA)
.with_context(|| format!("Parsing configuration file {} failed",
path.display()))?;
self.doc = doc;
Ok(())
}
/// Writes the configuration to the disk.
pub fn persist(&self, home: &Home) -> Result<()> {
let path = Self::file_name(home);
let dir = path.parent().unwrap();
fs::create_dir_all(dir)?;
let mut t =
tempfile::NamedTempFile::new_in(dir)?;
self.dump(&mut t)?;
t.persist(path)?;
Ok(())
}
/// Writes the configuration to the given writer.
pub fn dump(&self, sink: &mut dyn io::Write) -> Result<()> {
write!(sink, "{}", self.doc.to_string())?;
Ok(())
}
/// Verifies the configuration.
pub fn verify(&self) -> Result<()> {
let mut config = Default::default();
apply_schema(&mut Some(&mut config), &mut None, None, self.doc.iter(),
TOP_LEVEL_SCHEMA)?;
config.policy(SystemTime::now())?;
Ok(())
}
/// Augments the configuration with the given policy.
///
/// XXX: Due to the way doc.remove works, it will leave misleading
/// comments behind. Therefore, the resulting configuration is
/// not suitable for dumping, but may only be used for
/// commands::config::get.
pub fn augment_with_policy(&self, p: &StandardPolicy) -> Result<Self> {
use std::io::Write;
let mut raw = Vec::new();
// First, start with our configuration, and drop most of the
// policy with the exception of the path.
let p = ConfiguredStandardPolicy::from_policy(p.clone());
let mut doc = self.doc.clone();
doc.remove("policy");
use crate::toml_edit_tree::Node;
let policy_path: crate::toml_edit_tree::Path
= "policy.path".parse().unwrap();
if let Ok(p) = self.as_item().traverse(&policy_path) {
let p =
p.as_atomic_value().unwrap().as_str().unwrap().to_string();
doc.as_table_mut().insert("policy", Item::Table(
[("path", Value::from(p))]
.into_iter().collect()));
}
write!(&mut raw, "{}", doc.to_string())?;
// Then, augment the configuration with the effective policy.
let mut default_policy_inline = Vec::new();
p.dump(&mut default_policy_inline,
sequoia_policy_config::DumpDefault::Template)?;
let default_policy_inline =
regex::Regex::new(r"(?m)^\[")?.replace_all(
std::str::from_utf8(&default_policy_inline)?, "[policy.");
write!(&mut raw, "{}", default_policy_inline)?;
// Now, parse the resulting configuration.
let doc: DocumentMut = std::str::from_utf8(&raw)?.parse()?;
// Double check that it is well-formed.
apply_schema(&mut None, &mut None, None, doc.iter(), TOP_LEVEL_SCHEMA)?;
Ok(Self {
doc,
})
}
/// Returns the document tree.
pub fn as_item(&self) -> &Item {
self.doc.as_item()
}
/// Returns the mutable document tree.
pub fn as_item_mut(&mut self) -> &mut Item {
self.doc.as_item_mut()
}
}
/// Validates a configuration section using a schema, and optionally
/// applies changes to the configuration and CLI augmentations.
///
/// Returns an error if a key is unknown.
///
/// known_keys better be lowercase.
fn apply_schema<'toml>(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: Option<&str>,
section: toml_edit::Iter<'toml>,
schema: Schema) -> Result<()> {
let section = section.collect::<Vec<_>>();
let known_keys: Vec<_> =
schema.iter().map(|(key, _)| *key).collect();
// Schema keys better be lowercase.
debug_assert!(known_keys.iter().all(|&s| &s.to_lowercase() == s),
"keys in schema must be lowercase");
// Schema keys better be sorted.
debug_assert!(known_keys.windows(2).all(|v| v[0] <= v[1]),
"keys in schema must be sorted");
// XXX: once [].is_sorted is stabilized:
// debug_assert!(known_keys.is_sorted(), "keys in schema must be sorted");
let prefix = if let Some(path) = path {
format!("{}.", path)
} else {
"".to_string()
};
let keys: HashSet<&str> = section
.iter().map(|(key, _value)| *key)
.collect();
// The set of allowed keys are the known keys, plus
// "ignore_invalid", and the value of "ignore_invalid".
let mut allowed_keys: Vec<&str> = known_keys.to_vec();
if let Some(ignore) = section.iter()
.find_map(|(k, v)| (*k == "ignore_invalid").then_some(*v))
{
allowed_keys.push("ignore_invalid");
match ignore {
Item::Value(Value::String(k)) =>
allowed_keys.push(k.value().as_str()),
Item::Value(Value::Array(ks)) => {
for k in ks {
if let Value::String(k) = k {
allowed_keys.push(k.value().as_str());
} else {
Err(Error::ParseError(format!(
"'{}ignore_invalid' takes a string \
or an array of strings",
prefix)))?
}
}
}
_ => {
return Err(Error::ParseError(format!(
"Invalid value for '{}ignore_invalid': {}, \
expected a string or an array of strings",
prefix, ignore)).into());
}
}
}
// Now check if there are any unknown sections.
let unknown_keys = keys
.difference(&allowed_keys.into_iter().collect())
.map(|s| *s)
.collect::<Vec<_>>();
if ! unknown_keys.is_empty() {
return Err(Error::ParseError(format!(
"{} has unknown keys: {}, valid keys are: {}",
if let Some(path) = path {
path
} else {
"top-level section"
},
unknown_keys.join(", "),
// We don't include the keys listed in ignore_invalid.
known_keys.join(", "))).into());
}
// Now validate the values.
for (key, value) in &section {
if let Ok(i) = schema.binary_search_by_key(key, |(k, _)| k) {
let apply = schema[i].1;
(apply)(config, cli, &format!("{}{}", prefix, key), value)
.with_context(|| format!("Error validating {:?}", key))?;
}
}
Ok(())
}
/// Errors used in this module.
///
/// Note: This enum cannot be exhaustively matched to allow future
/// extensions.
#[non_exhaustive]
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
/// Parse error
#[error("Parse error: {0}")]
ParseError(String),
/// A Relative Path was provided where an absolute path was expected.
#[error("Relative path not allowed: {0}")]
RelativePathError(PathBuf),
/// An algorithm is not known to this crate.
#[error("Unknown algorithm: {0}")]
UnknownAlgorithm(String),
#[error("Configuration item {0:?} is not a {1} but a {2}")]
BadType(String, &'static str, &'static str),
}
impl Error {
/// Returns an `Error::BadType` given an item.
fn bad_item_type(path: &str, i: &Item, want_type: &'static str)
-> anyhow::Error
{
Error::BadType(path.into(), want_type, i.type_name()).into()
}
/// Returns an `Error::BadType` given a value.
fn bad_value_type(path: &str, v: &Value, want_type: &'static str)
-> anyhow::Error
{
Error::BadType(path.into(), want_type, v.type_name()).into()
}
}
/// A function that validates a node in the configuration tree with
/// the given path, and optionally makes changes to the configuration
/// and CLI augmentations.
type Applicator = fn(&mut Option<&mut Config>, &mut Option<&mut Augmentations>,
&str, &Item)
-> Result<()>;
/// Ignores a node.
fn apply_nop(_: &mut Option<&mut Config>, _: &mut Option<&mut Augmentations>,
_: &str, _: &Item)
-> Result<()>
{
Ok(())
}
/// A [`Schema`] maps keys to [`Applicator`]s.
type Schema = &'static [(&'static str, Applicator)];
/// Schema for the toplevel.
const TOP_LEVEL_SCHEMA: Schema = &[
("key", apply_key),
("network", apply_network),
("policy", apply_policy),
];
/// Schema for the `key` section.
const KEY_SCHEMA: Schema = &[
("generate", apply_key_generate),
];
/// Validates the `key` section.
fn apply_key(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), KEY_SCHEMA)?;
Ok(())
}
/// Schema for the `key.generate` section.
const KEY_GENERATE_SCHEMA: Schema = &[
("cipher-suite", apply_key_generate_cipher_suite),
];
/// Validates the `key.generate` section.
fn apply_key_generate(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), KEY_GENERATE_SCHEMA)?;
Ok(())
}
/// Validates the `key.generate.cipher-suite` value.
fn apply_key_generate_cipher_suite(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let s = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
let v = cli::key::CipherSuite::from_str(s, true)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if let Some(config) = config {
config.cipher_suite = Some(v.as_ciphersuite());
}
if let Some(cli) = cli {
cli.insert(
"key.generate.cipher-suite",
v.to_possible_value().expect("just validated").get_name().into());
}
Ok(())
}
/// Schema for the `network` section.
const NETWORK_SCHEMA: Schema = &[
("keyservers", apply_network_keyservers),
];
/// Validates the `network` section.
fn apply_network(config: &mut Option<&mut Config>, cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), NETWORK_SCHEMA)?;
Ok(())
}
/// Validates the `network.keyservers` value.
fn apply_network_keyservers(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let list = item.as_array()
.ok_or_else(|| Error::bad_item_type(path, item, "array"))?;
let mut servers_str = Vec::new();
let mut servers_url = Vec::new();
for (i, server) in list.iter().enumerate() {
let server_str = server.as_str()
.ok_or_else(|| Error::bad_value_type(&format!("{}.{}", path, i),
server, "string"))?;
let url = Url::parse(server_str)?;
let s = url.scheme();
match s {
"hkp" => (),
"hkps" => (),
_ => return Err(anyhow::anyhow!(
"must be a hkp:// or hkps:// URL: {}", url)),
}
servers_str.push(server_str);
servers_url.push(url);
}
if let Some(cli) = cli {
cli.insert("network.keyserver.servers", servers_str.join(" "));
}
if let Some(config) = config {
config.key_servers = Some(servers_url);
}
Ok(())
}
/// Schema for the `policy` section.
const POLICY_SCHEMA: Schema = &[
("aead_algorithms", apply_nop),
("asymmetric_algorithms", apply_nop),
("hash_algorithms", apply_nop),
("packets", apply_nop),
("path", apply_policy_path),
("symmetric_algorithms", apply_nop),
];
/// Validates the `policy` section.
fn apply_policy(config: &mut Option<&mut Config>,
cli: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let section = item.as_table_like()
.ok_or_else(|| Error::bad_item_type(path, item, "table"))?;
apply_schema(config, cli, Some(path), section.iter(), POLICY_SCHEMA)?;
if let Some(config) = config {
// Extract the inline policy.
// XXX: This doesn't work because toml_edit bug
// https://github.com/toml-rs/toml/issues/785
//
//let table = section.iter().collect::<Table>();
//
// Instead, we have to use a workaround:
let mut table = Table::new();
section.iter().for_each(|(k, v)| { table.insert(k, v.clone()); });
let mut inline = DocumentMut::from(table);
inline.remove("path");
config.policy_inline = Some(inline.to_string().into_bytes());
}
Ok(())
}
/// Validates the `policy.path` value.
fn apply_policy_path(config: &mut Option<&mut Config>,
_: &mut Option<&mut Augmentations>,
path: &str, item: &Item)
-> Result<()>
{
let path = item.as_str()
.ok_or_else(|| Error::bad_item_type(path, item, "string"))?;
if let Some(config) = config {
config.policy_path = Some(path.into());
}
Ok(())
}

View File

@ -26,6 +26,10 @@ use openpgp::cert::prelude::*;
use clap::FromArgMatches;
// XXX: This could be its own crate, or preferably integrated into
// toml_edit.
mod toml_edit_tree;
#[macro_use] mod macros;
#[macro_use] mod log;
@ -44,6 +48,7 @@ use cli::types::Version;
use cli::types::paths::StateDirectory;
mod commands;
pub mod config;
pub mod output;
pub use output::Model;
@ -195,9 +200,13 @@ fn real_main() -> Result<()> {
let matches = match matches {
Ok(matches) => matches,
Err(err) => {
Err(mut err) => {
// Warning: hack ahead!
//
// We want to hide global options in the help output for
// subcommands, and we want to include values from the
// configuration file in the help output.
//
// If we are showing the help output, we only want to
// display the global options at the top-level; for
// subcommands we hide the global options to not overwhelm
@ -219,12 +228,24 @@ fn real_main() -> Result<()> {
if err.kind() == ErrorKind::DisplayHelp
|| err.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
{
// We want to try to parse the configuration file. To
// that end, we first need to find the path to it.
let mut config = config::ConfigFile::default();
if let Some(augmentations) = cli::config::find_home().and_then(
|home| config.read_and_augment(&home).ok())
{
cli::config::set_augmentations(augmentations);
}
let output = err.render();
let output = if output == cli.render_long_help() {
Some(cli::build(false).render_long_help())
} else if output == cli.render_help() {
Some(cli::build(false).render_help())
} else {
// Redo the parse so that the help message will
// include any augmentations.
err = cli::build(true).try_get_matches().unwrap_err();
None
};
@ -257,6 +278,32 @@ fn real_main() -> Result<()> {
};
let c = cli::SqCommand::from_arg_matches(&matches)?;
let home = match &c.home {
Some(StateDirectory::Absolute(p)) =>
Some(sequoia_directories::Home::new(p.clone())?),
None | Some(StateDirectory::Default) =>
Some(sequoia_directories::Home::default()
.ok_or(anyhow::anyhow!("no default SEQUOIA_HOME \
on this platform"))?
.clone()),
Some(StateDirectory::None) => None,
};
// Parse the configuration file.
let mut config_file = config::ConfigFile::default_config(home.as_ref())?;
let mut config = if let Some(home) = &home {
// Sanity check `cli::config::find_home`.
debug_assert_eq!(home.location(),
cli::config::find_home().unwrap().location());
config_file.read(home)
.with_context(|| format!(
"while reading configuration file {}",
config::ConfigFile::file_name(home).display()))?
} else {
Default::default()
};
let time_is_now = c.time.is_none();
let time: SystemTime = if let Some(t) = c.time.as_ref() {
t.to_system_time(std::time::SystemTime::now())?
@ -270,10 +317,7 @@ fn real_main() -> Result<()> {
time.clone()
};
let mut policy
= sequoia_policy_config::ConfiguredStandardPolicy::at(policy_as_of);
policy.parse_default_config()?;
let mut policy = policy.build();
let mut policy = config.policy(policy_as_of)?;
let known_notations_store = c.known_notation.clone();
let known_notations = known_notations_store
@ -293,6 +337,8 @@ fn real_main() -> Result<()> {
#[allow(deprecated)]
let sq = Sq {
config_file,
config,
verbose: c.verbose,
quiet: c.quiet,
overwrite: c.overwrite,
@ -301,16 +347,7 @@ fn real_main() -> Result<()> {
time_is_now,
policy_as_of,
policy: &policy,
home: match &c.home {
Some(StateDirectory::Absolute(p)) =>
Some(sequoia_directories::Home::new(p.clone())?),
None | Some(StateDirectory::Default) =>
Some(sequoia_directories::Home::default()
.ok_or(anyhow::anyhow!("no default SEQUOIA_HOME \
on this platform"))?
.clone()),
Some(StateDirectory::None) => None,
},
home,
cert_store_path: c.cert_store.clone(),
keyrings: c.keyring.clone(),
keyring_tsks: Default::default(),
@ -322,7 +359,7 @@ fn real_main() -> Result<()> {
password_cache: password_cache.into(),
};
match commands::dispatch(sq, c) {
match commands::dispatch(sq, c, &matches) {
Ok(()) => Ok(()),
Err(err) => {
use clap::error::ErrorFormatter;

View File

@ -92,6 +92,9 @@ type WotStore<'store, 'rstore>
pub struct Sq<'store, 'rstore>
where 'store: 'rstore
{
pub config_file: crate::config::ConfigFile,
pub config: crate::config::Config,
pub verbose: bool,
pub quiet: bool,

638
src/toml_edit_tree.rs Normal file
View File

@ -0,0 +1,638 @@
//! Provides a homogeneous interface to toml_edit's various types, and
//! a traversal interface.
//!
//! toml_edit provides an inhomogeneous interface through `Item`,
//! `Table`, `Value`, `Array`, and various others, which makes
//! traversing the document tree and working with the nodes very
//! difficult.
//!
//! This module introduces a trait providing a homogeneous abstraction
//! over the various types. Then, it provides a convenient traversal
//! interface.
use std::{
borrow::Cow,
fmt,
};
use toml_edit::{
Array,
Item,
Table,
Value,
};
/// Represents a path in a document tree.
#[derive(Debug, Clone)]
pub struct Path {
components: Vec<PathComponent>,
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, c) in self.components.iter().enumerate() {
if i > 0 {
f.write_str(".")?;
}
write!(f, "{}", c)?;
}
Ok(())
}
}
impl std::str::FromStr for Path {
type Err = PathError;
fn from_str(s: &str) -> PathResult<Self> {
Ok(Path {
components: s.split(".").map(PathComponent::from_str)
.collect::<PathResult<Vec<_>>>()?,
})
}
}
impl Path {
/// Returns the number of path components.
pub fn len(&self) -> usize {
self.components.len()
}
/// Returns whether the path is empty.
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
/// Returns an empty path.
pub fn empty() -> Path {
Path {
components: Vec::new(),
}
}
/// Returns the `idx`th path component, if any.
pub fn get(&self, idx: usize) -> Option<&PathComponent> {
self.components.get(idx)
}
/// Iterates over the path's components.
pub fn iter(&self) -> impl Iterator<Item = &PathComponent> {
self.components.iter()
}
/// Appends a path component.
pub fn push(&mut self, component: PathComponent) {
self.components.push(component);
}
/// Removes and returns the last path component, if any.
pub fn pop(&mut self) -> Option<PathComponent> {
self.components.pop()
}
}
/// A path component.
#[derive(Debug, Clone)]
pub enum PathComponent {
/// A key name in a map.
Symbol(String),
/// An index into an array.
Index(usize),
}
impl fmt::Display for PathComponent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PathComponent::Symbol(s) => write!(f, "{}", s),
PathComponent::Index(i) => write!(f, "{}", i),
}
}
}
impl From<usize> for PathComponent {
fn from(v: usize) -> Self {
PathComponent::Index(v)
}
}
impl From<&str> for PathComponent {
fn from(v: &str) -> Self {
PathComponent::Symbol(v.into())
}
}
impl std::str::FromStr for PathComponent {
type Err = PathError;
fn from_str(s: &str) -> PathResult<Self> {
if s.contains('.') {
return Err(PathError::InvalidPathComponent(
"must not contain a period (\".\")"));
}
if let Ok(i) = s.parse::<usize>() {
Ok(PathComponent::Index(i))
} else {
Ok(PathComponent::Symbol(s.into()))
}
}
}
impl PathComponent {
/// Returns an symbolic key, or returns an error.
pub fn as_symbol(&self) -> Result<&str> {
match self {
PathComponent::Symbol(s) => Ok(s),
PathComponent::Index(i) => Err(Error::BadSymbol(*i)),
}
}
/// Returns an index, or returns an error.
pub fn as_index(&self) -> Result<usize> {
match self {
PathComponent::Symbol(s) => Err(Error::BadIndex(s.to_string())),
PathComponent::Index(i) => Ok(*i),
}
}
}
/// Result specialization for conversions to path components.
pub type PathResult<T> = std::result::Result<T, PathError>;
/// Errors converting to path components.
#[derive(thiserror::Error, Debug)]
pub enum PathError {
#[error("invalid path component: {0}")]
InvalidPathComponent(&'static str),
}
/// A unified interface to `toml_edit`.
pub trait Node: fmt::Debug {
/// Returns the node's type name.
fn type_name(&self) -> &'static str;
/// Returns the node as array, if possible.
fn as_array(&self) -> Option<&Array>;
/// Returns the node as mutable array, if possible.
fn as_array_mut(&mut self) -> Option<&mut Array>;
/// Returns the node as atomic value, if possible.
fn as_atomic_value(&self) -> Option<&Value>;
/// Returns the node as mutable table, if possible.
fn as_table_mut(&mut self) -> Option<&mut Table>;
/// Returns a reference to the child denoted by `key`.
fn get(&self, key: &PathComponent) -> Result<&dyn Node>;
/// Returns a mutable reference to the child denoted by `key`.
fn get_mut(&mut self, key: &PathComponent) -> Result<&mut dyn Node>;
/// Sets the child denoted by `key` to the given value.
fn set(&mut self, key: &PathComponent, value: Value) -> Result<()>;
/// Removes the child denoted by `key`.
fn remove(&mut self, key: &PathComponent) -> Result<()>;
/// Iterates over the children of this node.
fn iter<'i>(&'i self) -> Box<(dyn Iterator<Item = (PathComponent, &dyn Node)> + 'i)>;
/// Returns a reference to the node that is reached by traversing
/// `path` from the current node.
fn traverse(&self, path: &Path) -> TraversalResult<&dyn Node>
where
Self: Sized,
{
let mut node: &dyn Node = self as _;
for (i, pc) in path.iter().cloned().enumerate() {
let type_name = node.type_name();
node = node.get(&pc)
.map_err(|e| e.with_context(path, i, type_name))?;
}
Ok(node)
}
/// Returns a mutable reference to the node that is reached by
/// traversing `path` from the current node.
fn traverse_mut(&mut self, path: &Path) -> TraversalResult<&mut dyn Node>
where
Self: Sized,
{
let mut node: &mut dyn Node = self as _;
for (i, pc) in path.iter().cloned().enumerate() {
let type_name = node.type_name();
node = node.get_mut(&pc)
.map_err(|e| e.with_context(path, i, type_name))?;
}
Ok(node)
}
}
impl Node for Item {
fn type_name(&self) -> &'static str {
self.type_name()
}
fn as_array(&self) -> Option<&Array> {
match self {
Item::Value(v) => v.as_array(),
| Item::None
| Item::Table(_)
| Item::ArrayOfTables(_) => None,
}
}
fn as_array_mut(&mut self) -> Option<&mut Array> {
match self {
Item::Value(v) => v.as_array_mut(),
| Item::None
| Item::Table(_)
| Item::ArrayOfTables(_) => None,
}
}
fn as_atomic_value(&self) -> Option<&Value> {
match self {
Item::Value(v) => v.as_atomic_value(),
| Item::None
| Item::Table(_)
| Item::ArrayOfTables(_) => None,
}
}
fn as_table_mut(&mut self) -> Option<&mut Table> {
match self {
Item::Table(t) => Some(t),
| Item::None
| Item::Value(_)
| Item::ArrayOfTables(_) => None,
}
}
fn get(&self, key: &PathComponent) -> Result<&dyn Node> {
match self {
Item::None => Err(Error::LookupError("none")),
Item::Value(v) => v.get(key),
Item::Table(t) => Node::get(t, key),
Item::ArrayOfTables(a) => {
let i = key.as_index()?;
Ok(a.get(i).ok_or_else(|| Error::OutOfBounds(i, a.len()))?)
},
}
}
fn get_mut(&mut self, key: &PathComponent) -> Result<&mut dyn Node> {
match self {
Item::None => Err(Error::LookupError("none")),
Item::Value(v) => v.get_mut(key),
Item::Table(t) => Node::get_mut(t, key),
Item::ArrayOfTables(a) => {
let i = key.as_index()?;
let l = a.len();
Ok(a.get_mut(i).ok_or_else(|| Error::OutOfBounds(i, l))?)
},
}
}
fn set(&mut self, key: &PathComponent, value: Value) -> Result<()> {
match self {
Item::None => Err(Error::InsertionError("none")),
Item::Value(v) => v.set(key, value),
Item::Table(t) => {
t.insert(key.as_symbol()?, Item::Value(value));
Ok(())
},
Item::ArrayOfTables(_) => {
// XXX: ArrayOfTabels::insert does not exist.
Err(Error::InsertionError(self.type_name()))
},
}
}
fn remove(&mut self, key: &PathComponent) -> Result<()> {
match self {
Item::None => Err(Error::RemovalError("none")),
Item::Value(v) => v.remove(key),
Item::Table(t) => {
let s = key.as_symbol()?;
t.remove(s).ok_or(Error::KeyNotFound(s.into()))?;
Ok(())
},
Item::ArrayOfTables(a) => {
let i = key.as_index()?;
let l = a.len();
if i >= l {
return Err(Error::OutOfBounds(i, l));
}
a.remove(i);
Ok(())
},
}
}
fn iter<'i>(&'i self) -> Box<(dyn Iterator<Item = (PathComponent, &dyn Node)> + 'i)> {
match self {
Item::None => Box::new(std::iter::empty()),
Item::Value(v) => v.iter(),
Item::Table(t) => Node::iter(t),
Item::ArrayOfTables(a) =>
Box::new(a.iter().enumerate().map(|(k, v)| (k.into(), &*v as &dyn Node))),
}
}
}
impl Node for Table {
fn type_name(&self) -> &'static str {
"table"
}
fn as_array(&self) -> Option<&Array> {
None
}
fn as_array_mut(&mut self) -> Option<&mut Array> {
None
}
fn as_atomic_value(&self) -> Option<&Value> {
None
}
fn as_table_mut(&mut self) -> Option<&mut Table> {
Some(self)
}
fn get(&self, key: &PathComponent) -> Result<&dyn Node> {
let s = key.as_symbol()?;
Ok(self.get(s).ok_or_else(|| Error::KeyNotFound(s.into()))?)
}
fn get_mut(&mut self, key: &PathComponent) -> Result<&mut dyn Node> {
let s = key.as_symbol()?;
Ok(self.get_mut(s).ok_or_else(|| Error::KeyNotFound(s.into()))?)
}
fn set(&mut self, key: &PathComponent, value: Value) -> Result<()> {
let s = key.as_symbol()?;
self.insert(s, Item::Value(value));
Ok(())
}
fn remove(&mut self, key: &PathComponent) -> Result<()> {
let s = key.as_symbol()?;
self.remove(s).ok_or(Error::KeyNotFound(s.into()))?;
Ok(())
}
fn iter<'i>(&'i self) -> Box<(dyn Iterator<Item = (PathComponent, &dyn Node)> + 'i)> {
Box::new(self.iter().map(|(k, v)| (k.into(), &*v as &dyn Node)))
}
}
impl Node for Value {
fn type_name(&self) -> &'static str {
self.type_name()
}
fn as_array(&self) -> Option<&Array> {
match self {
Value::Array(a) => Some(a),
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_)
| Value::InlineTable(_) =>
None,
}
}
fn as_array_mut(&mut self) -> Option<&mut Array> {
match self {
Value::Array(a) => Some(a),
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_)
| Value::InlineTable(_) =>
None,
}
}
fn as_atomic_value(&self) -> Option<&Value> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Some(self),
| Value::Array(_)
| Value::InlineTable(_) =>
None,
}
}
fn as_table_mut(&mut self) -> Option<&mut Table> {
None
}
fn get(&self, key: &PathComponent) -> Result<&dyn Node> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Err(Error::LookupError(self.type_name())),
Value::Array(a) => {
let i = key.as_index()?;
Ok(a.get(i).ok_or_else(|| Error::OutOfBounds(i, a.len()))?)
},
Value::InlineTable(t) => {
let s = key.as_symbol()?;
Ok(t.get(s).ok_or_else(|| Error::KeyNotFound(s.into()))?)
},
}
}
fn get_mut(&mut self, key: &PathComponent) -> Result<&mut dyn Node> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Err(Error::LookupError(self.type_name())),
Value::Array(a) => {
let i = key.as_index()?;
let l = a.len();
Ok(a.get_mut(i).ok_or_else(|| Error::OutOfBounds(i, l))?)
},
Value::InlineTable(t) => {
let s = key.as_symbol()?;
Ok(t.get_mut(s).ok_or_else(|| Error::KeyNotFound(s.into()))?)
},
}
}
fn set(&mut self, key: &PathComponent, value: Value) -> Result<()> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Err(Error::InsertionError(self.type_name())),
Value::Array(a) => {
let i = key.as_index()?;
let l = a.len();
if i >= l {
return Err(Error::OutOfBounds(i, l));
}
a.replace(i, value);
Ok(())
},
Value::InlineTable(t) => {
t.insert(key.as_symbol()?, value);
Ok(())
},
}
}
fn remove(&mut self, key: &PathComponent) -> Result<()> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Err(Error::RemovalError(self.type_name())),
Value::Array(a) => {
let i = key.as_index()?;
let l = a.len();
if i >= l {
return Err(Error::OutOfBounds(i, l));
}
a.remove(i);
Ok(())
},
Value::InlineTable(t) => {
let s = key.as_symbol()?;
t.remove(s).ok_or(Error::KeyNotFound(s.into()))?;
Ok(())
},
}
}
fn iter<'i>(&'i self) -> Box<(dyn Iterator<Item = (PathComponent, &dyn Node)> + 'i)> {
match self {
| Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) =>
Box::new(std::iter::empty()),
Value::Array(a) =>
Box::new(a.iter().enumerate().map(|(k, v)| (k.into(), &*v as &dyn Node))),
Value::InlineTable(t) =>
Box::new(t.iter().map(|(k, v)| (k.into(), &*v as &dyn Node))),
}
}
}
/// Result specialization for this module.
pub type Result<T> = std::result::Result<T, Error>;
/// Errors for this module.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("key {0:?} not found")]
KeyNotFound(String),
#[error("index {0} is out of bounds")]
OutOfBounds(usize, usize),
#[error("cannot index into a {0}")]
LookupError(&'static str),
#[error("cannot insert into a {0}")]
InsertionError(&'static str),
#[error("cannot remove items from a {0}")]
RemovalError(&'static str),
#[error("{0:?} is not a numeric index")]
BadIndex(String),
#[error("{0:?} is not a valid symbol")]
BadSymbol(usize),
}
impl Error {
/// Adds context to the error, yielding a [`TraversalError`].
pub fn with_context(self, path: &Path, i: usize, type_name: &'static str)
-> TraversalError
{
let p = Path {
components: path.components[..i].iter().cloned().collect(),
};
match self {
Error::KeyNotFound(k) => TraversalError::KeyNotFound(p, k),
Error::OutOfBounds(i, l) => TraversalError::OutOfBounds(p, i, l),
Error::LookupError(t) => match &path.components[i] {
PathComponent::Symbol(s) =>
TraversalError::KeyLookupBadType(p, s.into(), t),
PathComponent::Index(i) =>
TraversalError::IndexLookupBadType(p, *i, t)
},
Error::BadIndex(s) =>
TraversalError::KeyLookupBadType(p, s, type_name),
Error::BadSymbol(i) =>
TraversalError::IndexLookupBadType(p, i, type_name),
Error::InsertionError(_) | Error::RemovalError(_) =>
unreachable!("not applicable for traversals"),
}
}
}
/// Result specialization for traversal errors.
pub type TraversalResult<T> = std::result::Result<T, TraversalError>;
/// Errors traversing the document tree.
#[derive(thiserror::Error, Debug)]
pub enum TraversalError {
#[error("Tried to look up {1:?}{}, \
but it does not exist", Self::fmt_path("in", &.0))]
KeyNotFound(Path, String),
#[error("Tried to get the item at index {1}{}, \
but there are only {2} items", Self::fmt_path("from", &.0))]
OutOfBounds(Path, usize, usize),
#[error("Tried to look up {1:?}{}, \
but the latter is a {2}, not a table", Self::fmt_path("in", &.0))]
KeyLookupBadType(Path, String, &'static str),
#[error("Tried to get the item at index {1}{}, \
but the latter is a {2}, not an array", Self::fmt_path("from", &.0))]
IndexLookupBadType(Path, usize, &'static str),
}
impl TraversalError {
/// Formats a position in the document tree for use in error
/// messages.
fn fmt_path(preposition: &'static str, p: &Path) -> Cow<'static, str> {
if p.is_empty() {
"".into()
} else {
format!(" {} {}", preposition, p).into()
}
}
}