Skip to main content

ferripfs_config/
lib.rs

1// Ported from: kubo/config/config.go
2// Kubo version: v0.39.0
3// Original: https://github.com/ipfs/kubo/blob/v0.39.0/config/config.go
4//
5// Original work: Copyright (c) Protocol Labs, Inc.
6// Port: Copyright (c) 2026 ferripfs contributors
7// SPDX-License-Identifier: MIT OR Apache-2.0
8
9//! Configuration types for ferripfs, ported from Kubo's config package.
10
11mod addresses;
12mod api;
13mod datastore;
14mod gateway;
15mod identity;
16mod profile;
17mod routing;
18mod swarm;
19mod types;
20
21pub use addresses::*;
22pub use api::*;
23pub use datastore::*;
24pub use gateway::*;
25pub use identity::*;
26pub use profile::*;
27pub use routing::*;
28pub use swarm::*;
29pub use types::*;
30
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34/// Default IPFS path name
35pub const DEFAULT_PATH_NAME: &str = ".ipfs";
36
37/// Default IPFS path root
38pub const DEFAULT_PATH_ROOT: &str = "~/.ipfs";
39
40/// Default config file name
41pub const DEFAULT_CONFIG_FILE: &str = "config";
42
43/// Environment variable for IPFS path
44pub const ENV_DIR: &str = "IPFS_PATH";
45
46/// Main configuration struct, matching Kubo's Config
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48#[serde(rename_all = "PascalCase")]
49pub struct Config {
50    /// Node identity (PeerID and private key)
51    #[serde(default)]
52    pub identity: Identity,
53
54    /// Datastore configuration
55    #[serde(default)]
56    pub datastore: Datastore,
57
58    /// Network addresses
59    #[serde(default)]
60    pub addresses: Addresses,
61
62    /// Mount points configuration
63    #[serde(default)]
64    pub mounts: Mounts,
65
66    /// Discovery settings
67    #[serde(default)]
68    pub discovery: Discovery,
69
70    /// Routing configuration
71    #[serde(default)]
72    pub routing: Routing,
73
74    /// IPNS settings
75    #[serde(default, rename = "Ipns")]
76    pub ipns: Ipns,
77
78    /// Bootstrap peers (multiaddrs)
79    #[serde(default, rename = "Bootstrap")]
80    pub bootstrap: Vec<String>,
81
82    /// Gateway configuration
83    #[serde(default)]
84    pub gateway: Gateway,
85
86    /// API configuration
87    #[serde(default, rename = "API")]
88    pub api: Api,
89
90    /// Swarm configuration
91    #[serde(default)]
92    pub swarm: SwarmConfig,
93
94    /// AutoNAT configuration
95    #[serde(default, rename = "AutoNAT")]
96    pub auto_nat: AutoNatConfig,
97
98    /// Pubsub configuration
99    #[serde(default)]
100    pub pubsub: PubsubConfig,
101
102    /// Peering configuration
103    #[serde(default)]
104    pub peering: Peering,
105
106    /// DNS configuration
107    #[serde(default, rename = "DNS")]
108    pub dns: DnsConfig,
109
110    /// Migration settings
111    #[serde(default)]
112    pub migration: Migration,
113
114    /// Experimental features
115    #[serde(default)]
116    pub experimental: Experiments,
117
118    /// Pinning configuration
119    #[serde(default)]
120    pub pinning: Pinning,
121
122    /// Import settings
123    #[serde(default)]
124    pub import: Import,
125
126    /// Internal settings
127    #[serde(default)]
128    pub internal: Internal,
129}
130
131impl Config {
132    /// Parse config from JSON string.
133    ///
134    /// # Security
135    ///
136    /// This performs only JSON deserialization. Call `validate()` after
137    /// deserialization to perform security-relevant validation.
138    pub fn parse(s: &str) -> Result<Self, serde_json::Error> {
139        serde_json::from_str(s)
140    }
141
142    /// Parse config from reader.
143    ///
144    /// # Security
145    ///
146    /// This performs only JSON deserialization. Call `validate()` after
147    /// deserialization to perform security-relevant validation.
148    pub fn from_reader<R: std::io::Read>(reader: R) -> Result<Self, serde_json::Error> {
149        serde_json::from_reader(reader)
150    }
151
152    /// Validate configuration values.
153    ///
154    /// # Security
155    ///
156    /// This validates:
157    /// - Address formats (API, Gateway, Swarm)
158    /// - Bootstrap peer multiaddrs
159    /// - Peer ID format (when present)
160    /// - Numeric values are within reasonable ranges
161    ///
162    /// Returns a list of validation warnings (non-fatal issues) and
163    /// returns an error for critical validation failures.
164    pub fn validate(&self) -> Result<Vec<String>, ConfigError> {
165        let mut warnings = Vec::new();
166
167        // Validate swarm addresses
168        for addr in &self.addresses.swarm {
169            if !Self::is_valid_multiaddr_format(addr) {
170                warnings.push(format!("Swarm address may be malformed: {}", addr));
171            }
172        }
173
174        // Validate API addresses
175        for addr in self.addresses.api.iter() {
176            if !Self::is_valid_multiaddr_format(addr) {
177                warnings.push(format!("API address may be malformed: {}", addr));
178            }
179        }
180
181        // Validate Gateway addresses
182        for addr in self.addresses.gateway.iter() {
183            if !Self::is_valid_multiaddr_format(addr) {
184                warnings.push(format!("Gateway address may be malformed: {}", addr));
185            }
186        }
187
188        // Validate bootstrap peers
189        for addr in &self.bootstrap {
190            if !Self::is_valid_multiaddr_format(addr) {
191                warnings.push(format!("Bootstrap address may be malformed: {}", addr));
192            }
193        }
194
195        // Validate peer ID format (should be base58 encoded)
196        if !self.identity.peer_id.is_empty()
197            && !Self::is_valid_peer_id_format(&self.identity.peer_id)
198        {
199            return Err(ConfigError::InvalidKey(format!(
200                "Invalid PeerID format: {}",
201                self.identity.peer_id
202            )));
203        }
204
205        // Validate connection manager values if present
206        let conn_mgr = &self.swarm.conn_mgr;
207        if let (Some(ref low), Some(ref high)) = (&conn_mgr.low_water, &conn_mgr.high_water) {
208            if let (Some(low_val), Some(high_val)) = (low.value(), high.value()) {
209                if low_val > high_val {
210                    warnings.push(format!(
211                        "ConnMgr.LowWater ({}) > HighWater ({})",
212                        low_val, high_val
213                    ));
214                }
215            }
216        }
217
218        Ok(warnings)
219    }
220
221    /// Check if a string looks like a valid multiaddr format.
222    ///
223    /// This is a basic format check, not full multiaddr validation.
224    fn is_valid_multiaddr_format(addr: &str) -> bool {
225        // Basic checks: must start with /, contain valid protocol names
226        if !addr.starts_with('/') {
227            return false;
228        }
229        // Should contain at least one known protocol
230        let known_protocols = [
231            "/ip4/",
232            "/ip6/",
233            "/tcp/",
234            "/udp/",
235            "/quic/",
236            "/quic-v1/",
237            "/ws/",
238            "/wss/",
239            "/p2p/",
240            "/ipfs/",
241            "/dns/",
242            "/dns4/",
243            "/dns6/",
244            "/dnsaddr/",
245            "/unix/",
246        ];
247        known_protocols.iter().any(|p| addr.contains(p))
248    }
249
250    /// Check if a string looks like a valid peer ID format.
251    ///
252    /// Peer IDs are typically base58-encoded multihashes starting with "12D3" or "Qm".
253    fn is_valid_peer_id_format(peer_id: &str) -> bool {
254        // Peer IDs are base58 encoded, common prefixes are:
255        // - "12D3K" for Ed25519 keys
256        // - "Qm" for RSA keys (sha256)
257        // - "16U" for secp256k1
258        if peer_id.is_empty() {
259            return false;
260        }
261        // Must be valid base58 characters
262        peer_id
263            .chars()
264            .all(|c| c.is_ascii_alphanumeric() && c != '0' && c != 'O' && c != 'I' && c != 'l')
265    }
266
267    /// Serialize config to JSON string
268    pub fn to_string_pretty(&self) -> Result<String, serde_json::Error> {
269        serde_json::to_string_pretty(self)
270    }
271
272    /// Get a config value by dot-separated key path
273    pub fn get_key(&self, key: &str) -> Option<serde_json::Value> {
274        let json = serde_json::to_value(self).ok()?;
275        let parts: Vec<&str> = key.split('.').collect();
276        let mut current = &json;
277
278        for part in parts {
279            current = current.get(part)?;
280        }
281
282        Some(current.clone())
283    }
284
285    /// Set a config value by dot-separated key path
286    pub fn set_key(&mut self, key: &str, value: serde_json::Value) -> Result<(), ConfigError> {
287        let mut json = serde_json::to_value(&self)?;
288        let parts: Vec<&str> = key.split('.').collect();
289
290        if parts.is_empty() {
291            return Err(ConfigError::InvalidKey(key.to_string()));
292        }
293
294        // Navigate to the parent object
295        let mut current = &mut json;
296        for part in &parts[..parts.len() - 1] {
297            current = current
298                .get_mut(*part)
299                .ok_or_else(|| ConfigError::InvalidKey(key.to_string()))?;
300        }
301
302        // Set the final value
303        let last_part = parts.last().unwrap();
304        if let Some(obj) = current.as_object_mut() {
305            obj.insert(last_part.to_string(), value);
306        } else {
307            return Err(ConfigError::InvalidKey(key.to_string()));
308        }
309
310        *self = serde_json::from_value(json)?;
311        Ok(())
312    }
313}
314
315/// Mount points configuration
316#[derive(Debug, Clone, Default, Serialize, Deserialize)]
317#[serde(rename_all = "PascalCase")]
318pub struct Mounts {
319    #[serde(default, rename = "IPFS")]
320    pub ipfs: String,
321    #[serde(default, rename = "IPNS")]
322    pub ipns: String,
323    #[serde(default, rename = "FuseAllowOther")]
324    pub fuse_allow_other: bool,
325}
326
327/// Discovery configuration
328#[derive(Debug, Clone, Default, Serialize, Deserialize)]
329#[serde(rename_all = "PascalCase")]
330pub struct Discovery {
331    #[serde(default, rename = "MDNS")]
332    pub mdns: Mdns,
333}
334
335/// mDNS discovery settings
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
337#[serde(rename_all = "PascalCase")]
338pub struct Mdns {
339    #[serde(default)]
340    pub enabled: bool,
341}
342
343/// IPNS configuration
344#[derive(Debug, Clone, Default, Serialize, Deserialize)]
345#[serde(rename_all = "PascalCase")]
346pub struct Ipns {
347    #[serde(default)]
348    pub republish_period: String,
349    #[serde(default)]
350    pub record_lifetime: String,
351    #[serde(default)]
352    pub resolve_cache_size: i32,
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub max_cache_ttl: Option<OptionalDuration>,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub use_pubsub: Option<Flag>,
357}
358
359/// AutoNAT configuration
360#[derive(Debug, Clone, Default, Serialize, Deserialize)]
361#[serde(rename_all = "PascalCase")]
362pub struct AutoNatConfig {
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub service_mode: Option<String>,
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub throttle: Option<AutoNatThrottle>,
367}
368
369/// AutoNAT throttle settings
370#[derive(Debug, Clone, Default, Serialize, Deserialize)]
371#[serde(rename_all = "PascalCase")]
372pub struct AutoNatThrottle {
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub global_limit: Option<i32>,
375    #[serde(default, skip_serializing_if = "Option::is_none")]
376    pub peer_limit: Option<i32>,
377    #[serde(default, skip_serializing_if = "Option::is_none")]
378    pub interval: Option<OptionalDuration>,
379}
380
381/// Pubsub configuration
382#[derive(Debug, Clone, Default, Serialize, Deserialize)]
383#[serde(rename_all = "PascalCase")]
384pub struct PubsubConfig {
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub enabled: Option<Flag>,
387    #[serde(default)]
388    pub router: String,
389}
390
391/// Peering configuration
392#[derive(Debug, Clone, Default, Serialize, Deserialize)]
393#[serde(rename_all = "PascalCase")]
394pub struct Peering {
395    #[serde(default)]
396    pub peers: Vec<PeeringPeer>,
397}
398
399/// A peer in the peering list
400#[derive(Debug, Clone, Default, Serialize, Deserialize)]
401#[serde(rename_all = "PascalCase")]
402pub struct PeeringPeer {
403    #[serde(default, rename = "ID")]
404    pub id: String,
405    #[serde(default)]
406    pub addrs: Vec<String>,
407}
408
409/// DNS configuration
410#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411#[serde(rename_all = "PascalCase")]
412pub struct DnsConfig {
413    #[serde(default)]
414    pub resolvers: HashMap<String, String>,
415}
416
417/// Migration settings
418#[derive(Debug, Clone, Default, Serialize, Deserialize)]
419#[serde(rename_all = "PascalCase")]
420pub struct Migration {
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub download_sources: Option<Vec<String>>,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub keep: Option<String>,
425}
426
427/// Experimental features
428#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429#[serde(rename_all = "PascalCase")]
430pub struct Experiments {
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub filestore_enabled: Option<bool>,
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub url_store_enabled: Option<bool>,
435    #[serde(
436        default,
437        skip_serializing_if = "Option::is_none",
438        rename = "GraphsyncEnabled"
439    )]
440    pub graphsync_enabled: Option<bool>,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub libp2p_stream_mounting: Option<bool>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub p2p_http_proxy: Option<bool>,
445    #[serde(
446        default,
447        skip_serializing_if = "Option::is_none",
448        rename = "OptimisticProvide"
449    )]
450    pub optimistic_provide: Option<bool>,
451    #[serde(
452        default,
453        skip_serializing_if = "Option::is_none",
454        rename = "OptimisticProvideJobsPoolSize"
455    )]
456    pub optimistic_provide_jobs_pool_size: Option<i32>,
457}
458
459/// Pinning configuration
460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461#[serde(rename_all = "PascalCase")]
462pub struct Pinning {
463    #[serde(default)]
464    pub remote_services: HashMap<String, RemotePinningService>,
465}
466
467/// Remote pinning service configuration
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
469#[serde(rename_all = "PascalCase")]
470pub struct RemotePinningService {
471    #[serde(default, rename = "API")]
472    pub api: RemotePinningApi,
473    #[serde(default)]
474    pub policies: RemotePinningPolicies,
475}
476
477/// Remote pinning API settings
478#[derive(Debug, Clone, Default, Serialize, Deserialize)]
479#[serde(rename_all = "PascalCase")]
480pub struct RemotePinningApi {
481    #[serde(default)]
482    pub endpoint: String,
483    #[serde(default)]
484    pub key: String,
485}
486
487/// Remote pinning policies
488#[derive(Debug, Clone, Default, Serialize, Deserialize)]
489#[serde(rename_all = "PascalCase")]
490pub struct RemotePinningPolicies {
491    #[serde(default, rename = "MFS")]
492    pub mfs: MfsPinPolicy,
493}
494
495/// MFS pin policy
496#[derive(Debug, Clone, Default, Serialize, Deserialize)]
497#[serde(rename_all = "PascalCase")]
498pub struct MfsPinPolicy {
499    #[serde(default)]
500    pub enabled: bool,
501    #[serde(default)]
502    pub pin_name: String,
503    #[serde(default)]
504    pub repinning_interval: String,
505}
506
507/// Import configuration
508#[derive(Debug, Clone, Default, Serialize, Deserialize)]
509#[serde(rename_all = "PascalCase")]
510pub struct Import {
511    #[serde(
512        default,
513        skip_serializing_if = "Option::is_none",
514        rename = "CidVersion"
515    )]
516    pub cid_version: Option<OptionalInteger>,
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub hash_function: Option<OptionalString>,
519    #[serde(default, skip_serializing_if = "Option::is_none")]
520    pub unixfs_raw_leaves: Option<Flag>,
521    #[serde(
522        default,
523        skip_serializing_if = "Option::is_none",
524        rename = "UnixFSChunker"
525    )]
526    pub unixfs_chunker: Option<OptionalString>,
527}
528
529/// Internal settings
530#[derive(Debug, Clone, Default, Serialize, Deserialize)]
531#[serde(rename_all = "PascalCase")]
532pub struct Internal {
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub bitswap: Option<InternalBitswap>,
535    #[serde(
536        default,
537        skip_serializing_if = "Option::is_none",
538        rename = "UnixFSShardingSizeThreshold"
539    )]
540    pub unixfs_sharding_size_threshold: Option<OptionalString>,
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub libp2p_force_pnet: Option<Flag>,
543    #[serde(default, skip_serializing_if = "Option::is_none")]
544    pub backoff_init: Option<OptionalDuration>,
545    #[serde(default, skip_serializing_if = "Option::is_none")]
546    pub backoff_max: Option<OptionalDuration>,
547}
548
549/// Internal bitswap settings
550#[derive(Debug, Clone, Default, Serialize, Deserialize)]
551#[serde(rename_all = "PascalCase")]
552pub struct InternalBitswap {
553    #[serde(default, skip_serializing_if = "Option::is_none")]
554    pub task_worker_count: Option<OptionalInteger>,
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub engine_block_store_worker_count: Option<OptionalInteger>,
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    pub engine_task_worker_count: Option<OptionalInteger>,
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub max_outstanding_bytes_per_peer: Option<OptionalInteger>,
561    #[serde(default, skip_serializing_if = "Option::is_none")]
562    pub provide_enabled: Option<Flag>,
563}
564
565/// Configuration error type
566#[derive(Debug, thiserror::Error)]
567pub enum ConfigError {
568    #[error("JSON error: {0}")]
569    Json(#[from] serde_json::Error),
570    #[error("Invalid key: {0}")]
571    InvalidKey(String),
572    #[error("IO error: {0}")]
573    Io(#[from] std::io::Error),
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_default_config() {
582        let config = Config::default();
583        let json = config.to_string_pretty().unwrap();
584        assert!(json.contains("Identity"));
585    }
586
587    #[test]
588    fn test_config_roundtrip() {
589        let config = Config::default();
590        let json = config.to_string_pretty().unwrap();
591        let parsed: Config = Config::parse(&json).unwrap();
592        assert_eq!(config.identity.peer_id, parsed.identity.peer_id);
593    }
594}