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:
parent
3b1bd79195
commit
4b3f2c97ad
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
3
NEWS
@ -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
105
src/cli/config.rs
Normal 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
45
src/cli/config/get.rs
Normal 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
90
src/cli/config/set.rs
Normal 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);
|
42
src/cli/config/template.rs
Normal file
42
src/cli/config/template.rs
Normal 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);
|
@ -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",
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
|
@ -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
213
src/commands/config.rs
Normal 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(())
|
||||
}
|
@ -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(())
|
||||
|
@ -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(
|
||||
|
@ -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)?,
|
||||
|
@ -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)?
|
||||
|
@ -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
697
src/config.rs
Normal 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 §ion {
|
||||
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(())
|
||||
}
|
69
src/main.rs
69
src/main.rs
@ -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;
|
||||
|
@ -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
638
src/toml_edit_tree.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user