mod addresses;
mod api;
mod datastore;
mod gateway;
mod identity;
mod profile;
mod routing;
mod swarm;
mod types;
pub use addresses::*;
pub use api::*;
pub use datastore::*;
pub use gateway::*;
pub use identity::*;
pub use profile::*;
pub use routing::*;
pub use swarm::*;
pub use types::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const DEFAULT_PATH_NAME: &str = ".ipfs";
pub const DEFAULT_PATH_ROOT: &str = "~/.ipfs";
pub const DEFAULT_CONFIG_FILE: &str = "config";
pub const ENV_DIR: &str = "IPFS_PATH";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Config {
#[serde(default)]
pub identity: Identity,
#[serde(default)]
pub datastore: Datastore,
#[serde(default)]
pub addresses: Addresses,
#[serde(default)]
pub mounts: Mounts,
#[serde(default)]
pub discovery: Discovery,
#[serde(default)]
pub routing: Routing,
#[serde(default, rename = "Ipns")]
pub ipns: Ipns,
#[serde(default, rename = "Bootstrap")]
pub bootstrap: Vec<String>,
#[serde(default)]
pub gateway: Gateway,
#[serde(default, rename = "API")]
pub api: Api,
#[serde(default)]
pub swarm: SwarmConfig,
#[serde(default, rename = "AutoNAT")]
pub auto_nat: AutoNatConfig,
#[serde(default)]
pub pubsub: PubsubConfig,
#[serde(default)]
pub peering: Peering,
#[serde(default, rename = "DNS")]
pub dns: DnsConfig,
#[serde(default)]
pub migration: Migration,
#[serde(default)]
pub experimental: Experiments,
#[serde(default)]
pub pinning: Pinning,
#[serde(default)]
pub import: Import,
#[serde(default)]
pub internal: Internal,
}
impl Config {
pub fn parse(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
pub fn from_reader<R: std::io::Read>(reader: R) -> Result<Self, serde_json::Error> {
serde_json::from_reader(reader)
}
pub fn validate(&self) -> Result<Vec<String>, ConfigError> {
let mut warnings = Vec::new();
for addr in &self.addresses.swarm {
if !Self::is_valid_multiaddr_format(addr) {
warnings.push(format!("Swarm address may be malformed: {}", addr));
}
}
for addr in self.addresses.api.iter() {
if !Self::is_valid_multiaddr_format(addr) {
warnings.push(format!("API address may be malformed: {}", addr));
}
}
for addr in self.addresses.gateway.iter() {
if !Self::is_valid_multiaddr_format(addr) {
warnings.push(format!("Gateway address may be malformed: {}", addr));
}
}
for addr in &self.bootstrap {
if !Self::is_valid_multiaddr_format(addr) {
warnings.push(format!("Bootstrap address may be malformed: {}", addr));
}
}
if !self.identity.peer_id.is_empty()
&& !Self::is_valid_peer_id_format(&self.identity.peer_id)
{
return Err(ConfigError::InvalidKey(format!(
"Invalid PeerID format: {}",
self.identity.peer_id
)));
}
let conn_mgr = &self.swarm.conn_mgr;
if let (Some(ref low), Some(ref high)) = (&conn_mgr.low_water, &conn_mgr.high_water) {
if let (Some(low_val), Some(high_val)) = (low.value(), high.value()) {
if low_val > high_val {
warnings.push(format!(
"ConnMgr.LowWater ({}) > HighWater ({})",
low_val, high_val
));
}
}
}
Ok(warnings)
}
fn is_valid_multiaddr_format(addr: &str) -> bool {
if !addr.starts_with('/') {
return false;
}
let known_protocols = [
"/ip4/",
"/ip6/",
"/tcp/",
"/udp/",
"/quic/",
"/quic-v1/",
"/ws/",
"/wss/",
"/p2p/",
"/ipfs/",
"/dns/",
"/dns4/",
"/dns6/",
"/dnsaddr/",
"/unix/",
];
known_protocols.iter().any(|p| addr.contains(p))
}
fn is_valid_peer_id_format(peer_id: &str) -> bool {
if peer_id.is_empty() {
return false;
}
peer_id
.chars()
.all(|c| c.is_ascii_alphanumeric() && c != '0' && c != 'O' && c != 'I' && c != 'l')
}
pub fn to_string_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn get_key(&self, key: &str) -> Option<serde_json::Value> {
let json = serde_json::to_value(self).ok()?;
let parts: Vec<&str> = key.split('.').collect();
let mut current = &json;
for part in parts {
current = current.get(part)?;
}
Some(current.clone())
}
pub fn set_key(&mut self, key: &str, value: serde_json::Value) -> Result<(), ConfigError> {
let mut json = serde_json::to_value(&self)?;
let parts: Vec<&str> = key.split('.').collect();
if parts.is_empty() {
return Err(ConfigError::InvalidKey(key.to_string()));
}
let mut current = &mut json;
for part in &parts[..parts.len() - 1] {
current = current
.get_mut(*part)
.ok_or_else(|| ConfigError::InvalidKey(key.to_string()))?;
}
let last_part = parts.last().unwrap();
if let Some(obj) = current.as_object_mut() {
obj.insert(last_part.to_string(), value);
} else {
return Err(ConfigError::InvalidKey(key.to_string()));
}
*self = serde_json::from_value(json)?;
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Mounts {
#[serde(default, rename = "IPFS")]
pub ipfs: String,
#[serde(default, rename = "IPNS")]
pub ipns: String,
#[serde(default, rename = "FuseAllowOther")]
pub fuse_allow_other: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Discovery {
#[serde(default, rename = "MDNS")]
pub mdns: Mdns,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Mdns {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Ipns {
#[serde(default)]
pub republish_period: String,
#[serde(default)]
pub record_lifetime: String,
#[serde(default)]
pub resolve_cache_size: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cache_ttl: Option<OptionalDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub use_pubsub: Option<Flag>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AutoNatConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub throttle: Option<AutoNatThrottle>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AutoNatThrottle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub global_limit: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_limit: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval: Option<OptionalDuration>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PubsubConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<Flag>,
#[serde(default)]
pub router: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Peering {
#[serde(default)]
pub peers: Vec<PeeringPeer>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PeeringPeer {
#[serde(default, rename = "ID")]
pub id: String,
#[serde(default)]
pub addrs: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DnsConfig {
#[serde(default)]
pub resolvers: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Migration {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub download_sources: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub keep: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Experiments {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filestore_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url_store_enabled: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "GraphsyncEnabled"
)]
pub graphsync_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub libp2p_stream_mounting: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub p2p_http_proxy: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "OptimisticProvide"
)]
pub optimistic_provide: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "OptimisticProvideJobsPoolSize"
)]
pub optimistic_provide_jobs_pool_size: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Pinning {
#[serde(default)]
pub remote_services: HashMap<String, RemotePinningService>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RemotePinningService {
#[serde(default, rename = "API")]
pub api: RemotePinningApi,
#[serde(default)]
pub policies: RemotePinningPolicies,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RemotePinningApi {
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub key: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RemotePinningPolicies {
#[serde(default, rename = "MFS")]
pub mfs: MfsPinPolicy,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MfsPinPolicy {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub pin_name: String,
#[serde(default)]
pub repinning_interval: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Import {
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "CidVersion"
)]
pub cid_version: Option<OptionalInteger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash_function: Option<OptionalString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unixfs_raw_leaves: Option<Flag>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "UnixFSChunker"
)]
pub unixfs_chunker: Option<OptionalString>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Internal {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bitswap: Option<InternalBitswap>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "UnixFSShardingSizeThreshold"
)]
pub unixfs_sharding_size_threshold: Option<OptionalString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub libp2p_force_pnet: Option<Flag>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backoff_init: Option<OptionalDuration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backoff_max: Option<OptionalDuration>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct InternalBitswap {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_worker_count: Option<OptionalInteger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine_block_store_worker_count: Option<OptionalInteger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine_task_worker_count: Option<OptionalInteger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_outstanding_bytes_per_peer: Option<OptionalInteger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provide_enabled: Option<Flag>,
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid key: {0}")]
InvalidKey(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
let json = config.to_string_pretty().unwrap();
assert!(json.contains("Identity"));
}
#[test]
fn test_config_roundtrip() {
let config = Config::default();
let json = config.to_string_pretty().unwrap();
let parsed: Config = Config::parse(&json).unwrap();
assert_eq!(config.identity.peer_id, parsed.identity.peer_id);
}
}