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"
|
name = "sequoia-sq"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"buffered-reader",
|
"buffered-reader",
|
||||||
@ -3496,6 +3497,7 @@ dependencies = [
|
|||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml_edit",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4083,6 +4085,23 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "tower-service"
|
name = "tower-service"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -4669,6 +4688,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.6.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.50.0"
|
version = "0.50.0"
|
||||||
|
@ -29,6 +29,7 @@ gitlab = { repository = "sequoia-pgp/sequoia-sq" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
aho-corasick = "1"
|
||||||
buffered-reader = { version = "1.3.1", default-features = false, features = ["compression"] }
|
buffered-reader = { version = "1.3.1", default-features = false, features = ["compression"] }
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
fs_extra = "1"
|
fs_extra = "1"
|
||||||
@ -36,7 +37,7 @@ sequoia-directories = "0.1"
|
|||||||
sequoia-openpgp = { version = "1.18", default-features = false, features = ["compression"] }
|
sequoia-openpgp = { version = "1.18", default-features = false, features = ["compression"] }
|
||||||
sequoia-autocrypt = { version = "0.25", default-features = false }
|
sequoia-autocrypt = { version = "0.25", default-features = false }
|
||||||
sequoia-net = { version = "0.28", 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"
|
anyhow = "1.0.18"
|
||||||
chrono = "0.4.10"
|
chrono = "0.4.10"
|
||||||
clap = { version = "4", features = ["derive", "env", "string", "wrap_help"] }
|
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"
|
tempfile = "3.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1.13.1" }
|
tokio = { version = "1.13.1" }
|
||||||
|
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse"] }
|
||||||
|
regex = "1"
|
||||||
rpassword = "7.0"
|
rpassword = "7.0"
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
terminal_size = ">=0.2.6, <0.5"
|
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
|
removed: if a secret key is provided as file input, it will be
|
||||||
emitted.
|
emitted.
|
||||||
- The argument `sq key subkey export --cert-file` has been removed.
|
- 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
|
* Changes in 0.39.0
|
||||||
** Notable changes
|
** 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_DURATION;
|
||||||
use crate::cli::KEY_VALIDITY_IN_YEARS;
|
use crate::cli::KEY_VALIDITY_IN_YEARS;
|
||||||
|
use crate::cli::config;
|
||||||
use crate::cli::types::ClapData;
|
use crate::cli::types::ClapData;
|
||||||
use crate::cli::types::EncryptPurpose;
|
use crate::cli::types::EncryptPurpose;
|
||||||
use crate::cli::types::Expiration;
|
use crate::cli::types::Expiration;
|
||||||
@ -134,11 +135,17 @@ Canonical user IDs are of the form `Name (Comment) \
|
|||||||
long = "cipher-suite",
|
long = "cipher-suite",
|
||||||
value_name = "CIPHER-SUITE",
|
value_name = "CIPHER-SUITE",
|
||||||
default_value_t = Default::default(),
|
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,
|
value_enum,
|
||||||
)]
|
)]
|
||||||
pub cipher_suite: CipherSuite,
|
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(
|
#[clap(
|
||||||
long = "new-password-file",
|
long = "new-password-file",
|
||||||
value_name = "PASSWORD_FILE",
|
value_name = "PASSWORD_FILE",
|
||||||
|
@ -9,6 +9,7 @@ use examples::Actions;
|
|||||||
use examples::Example;
|
use examples::Example;
|
||||||
use examples::Setup;
|
use examples::Setup;
|
||||||
|
|
||||||
|
use crate::cli::config;
|
||||||
use crate::cli::key::CipherSuite;
|
use crate::cli::key::CipherSuite;
|
||||||
use crate::cli::types::CertDesignators;
|
use crate::cli::types::CertDesignators;
|
||||||
use crate::cli::types::ClapData;
|
use crate::cli::types::ClapData;
|
||||||
@ -90,11 +91,17 @@ pub struct Command {
|
|||||||
long,
|
long,
|
||||||
value_name = "CIPHER-SUITE",
|
value_name = "CIPHER-SUITE",
|
||||||
default_value_t = CipherSuite::Cv25519,
|
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,
|
value_enum,
|
||||||
)]
|
)]
|
||||||
pub cipher_suite: CipherSuite,
|
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)]
|
#[command(flatten)]
|
||||||
pub expiration: ExpirationArg,
|
pub expiration: ExpirationArg,
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ use openpgp::Fingerprint;
|
|||||||
pub mod examples;
|
pub mod examples;
|
||||||
|
|
||||||
pub mod cert;
|
pub mod cert;
|
||||||
|
pub mod config;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod encrypt;
|
pub mod encrypt;
|
||||||
@ -565,5 +566,6 @@ pub enum SqSubcommands {
|
|||||||
Keyring(keyring::Command),
|
Keyring(keyring::Command),
|
||||||
Packet(packet::Command),
|
Packet(packet::Command),
|
||||||
|
|
||||||
|
Config(config::Command),
|
||||||
Version(version::Command),
|
Version(version::Command),
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::cli::config;
|
||||||
use crate::cli::examples::*;
|
use crate::cli::examples::*;
|
||||||
use crate::cli::types::ClapData;
|
use crate::cli::types::ClapData;
|
||||||
use crate::cli::types::FileOrCertStore;
|
use crate::cli::types::FileOrCertStore;
|
||||||
@ -38,7 +39,9 @@ pub struct Command {
|
|||||||
// that they are sorted to the bottom.
|
// that they are sorted to the bottom.
|
||||||
display_order = 800,
|
display_order = 800,
|
||||||
value_name = "URI",
|
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>,
|
pub servers: Vec<String>,
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::cli::config;
|
||||||
use crate::cli::examples::Action;
|
use crate::cli::examples::Action;
|
||||||
use crate::cli::examples::Actions;
|
use crate::cli::examples::Actions;
|
||||||
use crate::cli::examples::Example;
|
use crate::cli::examples::Example;
|
||||||
@ -50,10 +51,16 @@ pub struct Command {
|
|||||||
long = "server",
|
long = "server",
|
||||||
default_values_t = DEFAULT_KEYSERVERS.iter().map(ToString::to_string),
|
default_values_t = DEFAULT_KEYSERVERS.iter().map(ToString::to_string),
|
||||||
value_name = "URI",
|
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>,
|
pub servers: Vec<String>,
|
||||||
|
|
||||||
|
/// Workaround for https://github.com/clap-rs/clap/issues/3846
|
||||||
|
#[clap(skip)]
|
||||||
|
pub servers_source: Option<clap::parser::ValueSource>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
help = FileOrCertStore::HELP_OPTIONAL,
|
help = FileOrCertStore::HELP_OPTIONAL,
|
||||||
long,
|
long,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use clap::ArgMatches;
|
||||||
|
|
||||||
use sequoia_openpgp as openpgp;
|
use sequoia_openpgp as openpgp;
|
||||||
use openpgp::cert::prelude::*;
|
use openpgp::cert::prelude::*;
|
||||||
use openpgp::{Cert, Result};
|
use openpgp::{Cert, Result};
|
||||||
@ -16,6 +18,7 @@ use crate::cli::{SqCommand, SqSubcommands};
|
|||||||
|
|
||||||
pub mod autocrypt;
|
pub mod autocrypt;
|
||||||
pub mod cert;
|
pub mod cert;
|
||||||
|
pub mod config;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod encrypt;
|
pub mod encrypt;
|
||||||
@ -30,8 +33,9 @@ pub mod verify;
|
|||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
/// Dispatches the top-level subcommand.
|
/// 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 {
|
match command.subcommand {
|
||||||
SqSubcommands::Encrypt(command) =>
|
SqSubcommands::Encrypt(command) =>
|
||||||
encrypt::dispatch(sq, command),
|
encrypt::dispatch(sq, command),
|
||||||
@ -50,18 +54,21 @@ pub fn dispatch(sq: Sq, command: SqCommand) -> Result<()>
|
|||||||
SqSubcommands::Cert(command) =>
|
SqSubcommands::Cert(command) =>
|
||||||
cert::dispatch(sq, command),
|
cert::dispatch(sq, command),
|
||||||
SqSubcommands::Key(command) =>
|
SqSubcommands::Key(command) =>
|
||||||
key::dispatch(sq, command),
|
key::dispatch(sq, command, matches),
|
||||||
|
|
||||||
SqSubcommands::Pki(command) =>
|
SqSubcommands::Pki(command) =>
|
||||||
pki::dispatch(sq, command),
|
pki::dispatch(sq, command),
|
||||||
|
|
||||||
SqSubcommands::Network(command) =>
|
SqSubcommands::Network(command) =>
|
||||||
network::dispatch(sq, command),
|
network::dispatch(sq, command, matches),
|
||||||
SqSubcommands::Keyring(command) =>
|
SqSubcommands::Keyring(command) =>
|
||||||
keyring::dispatch(sq, command),
|
keyring::dispatch(sq, command),
|
||||||
SqSubcommands::Packet(command) =>
|
SqSubcommands::Packet(command) =>
|
||||||
packet::dispatch(sq, command),
|
packet::dispatch(sq, command),
|
||||||
|
|
||||||
|
SqSubcommands::Config(command) =>
|
||||||
|
config::dispatch(sq, command),
|
||||||
|
|
||||||
SqSubcommands::Version(command) =>
|
SqSubcommands::Version(command) =>
|
||||||
version::dispatch(sq, 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 sequoia_openpgp as openpgp;
|
||||||
use openpgp::Result;
|
use openpgp::Result;
|
||||||
|
|
||||||
@ -20,12 +22,17 @@ use revoke::certificate_revoke;
|
|||||||
mod subkey;
|
mod subkey;
|
||||||
pub mod userid;
|
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::*;
|
use cli::key::Subcommands::*;
|
||||||
match command.subcommand {
|
match command.subcommand {
|
||||||
List(c) => list(sq, c)?,
|
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)?,
|
Import(c) => import(sq, c)?,
|
||||||
Export(c) => export::dispatch(sq, c)?,
|
Export(c) => export::dispatch(sq, c)?,
|
||||||
Delete(c) => delete::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)?,
|
Expire(c) => expire::dispatch(sq, c)?,
|
||||||
Userid(c) => userid::dispatch(sq, c)?,
|
Userid(c) => userid::dispatch(sq, c)?,
|
||||||
Revoke(c) => certificate_revoke(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)?,
|
Approvals(c) => approvals::dispatch(sq, c)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -93,8 +93,8 @@ pub fn generate(
|
|||||||
|
|
||||||
// Cipher Suite
|
// Cipher Suite
|
||||||
builder = builder.set_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.
|
// Primary key capabilities.
|
||||||
builder = builder.set_primary_key_flags(
|
builder = builder.set_primary_key_flags(
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
//! Dispatches `sq key subkey`.
|
||||||
|
|
||||||
|
use clap::ArgMatches;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use crate::Sq;
|
use crate::Sq;
|
||||||
use crate::cli::key::subkey::Command;
|
use crate::cli::key::subkey::Command;
|
||||||
@ -10,9 +14,13 @@ mod export;
|
|||||||
mod password;
|
mod password;
|
||||||
mod revoke;
|
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 {
|
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::Export(c) => export::dispatch(sq, c)?,
|
||||||
Command::Delete(c) => delete::dispatch(sq, c)?,
|
Command::Delete(c) => delete::dispatch(sq, c)?,
|
||||||
Command::Password(c) => password::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)
|
let new_cert = KeyBuilder::new(keyflags)
|
||||||
.set_creation_time(sq.time)
|
.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)
|
.set_password(password)
|
||||||
.subkey(valid_cert)?
|
.subkey(valid_cert)?
|
||||||
.set_key_validity_period(validity)?
|
.set_key_validity_period(validity)?
|
||||||
|
@ -8,6 +8,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use clap::ArgMatches;
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use tokio::task::JoinSet;
|
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.
|
/// How long to wait for each individual http request.
|
||||||
pub const REQUEST_TIMEOUT: Duration = Duration::new(5, 0);
|
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<()>
|
-> Result<()>
|
||||||
{
|
{
|
||||||
|
let matches = matches.subcommand().unwrap().1;
|
||||||
use cli::network::Subcommands;
|
use cli::network::Subcommands;
|
||||||
match c.subcommand {
|
match c.subcommand {
|
||||||
Subcommands::Search(command) =>
|
Subcommands::Search(mut command) => {
|
||||||
dispatch_search(sq, command),
|
command.servers_source = matches.value_source("servers");
|
||||||
|
dispatch_search(sq, command)
|
||||||
|
},
|
||||||
|
|
||||||
Subcommands::Keyserver(command) =>
|
Subcommands::Keyserver(command) =>
|
||||||
dispatch_keyserver(sq, command),
|
dispatch_keyserver(sq, command, matches),
|
||||||
|
|
||||||
Subcommands::Wkd(command) =>
|
Subcommands::Wkd(command) =>
|
||||||
dispatch_wkd(sq, command),
|
dispatch_wkd(sq, command),
|
||||||
@ -888,12 +892,15 @@ pub fn dispatch_search(mut sq: Sq, c: cli::network::search::Command)
|
|||||||
sq.cert_store_or_else()?;
|
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 http_client = http_client()?;
|
||||||
let servers = c.servers.iter().map(
|
let servers = sq.config.key_servers(&c.servers, c.servers_source)
|
||||||
|uri| KeyServer::with_client(uri, http_client.clone())
|
.map(|uri| KeyServer::with_client(uri, http_client.clone())
|
||||||
.with_context(|| format!("Malformed keyserver URI: {}", uri))
|
.with_context(|| format!("Malformed keyserver URI: {}", uri))
|
||||||
.map(Arc::new))
|
.map(Arc::new))
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
let mut seen_emails = HashSet::new();
|
let mut seen_emails = HashSet::new();
|
||||||
@ -1054,30 +1061,22 @@ pub fn dispatch_search(mut sq: Sq, c: cli::network::search::Command)
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Figures out whether the given set of key servers is the default
|
pub fn dispatch_keyserver(
|
||||||
/// set.
|
mut sq: Sq,
|
||||||
fn default_keyservers_p(servers: &[String]) -> bool {
|
c: cli::network::keyserver::Command,
|
||||||
// XXX: This could be nicer, maybe with a custom clap parser
|
matches: &ArgMatches,
|
||||||
// that encodes it in the type. For now we live with the
|
) -> Result<()>
|
||||||
// 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<()>
|
|
||||||
{
|
{
|
||||||
make_qprintln!(sq.quiet);
|
make_qprintln!(sq.quiet);
|
||||||
|
|
||||||
let default_servers = default_keyservers_p(&c.servers);
|
let servers_source = matches.value_source("servers").unwrap();
|
||||||
let servers = c.servers.iter().map(
|
let default_servers =
|
||||||
|uri| KeyServer::with_client(uri, http_client()?)
|
matches!(servers_source, clap::parser::ValueSource::DefaultValue);
|
||||||
.with_context(|| format!("Malformed keyserver URI: {}", uri))
|
|
||||||
.map(Arc::new))
|
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<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
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;
|
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 macros;
|
||||||
#[macro_use] mod log;
|
#[macro_use] mod log;
|
||||||
|
|
||||||
@ -44,6 +48,7 @@ use cli::types::Version;
|
|||||||
use cli::types::paths::StateDirectory;
|
use cli::types::paths::StateDirectory;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
pub mod config;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub use output::Model;
|
pub use output::Model;
|
||||||
|
|
||||||
@ -195,9 +200,13 @@ fn real_main() -> Result<()> {
|
|||||||
|
|
||||||
let matches = match matches {
|
let matches = match matches {
|
||||||
Ok(matches) => matches,
|
Ok(matches) => matches,
|
||||||
Err(err) => {
|
Err(mut err) => {
|
||||||
// Warning: hack ahead!
|
// 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
|
// If we are showing the help output, we only want to
|
||||||
// display the global options at the top-level; for
|
// display the global options at the top-level; for
|
||||||
// subcommands we hide the global options to not overwhelm
|
// subcommands we hide the global options to not overwhelm
|
||||||
@ -219,12 +228,24 @@ fn real_main() -> Result<()> {
|
|||||||
if err.kind() == ErrorKind::DisplayHelp
|
if err.kind() == ErrorKind::DisplayHelp
|
||||||
|| err.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
|
|| 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 = err.render();
|
||||||
let output = if output == cli.render_long_help() {
|
let output = if output == cli.render_long_help() {
|
||||||
Some(cli::build(false).render_long_help())
|
Some(cli::build(false).render_long_help())
|
||||||
} else if output == cli.render_help() {
|
} else if output == cli.render_help() {
|
||||||
Some(cli::build(false).render_help())
|
Some(cli::build(false).render_help())
|
||||||
} else {
|
} else {
|
||||||
|
// Redo the parse so that the help message will
|
||||||
|
// include any augmentations.
|
||||||
|
err = cli::build(true).try_get_matches().unwrap_err();
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -257,6 +278,32 @@ fn real_main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
let c = cli::SqCommand::from_arg_matches(&matches)?;
|
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_is_now = c.time.is_none();
|
||||||
let time: SystemTime = if let Some(t) = c.time.as_ref() {
|
let time: SystemTime = if let Some(t) = c.time.as_ref() {
|
||||||
t.to_system_time(std::time::SystemTime::now())?
|
t.to_system_time(std::time::SystemTime::now())?
|
||||||
@ -270,10 +317,7 @@ fn real_main() -> Result<()> {
|
|||||||
time.clone()
|
time.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut policy
|
let mut policy = config.policy(policy_as_of)?;
|
||||||
= sequoia_policy_config::ConfiguredStandardPolicy::at(policy_as_of);
|
|
||||||
policy.parse_default_config()?;
|
|
||||||
let mut policy = policy.build();
|
|
||||||
|
|
||||||
let known_notations_store = c.known_notation.clone();
|
let known_notations_store = c.known_notation.clone();
|
||||||
let known_notations = known_notations_store
|
let known_notations = known_notations_store
|
||||||
@ -293,6 +337,8 @@ fn real_main() -> Result<()> {
|
|||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let sq = Sq {
|
let sq = Sq {
|
||||||
|
config_file,
|
||||||
|
config,
|
||||||
verbose: c.verbose,
|
verbose: c.verbose,
|
||||||
quiet: c.quiet,
|
quiet: c.quiet,
|
||||||
overwrite: c.overwrite,
|
overwrite: c.overwrite,
|
||||||
@ -301,16 +347,7 @@ fn real_main() -> Result<()> {
|
|||||||
time_is_now,
|
time_is_now,
|
||||||
policy_as_of,
|
policy_as_of,
|
||||||
policy: &policy,
|
policy: &policy,
|
||||||
home: match &c.home {
|
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,
|
|
||||||
},
|
|
||||||
cert_store_path: c.cert_store.clone(),
|
cert_store_path: c.cert_store.clone(),
|
||||||
keyrings: c.keyring.clone(),
|
keyrings: c.keyring.clone(),
|
||||||
keyring_tsks: Default::default(),
|
keyring_tsks: Default::default(),
|
||||||
@ -322,7 +359,7 @@ fn real_main() -> Result<()> {
|
|||||||
password_cache: password_cache.into(),
|
password_cache: password_cache.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match commands::dispatch(sq, c) {
|
match commands::dispatch(sq, c, &matches) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
use clap::error::ErrorFormatter;
|
use clap::error::ErrorFormatter;
|
||||||
|
@ -92,6 +92,9 @@ type WotStore<'store, 'rstore>
|
|||||||
pub struct Sq<'store, 'rstore>
|
pub struct Sq<'store, 'rstore>
|
||||||
where 'store: 'rstore
|
where 'store: 'rstore
|
||||||
{
|
{
|
||||||
|
pub config_file: crate::config::ConfigFile,
|
||||||
|
pub config: crate::config::Config,
|
||||||
|
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
pub quiet: 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