689 lines
22 KiB
Rust
689 lines
22 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
|
|
pub const DEFAULT_CONFIG_TOML: &str = r#"# geth local node config
|
|
# Remote node-to-node communication is Iroh-only.
|
|
#
|
|
# relay_mode controls Iroh relay use for practical internet connectivity.
|
|
# Values: "default", "staging", "disabled", "custom".
|
|
[iroh]
|
|
relay_mode = "default"
|
|
local_discovery = true
|
|
# relay_map = "home"
|
|
#
|
|
# [iroh.relay_maps.home]
|
|
# urls = ["https://relay.example.com"]
|
|
#
|
|
[sync]
|
|
live_sync_enabled = true
|
|
live_sync_interval_ms = 30000
|
|
"#;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct GethPaths {
|
|
home: PathBuf,
|
|
}
|
|
|
|
impl GethPaths {
|
|
pub fn resolve() -> Result<Self, ConfigError> {
|
|
if let Some(home) = std::env::var_os("GETH_HOME") {
|
|
return Ok(Self {
|
|
home: PathBuf::from(home),
|
|
});
|
|
}
|
|
let project_dirs = directories::ProjectDirs::from("local", "geth", "geth")
|
|
.ok_or(ConfigError::NoDataDirectory)?;
|
|
Ok(Self {
|
|
home: project_dirs.data_dir().to_path_buf(),
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn from_home(home: impl Into<PathBuf>) -> Self {
|
|
Self { home: home.into() }
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn home(&self) -> &Path {
|
|
&self.home
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn config_file(&self) -> PathBuf {
|
|
self.home.join("config.toml")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn metadata_db(&self) -> PathBuf {
|
|
self.home.join("geth.sqlite")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn identity_dir(&self) -> PathBuf {
|
|
self.home.join("identity")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn agent_key(&self) -> PathBuf {
|
|
self.identity_dir().join("agent.ed25519")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn iroh_key(&self) -> PathBuf {
|
|
self.identity_dir().join("iroh.ed25519")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn cas_dir(&self) -> PathBuf {
|
|
self.home.join("cas")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn run_dir(&self) -> PathBuf {
|
|
self.home.join("run")
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn socket_path(&self) -> PathBuf {
|
|
PathBuf::from(self.control_endpoint())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn control_endpoint(&self) -> String {
|
|
#[cfg(unix)]
|
|
{
|
|
self.run_dir().join("geth.sock").display().to_string()
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
let home = self.home.display().to_string().to_ascii_lowercase();
|
|
format!(
|
|
r"\\.\pipe\geth-{:016x}",
|
|
stable_control_endpoint_suffix(home.as_bytes())
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn ensure_base_dirs(&self) -> Result<(), ConfigError> {
|
|
std::fs::create_dir_all(self.identity_dir())?;
|
|
std::fs::create_dir_all(self.cas_dir().join("blobs"))?;
|
|
std::fs::create_dir_all(self.run_dir())?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(any(windows, test))]
|
|
fn stable_control_endpoint_suffix(bytes: &[u8]) -> u64 {
|
|
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
|
|
for byte in bytes {
|
|
hash ^= u64::from(*byte);
|
|
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
|
}
|
|
hash
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ConfigError {
|
|
#[error("could not determine OS data directory and GETH_HOME is unset")]
|
|
NoDataDirectory,
|
|
#[error("io error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("toml parse error: {0}")]
|
|
TomlParse(#[from] toml_edit::TomlError),
|
|
#[error("invalid iroh relay_mode: {0}")]
|
|
InvalidRelayMode(String),
|
|
#[error("invalid iroh local_discovery value: {0}")]
|
|
InvalidLocalDiscovery(String),
|
|
#[error("custom iroh relay_mode requires iroh.relay_map")]
|
|
MissingRelayMapSelection,
|
|
#[error("selected iroh relay map does not exist: {0}")]
|
|
UnknownRelayMap(String),
|
|
#[error("invalid iroh relay_maps table")]
|
|
InvalidRelayMapsTable,
|
|
#[error("invalid iroh relay map `{0}`")]
|
|
InvalidRelayMap(String),
|
|
#[error("iroh relay map `{0}` must include at least one URL")]
|
|
EmptyRelayMap(String),
|
|
#[error("invalid iroh relay URL in map `{map}`: {url}")]
|
|
InvalidRelayUrl { map: String, url: String },
|
|
#[error("invalid sync live_sync_enabled value: {0}")]
|
|
InvalidLiveSyncEnabled(String),
|
|
#[error("invalid sync live_sync_interval_ms value: {0}")]
|
|
InvalidLiveSyncInterval(String),
|
|
#[error(
|
|
"unsupported config key `{0}`; supported keys: iroh.relay_mode, iroh.relay_map, iroh.local_discovery, sync.live_sync_enabled, sync.live_sync_interval_ms"
|
|
)]
|
|
UnsupportedKey(String),
|
|
#[error("invalid value `{value}` for {key}; expected {expected}")]
|
|
InvalidSetting {
|
|
key: &'static str,
|
|
value: String,
|
|
expected: &'static str,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ConfigKey {
|
|
IrohRelayMode,
|
|
IrohRelayMap,
|
|
IrohLocalDiscovery,
|
|
SyncLiveSyncEnabled,
|
|
SyncLiveSyncIntervalMs,
|
|
}
|
|
|
|
impl ConfigKey {
|
|
#[must_use]
|
|
pub const fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::IrohRelayMode => "iroh.relay_mode",
|
|
Self::IrohRelayMap => "iroh.relay_map",
|
|
Self::IrohLocalDiscovery => "iroh.local_discovery",
|
|
Self::SyncLiveSyncEnabled => "sync.live_sync_enabled",
|
|
Self::SyncLiveSyncIntervalMs => "sync.live_sync_interval_ms",
|
|
}
|
|
}
|
|
|
|
fn set(
|
|
self,
|
|
document: &mut toml_edit::DocumentMut,
|
|
raw_value: &str,
|
|
) -> Result<(), ConfigError> {
|
|
match self {
|
|
Self::IrohRelayMode => {
|
|
if !matches!(raw_value, "default" | "staging" | "disabled" | "custom") {
|
|
return Err(invalid_setting(
|
|
self,
|
|
raw_value,
|
|
"default, staging, disabled, or custom",
|
|
));
|
|
}
|
|
document["iroh"]["relay_mode"] = toml_edit::value(raw_value);
|
|
}
|
|
Self::IrohRelayMap => {
|
|
if raw_value.trim().is_empty() {
|
|
return Err(invalid_setting(
|
|
self,
|
|
raw_value,
|
|
"a non-empty relay-map name",
|
|
));
|
|
}
|
|
document["iroh"]["relay_map"] = toml_edit::value(raw_value);
|
|
}
|
|
Self::IrohLocalDiscovery => {
|
|
document["iroh"]["local_discovery"] =
|
|
toml_edit::value(parse_bool_setting(self, raw_value)?);
|
|
}
|
|
Self::SyncLiveSyncEnabled => {
|
|
document["sync"]["live_sync_enabled"] =
|
|
toml_edit::value(parse_bool_setting(self, raw_value)?);
|
|
}
|
|
Self::SyncLiveSyncIntervalMs => {
|
|
let interval = raw_value
|
|
.parse::<i64>()
|
|
.ok()
|
|
.filter(|interval| *interval >= 100)
|
|
.ok_or_else(|| {
|
|
invalid_setting(self, raw_value, "an integer of at least 100")
|
|
})?;
|
|
document["sync"]["live_sync_interval_ms"] = toml_edit::value(interval);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl FromStr for ConfigKey {
|
|
type Err = ConfigError;
|
|
|
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
match value {
|
|
"iroh.relay_mode" => Ok(Self::IrohRelayMode),
|
|
"iroh.relay_map" => Ok(Self::IrohRelayMap),
|
|
"iroh.local_discovery" => Ok(Self::IrohLocalDiscovery),
|
|
"sync.live_sync_enabled" => Ok(Self::SyncLiveSyncEnabled),
|
|
"sync.live_sync_interval_ms" => Ok(Self::SyncLiveSyncIntervalMs),
|
|
other => Err(ConfigError::UnsupportedKey(other.to_owned())),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn invalid_setting(key: ConfigKey, value: &str, expected: &'static str) -> ConfigError {
|
|
ConfigError::InvalidSetting {
|
|
key: key.as_str(),
|
|
value: value.to_owned(),
|
|
expected,
|
|
}
|
|
}
|
|
|
|
fn parse_bool_setting(key: ConfigKey, value: &str) -> Result<bool, ConfigError> {
|
|
match value {
|
|
"true" => Ok(true),
|
|
"false" => Ok(false),
|
|
_ => Err(invalid_setting(key, value, "true or false")),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct GethConfig {
|
|
pub iroh: IrohConfig,
|
|
pub sync: SyncConfig,
|
|
}
|
|
|
|
impl GethConfig {
|
|
#[must_use]
|
|
pub fn default_toml() -> &'static str {
|
|
DEFAULT_CONFIG_TOML
|
|
}
|
|
|
|
pub fn load(path: &Path) -> Result<Self, ConfigError> {
|
|
if !path.exists() {
|
|
return Ok(Self::default());
|
|
}
|
|
Self::parse(&std::fs::read_to_string(path)?)
|
|
}
|
|
|
|
pub fn parse(text: &str) -> Result<Self, ConfigError> {
|
|
let document = text.parse::<toml_edit::DocumentMut>()?;
|
|
let relay_maps = parse_relay_maps(&document)?;
|
|
let local_discovery = parse_local_discovery(&document)?;
|
|
let sync = parse_sync_config(&document)?;
|
|
let relay_mode = match document.get("iroh").and_then(|iroh| iroh.get("relay_mode")) {
|
|
Some(item) => {
|
|
let value = item
|
|
.as_str()
|
|
.ok_or_else(|| ConfigError::InvalidRelayMode(item.to_string()))?;
|
|
RelayMode::parse(value, &document, &relay_maps)?
|
|
}
|
|
None => RelayMode::default(),
|
|
};
|
|
|
|
Ok(Self {
|
|
iroh: IrohConfig {
|
|
relay_mode,
|
|
relay_maps,
|
|
local_discovery,
|
|
},
|
|
sync,
|
|
})
|
|
}
|
|
|
|
pub fn set(path: &Path, key: ConfigKey, value: &str) -> Result<Self, ConfigError> {
|
|
let text = if path.exists() {
|
|
std::fs::read_to_string(path)?
|
|
} else {
|
|
Self::default_toml().to_owned()
|
|
};
|
|
let mut document = text.parse::<toml_edit::DocumentMut>()?;
|
|
key.set(&mut document, value)?;
|
|
let updated = document.to_string();
|
|
let config = Self::parse(&updated)?;
|
|
|
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
|
std::fs::create_dir_all(parent)?;
|
|
let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
|
|
temporary.write_all(updated.as_bytes())?;
|
|
temporary.as_file().sync_all()?;
|
|
temporary
|
|
.persist(path)
|
|
.map_err(|error| ConfigError::Io(error.error))?;
|
|
Ok(config)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct IrohConfig {
|
|
pub relay_mode: RelayMode,
|
|
pub relay_maps: BTreeMap<String, RelayMapConfig>,
|
|
pub local_discovery: bool,
|
|
}
|
|
|
|
impl Default for IrohConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
relay_mode: RelayMode::Default,
|
|
relay_maps: BTreeMap::new(),
|
|
local_discovery: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SyncConfig {
|
|
pub live_sync_enabled: bool,
|
|
pub live_sync_interval_ms: u64,
|
|
}
|
|
|
|
impl Default for SyncConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
live_sync_enabled: true,
|
|
live_sync_interval_ms: 30_000,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RelayMapConfig {
|
|
pub urls: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum RelayMode {
|
|
Disabled,
|
|
#[default]
|
|
Default,
|
|
Staging,
|
|
Custom {
|
|
map: String,
|
|
},
|
|
}
|
|
|
|
impl RelayMode {
|
|
fn parse(
|
|
value: &str,
|
|
document: &toml_edit::DocumentMut,
|
|
relay_maps: &BTreeMap<String, RelayMapConfig>,
|
|
) -> Result<Self, ConfigError> {
|
|
match value {
|
|
"disabled" => Ok(Self::Disabled),
|
|
"default" => Ok(Self::Default),
|
|
"staging" => Ok(Self::Staging),
|
|
"custom" => {
|
|
let map = document
|
|
.get("iroh")
|
|
.and_then(|iroh| iroh.get("relay_map"))
|
|
.and_then(toml_edit::Item::as_str)
|
|
.ok_or(ConfigError::MissingRelayMapSelection)?
|
|
.to_owned();
|
|
if !relay_maps.contains_key(&map) {
|
|
return Err(ConfigError::UnknownRelayMap(map));
|
|
}
|
|
Ok(Self::Custom { map })
|
|
}
|
|
other => Err(ConfigError::InvalidRelayMode(other.to_owned())),
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn label(&self) -> String {
|
|
match self {
|
|
Self::Disabled => "disabled".to_owned(),
|
|
Self::Default => "default".to_owned(),
|
|
Self::Staging => "staging".to_owned(),
|
|
Self::Custom { map } => format!("custom:{map}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_relay_maps(
|
|
document: &toml_edit::DocumentMut,
|
|
) -> Result<BTreeMap<String, RelayMapConfig>, ConfigError> {
|
|
let Some(relay_maps_item) = document.get("iroh").and_then(|iroh| iroh.get("relay_maps")) else {
|
|
return Ok(BTreeMap::new());
|
|
};
|
|
let relay_maps_table = relay_maps_item
|
|
.as_table_like()
|
|
.ok_or(ConfigError::InvalidRelayMapsTable)?;
|
|
let mut relay_maps = BTreeMap::new();
|
|
for (name, item) in relay_maps_table.iter() {
|
|
let urls = parse_relay_map_urls(name, item)?;
|
|
relay_maps.insert(name.to_owned(), RelayMapConfig { urls });
|
|
}
|
|
Ok(relay_maps)
|
|
}
|
|
|
|
fn parse_relay_map_urls(name: &str, item: &toml_edit::Item) -> Result<Vec<String>, ConfigError> {
|
|
let table = item
|
|
.as_table_like()
|
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
|
let urls_item = table
|
|
.get("urls")
|
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
|
let urls_array = urls_item
|
|
.as_array()
|
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
|
let mut urls = Vec::new();
|
|
for value in urls_array.iter() {
|
|
let url = value
|
|
.as_str()
|
|
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
|
validate_relay_url(name, url)?;
|
|
urls.push(url.to_owned());
|
|
}
|
|
if urls.is_empty() {
|
|
return Err(ConfigError::EmptyRelayMap(name.to_owned()));
|
|
}
|
|
Ok(urls)
|
|
}
|
|
|
|
fn validate_relay_url(map: &str, relay_url: &str) -> Result<(), ConfigError> {
|
|
let url = url::Url::parse(relay_url).map_err(|_| ConfigError::InvalidRelayUrl {
|
|
map: map.to_owned(),
|
|
url: relay_url.to_owned(),
|
|
})?;
|
|
match url.scheme() {
|
|
"http" | "https" => Ok(()),
|
|
_ => Err(ConfigError::InvalidRelayUrl {
|
|
map: map.to_owned(),
|
|
url: relay_url.to_owned(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn parse_local_discovery(document: &toml_edit::DocumentMut) -> Result<bool, ConfigError> {
|
|
match document
|
|
.get("iroh")
|
|
.and_then(|iroh| iroh.get("local_discovery"))
|
|
{
|
|
Some(item) => item
|
|
.as_bool()
|
|
.ok_or_else(|| ConfigError::InvalidLocalDiscovery(item.to_string())),
|
|
None => Ok(true),
|
|
}
|
|
}
|
|
|
|
fn parse_sync_config(document: &toml_edit::DocumentMut) -> Result<SyncConfig, ConfigError> {
|
|
let live_sync_enabled = match document
|
|
.get("sync")
|
|
.and_then(|sync| sync.get("live_sync_enabled"))
|
|
{
|
|
Some(item) => item
|
|
.as_bool()
|
|
.ok_or_else(|| ConfigError::InvalidLiveSyncEnabled(item.to_string()))?,
|
|
None => true,
|
|
};
|
|
let live_sync_interval_ms = match document
|
|
.get("sync")
|
|
.and_then(|sync| sync.get("live_sync_interval_ms"))
|
|
{
|
|
Some(item) => {
|
|
let interval = item
|
|
.as_integer()
|
|
.ok_or_else(|| ConfigError::InvalidLiveSyncInterval(item.to_string()))?;
|
|
if interval < 100 {
|
|
return Err(ConfigError::InvalidLiveSyncInterval(interval.to_string()));
|
|
}
|
|
interval as u64
|
|
}
|
|
None => 30_000,
|
|
};
|
|
Ok(SyncConfig {
|
|
live_sync_enabled,
|
|
live_sync_interval_ms,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_config_uses_iroh_relays() {
|
|
let config = GethConfig::parse(GethConfig::default_toml()).expect("parse config");
|
|
assert_eq!(config.iroh.relay_mode, RelayMode::Default);
|
|
assert!(config.iroh.local_discovery);
|
|
assert!(config.sync.live_sync_enabled);
|
|
assert_eq!(config.sync.live_sync_interval_ms, 30_000);
|
|
}
|
|
|
|
#[test]
|
|
fn config_parses_disabled_relay_mode() {
|
|
let config = GethConfig::parse("[iroh]\nrelay_mode = \"disabled\"\n").expect("parse");
|
|
assert_eq!(config.iroh.relay_mode, RelayMode::Disabled);
|
|
}
|
|
|
|
#[test]
|
|
fn config_parses_custom_relay_map() {
|
|
let config = GethConfig::parse(
|
|
r#"
|
|
[iroh]
|
|
relay_mode = "custom"
|
|
relay_map = "home"
|
|
|
|
[iroh.relay_maps.home]
|
|
urls = ["https://relay.example.com"]
|
|
"#,
|
|
)
|
|
.expect("parse");
|
|
assert_eq!(
|
|
config.iroh.relay_mode,
|
|
RelayMode::Custom {
|
|
map: "home".to_owned()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
config.iroh.relay_maps["home"].urls,
|
|
vec!["https://relay.example.com".to_owned()]
|
|
);
|
|
assert_eq!(config.iroh.relay_mode.label(), "custom:home");
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_unknown_relay_mode() {
|
|
let error = GethConfig::parse("[iroh]\nrelay_mode = \"magic\"\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidRelayMode(value) if value == "magic"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_non_string_relay_mode() {
|
|
let error = GethConfig::parse("[iroh]\nrelay_mode = true\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidRelayMode(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_invalid_custom_relay_url() {
|
|
let error = GethConfig::parse(
|
|
r#"
|
|
[iroh]
|
|
relay_mode = "custom"
|
|
relay_map = "home"
|
|
|
|
[iroh.relay_maps.home]
|
|
urls = ["not a url"]
|
|
"#,
|
|
)
|
|
.expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidRelayUrl { map, .. } if map == "home"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_missing_custom_relay_map() {
|
|
let error = GethConfig::parse("[iroh]\nrelay_mode = \"custom\"\nrelay_map = \"home\"\n")
|
|
.expect_err("error");
|
|
assert!(matches!(error, ConfigError::UnknownRelayMap(map) if map == "home"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_parses_local_discovery_toggle() {
|
|
let config = GethConfig::parse("[iroh]\nlocal_discovery = false\n").expect("parse");
|
|
assert!(!config.iroh.local_discovery);
|
|
}
|
|
|
|
#[test]
|
|
fn config_parses_live_sync_settings() {
|
|
let config =
|
|
GethConfig::parse("[sync]\nlive_sync_enabled = false\nlive_sync_interval_ms = 250\n")
|
|
.expect("parse");
|
|
assert!(!config.sync.live_sync_enabled);
|
|
assert_eq!(config.sync.live_sync_interval_ms, 250);
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_invalid_live_sync_settings() {
|
|
let error = GethConfig::parse("[sync]\nlive_sync_enabled = \"yes\"\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidLiveSyncEnabled(_)));
|
|
|
|
let error = GethConfig::parse("[sync]\nlive_sync_interval_ms = 99\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidLiveSyncInterval(_)));
|
|
|
|
let error =
|
|
GethConfig::parse("[sync]\nlive_sync_interval_ms = \"fast\"\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidLiveSyncInterval(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn config_rejects_non_bool_local_discovery() {
|
|
let error = GethConfig::parse("[iroh]\nlocal_discovery = \"yes\"\n").expect_err("error");
|
|
assert!(matches!(error, ConfigError::InvalidLocalDiscovery(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn control_endpoint_suffix_is_stable_and_home_scoped() {
|
|
assert_eq!(
|
|
stable_control_endpoint_suffix(b"C:/Users/Eric/geth"),
|
|
stable_control_endpoint_suffix(b"C:/Users/Eric/geth")
|
|
);
|
|
assert_ne!(
|
|
stable_control_endpoint_suffix(b"C:/Users/Eric/geth"),
|
|
stable_control_endpoint_suffix(b"C:/Users/Eric/other")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_set_preserves_comments_and_validates_the_result() {
|
|
let home = tempfile::tempdir().expect("tempdir");
|
|
let path = home.path().join("config.toml");
|
|
std::fs::write(
|
|
&path,
|
|
"# operator note\n[iroh]\nrelay_mode = \"default\" # keep relays\n\n[sync]\nlive_sync_enabled = true\nlive_sync_interval_ms = 30000\n",
|
|
)
|
|
.expect("write config");
|
|
|
|
let config = GethConfig::set(&path, ConfigKey::SyncLiveSyncIntervalMs, "750")
|
|
.expect("update config");
|
|
assert_eq!(config.sync.live_sync_interval_ms, 750);
|
|
let updated = std::fs::read_to_string(&path).expect("read config");
|
|
assert!(updated.contains("# operator note"));
|
|
assert!(updated.contains("relay_mode = \"default\" # keep relays"));
|
|
assert!(updated.contains("live_sync_interval_ms = 750"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_set_does_not_replace_a_valid_file_with_an_invalid_update() {
|
|
let home = tempfile::tempdir().expect("tempdir");
|
|
let path = home.path().join("config.toml");
|
|
let original = GethConfig::default_toml();
|
|
std::fs::write(&path, original).expect("write config");
|
|
|
|
let error = GethConfig::set(&path, ConfigKey::IrohRelayMode, "custom")
|
|
.expect_err("custom mode needs an existing map");
|
|
assert!(matches!(error, ConfigError::MissingRelayMapSelection));
|
|
assert_eq!(
|
|
std::fs::read_to_string(path).expect("read config"),
|
|
original
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_set_creates_a_default_config_when_missing() {
|
|
let home = tempfile::tempdir().expect("tempdir");
|
|
let path = home.path().join("nested/config.toml");
|
|
|
|
let config =
|
|
GethConfig::set(&path, ConfigKey::IrohLocalDiscovery, "false").expect("create config");
|
|
assert!(!config.iroh.local_discovery);
|
|
assert!(path.is_file());
|
|
}
|
|
}
|