Skip to main content

ferripfs_network/
lib.rs

1// Ported from: kubo/core/node/libp2p
2// Kubo version: v0.39.0
3// Original: https://github.com/ipfs/kubo/tree/v0.39.0/core/node/libp2p
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//! libp2p networking for ferripfs, ported from Kubo's core/node/libp2p.
10//!
11//! This crate provides:
12//! - libp2p host initialization with configurable transports
13//! - TCP, QUIC, WebSocket transport support
14//! - Noise encryption and Yamux multiplexing
15//! - mDNS local peer discovery
16//! - Kademlia DHT for peer/content routing
17//! - AutoNAT for NAT detection
18//! - Circuit relay client
19//! - Connection management
20//! - Daemon process management
21
22pub mod behavior;
23pub mod daemon;
24pub mod host;
25pub mod swarm;
26
27pub use behavior::FerripfsBehavior;
28pub use daemon::{Daemon, DaemonConfig, DaemonHandle};
29pub use host::{HostConfig, NetworkHost};
30pub use swarm::{SwarmBuilder, SwarmConfig};
31
32use thiserror::Error;
33
34/// Network operation error types
35#[derive(Debug, Error)]
36pub enum NetworkError {
37    #[error("Failed to initialize libp2p: {0}")]
38    Init(String),
39
40    #[error("Transport error: {0}")]
41    Transport(String),
42
43    #[error("Swarm error: {0}")]
44    Swarm(String),
45
46    #[error("Connection error: {0}")]
47    Connection(String),
48
49    #[error("Peer not found: {0}")]
50    PeerNotFound(String),
51
52    #[error("Invalid multiaddr: {0}")]
53    InvalidMultiaddr(String),
54
55    #[error("Invalid peer ID: {0}")]
56    InvalidPeerId(String),
57
58    #[error("Daemon error: {0}")]
59    Daemon(String),
60
61    #[error("Already running")]
62    AlreadyRunning,
63
64    #[error("Not running")]
65    NotRunning,
66
67    #[error("Config error: {0}")]
68    Config(#[from] ferripfs_config::ConfigError),
69
70    #[error("Repo error: {0}")]
71    Repo(#[from] ferripfs_repo::RepoError),
72
73    #[error("IO error: {0}")]
74    Io(#[from] std::io::Error),
75}
76
77/// Result type for network operations
78pub type NetworkResult<T> = Result<T, NetworkError>;
79
80/// Peer information returned by id command
81#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
82pub struct PeerInfo {
83    #[serde(rename = "ID")]
84    pub id: String,
85    #[serde(rename = "PublicKey")]
86    pub public_key: String,
87    #[serde(rename = "Addresses")]
88    pub addresses: Vec<String>,
89    #[serde(rename = "AgentVersion")]
90    pub agent_version: String,
91    #[serde(rename = "ProtocolVersion")]
92    pub protocol_version: String,
93    #[serde(rename = "Protocols")]
94    pub protocols: Vec<String>,
95}
96
97/// Connected peer information
98#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
99pub struct ConnectedPeer {
100    #[serde(rename = "Addr")]
101    pub addr: String,
102    #[serde(rename = "Peer")]
103    pub peer: String,
104    #[serde(rename = "Latency")]
105    pub latency: Option<String>,
106    #[serde(rename = "Muxer")]
107    pub muxer: Option<String>,
108    #[serde(rename = "Direction")]
109    pub direction: String,
110}
111
112/// Ping result
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub struct PingResult {
115    #[serde(rename = "Success")]
116    pub success: bool,
117    #[serde(rename = "Time")]
118    pub time: u64,
119    #[serde(rename = "Text")]
120    pub text: String,
121}
122
123/// DHT provider information
124#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
125pub struct DhtProvider {
126    #[serde(rename = "ID")]
127    pub id: String,
128    #[serde(rename = "Addrs")]
129    pub addrs: Vec<String>,
130}
131
132/// DHT query result
133#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
134pub struct DhtQueryResult {
135    #[serde(rename = "Type")]
136    pub result_type: String,
137    #[serde(rename = "Responses")]
138    pub responses: Vec<DhtProvider>,
139    #[serde(rename = "Extra")]
140    pub extra: Option<String>,
141    #[serde(rename = "Success")]
142    pub success: bool,
143    #[serde(rename = "Error")]
144    pub error: Option<String>,
145}
146
147impl DhtQueryResult {
148    /// Create a successful query result.
149    pub fn success(result_type: &str, responses: Vec<DhtProvider>) -> Self {
150        Self {
151            result_type: result_type.to_string(),
152            responses,
153            extra: None,
154            success: true,
155            error: None,
156        }
157    }
158
159    /// Create a successful query result with extra data.
160    pub fn success_with_extra(result_type: &str, extra: String) -> Self {
161        Self {
162            result_type: result_type.to_string(),
163            responses: vec![],
164            extra: Some(extra),
165            success: true,
166            error: None,
167        }
168    }
169
170    /// Create a failed query result.
171    pub fn failure(result_type: &str, error: String) -> Self {
172        Self {
173            result_type: result_type.to_string(),
174            responses: vec![],
175            extra: None,
176            success: false,
177            error: Some(error),
178        }
179    }
180}
181
182/// DHT statistics
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
184pub struct DhtStats {
185    #[serde(rename = "Name")]
186    pub name: String,
187    #[serde(rename = "Buckets")]
188    pub buckets: u32,
189    #[serde(rename = "TotalPeers")]
190    pub total_peers: u32,
191    #[serde(rename = "Mode")]
192    pub mode: String,
193}
194
195// =============================================================================
196// Bitswap Types (Phase 9)
197// Ported from: boxo/bitswap
198// =============================================================================
199
200/// Bitswap statistics
201/// Ported from: boxo/bitswap/stat.go
202#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
203pub struct BitswapStats {
204    /// Protocol version
205    #[serde(rename = "ProvideBufLen")]
206    pub provide_buf_len: u64,
207    /// Number of blocks received from other peers
208    #[serde(rename = "BlocksReceived")]
209    pub blocks_received: u64,
210    /// Total bytes of data received
211    #[serde(rename = "DataReceived")]
212    pub data_received: u64,
213    /// Number of blocks sent to other peers
214    #[serde(rename = "BlocksSent")]
215    pub blocks_sent: u64,
216    /// Total bytes of data sent
217    #[serde(rename = "DataSent")]
218    pub data_sent: u64,
219    /// Number of duplicate blocks received
220    #[serde(rename = "DupBlksReceived")]
221    pub dup_blks_received: u64,
222    /// Total bytes of duplicate data received
223    #[serde(rename = "DupDataReceived")]
224    pub dup_data_received: u64,
225    /// Number of messages received
226    #[serde(rename = "MessagesReceived")]
227    pub messages_received: u64,
228    /// Number of peers in the wantlist
229    #[serde(rename = "Wantlist")]
230    pub wantlist: Vec<WantlistEntry>,
231    /// Connected peers
232    #[serde(rename = "Peers")]
233    pub peers: Vec<String>,
234}
235
236impl BitswapStats {
237    /// Create new empty stats
238    pub fn new() -> Self {
239        Self::default()
240    }
241
242    /// Record a block received
243    pub fn record_block_received(&mut self, size: u64) {
244        self.blocks_received += 1;
245        self.data_received += size;
246    }
247
248    /// Record a block sent
249    pub fn record_block_sent(&mut self, size: u64) {
250        self.blocks_sent += 1;
251        self.data_sent += size;
252    }
253
254    /// Record a duplicate block received
255    pub fn record_duplicate(&mut self, size: u64) {
256        self.dup_blks_received += 1;
257        self.dup_data_received += size;
258    }
259
260    /// Record a message received
261    pub fn record_message(&mut self) {
262        self.messages_received += 1;
263    }
264}
265
266/// Wantlist entry
267/// Ported from: boxo/bitswap/wantlist/wantlist.go
268#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
269pub struct WantlistEntry {
270    /// CID of the wanted block
271    #[serde(rename = "/")]
272    pub cid: String,
273    /// Priority of this want (higher = more important)
274    #[serde(rename = "Priority", skip_serializing_if = "Option::is_none")]
275    pub priority: Option<i32>,
276    /// Whether this is a want-have or want-block
277    #[serde(rename = "WantType", skip_serializing_if = "Option::is_none")]
278    pub want_type: Option<String>,
279}
280
281impl WantlistEntry {
282    /// Create a new wantlist entry
283    pub fn new(cid: String) -> Self {
284        Self {
285            cid,
286            priority: None,
287            want_type: None,
288        }
289    }
290
291    /// Create a new wantlist entry with priority
292    pub fn with_priority(cid: String, priority: i32) -> Self {
293        Self {
294            cid,
295            priority: Some(priority),
296            want_type: None,
297        }
298    }
299
300    /// Create a want-block entry
301    pub fn want_block(cid: String, priority: i32) -> Self {
302        Self {
303            cid,
304            priority: Some(priority),
305            want_type: Some("Block".to_string()),
306        }
307    }
308
309    /// Create a want-have entry
310    pub fn want_have(cid: String, priority: i32) -> Self {
311        Self {
312            cid,
313            priority: Some(priority),
314            want_type: Some("Have".to_string()),
315        }
316    }
317}
318
319/// Bitswap ledger for a peer
320/// Ported from: boxo/bitswap/decision/ledger.go
321#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
322pub struct BitswapLedger {
323    /// Peer ID this ledger is for
324    #[serde(rename = "Peer")]
325    pub peer: String,
326    /// Value exchanged (positive = we sent more, negative = they sent more)
327    #[serde(rename = "Value")]
328    pub value: f64,
329    /// Number of bytes sent to this peer
330    #[serde(rename = "Sent")]
331    pub sent: u64,
332    /// Number of bytes received from this peer
333    #[serde(rename = "Recv")]
334    pub recv: u64,
335    /// Number of blocks exchanged
336    #[serde(rename = "Exchanged")]
337    pub exchanged: u64,
338}
339
340impl BitswapLedger {
341    /// Create a new ledger for a peer
342    pub fn new(peer: String) -> Self {
343        Self {
344            peer,
345            value: 0.0,
346            sent: 0,
347            recv: 0,
348            exchanged: 0,
349        }
350    }
351
352    /// Record bytes sent to this peer
353    pub fn record_sent(&mut self, bytes: u64) {
354        self.sent += bytes;
355        self.exchanged += 1;
356        self.update_value();
357    }
358
359    /// Record bytes received from this peer
360    pub fn record_recv(&mut self, bytes: u64) {
361        self.recv += bytes;
362        self.exchanged += 1;
363        self.update_value();
364    }
365
366    /// Update the exchange value (debt ratio)
367    fn update_value(&mut self) {
368        if self.recv > 0 {
369            self.value = self.sent as f64 / self.recv as f64;
370        } else if self.sent > 0 {
371            self.value = f64::INFINITY;
372        } else {
373            self.value = 0.0;
374        }
375    }
376}
377
378/// Bitswap reprovide result
379#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
380pub struct ReprovideResult {
381    /// Number of CIDs reprovided
382    #[serde(rename = "Count")]
383    pub count: u64,
384    /// Duration of reprovide operation
385    #[serde(rename = "Duration")]
386    pub duration_ms: u64,
387}
388
389/// Bitswap protocol version
390pub const BITSWAP_PROTOCOL_VERSION: &str = "/ipfs/bitswap/1.2.0";
391
392/// Agent version string
393pub const AGENT_VERSION: &str = concat!("ferripfs/", env!("CARGO_PKG_VERSION"));
394
395/// Protocol version
396pub const PROTOCOL_VERSION: &str = "ipfs/0.1.0";
397
398// =============================================================================
399// IPNS Types (Phase 10)
400// Ported from: boxo/ipns
401// =============================================================================
402
403/// IPNS record validity type
404/// Ported from: boxo/ipns/pb/ipns.proto
405#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
406#[derive(Default)]
407pub enum IpnsValidityType {
408    /// End of life - record expires at a specific time
409    #[serde(rename = "EOL")]
410    #[default]
411    Eol = 0,
412}
413
414
415impl std::fmt::Display for IpnsValidityType {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        match self {
418            Self::Eol => write!(f, "EOL"),
419        }
420    }
421}
422
423/// IPNS record structure
424/// Ported from: boxo/ipns/record.go
425#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
426pub struct IpnsRecord {
427    /// The value being published (usually `/ipfs/<cid>`)
428    #[serde(rename = "Value")]
429    pub value: String,
430    /// Signature data (base64 encoded)
431    #[serde(rename = "SignatureV2")]
432    pub signature_v2: String,
433    /// Validity type (EOL)
434    #[serde(rename = "ValidityType")]
435    pub validity_type: IpnsValidityType,
436    /// Validity timestamp (RFC3339)
437    #[serde(rename = "Validity")]
438    pub validity: String,
439    /// Sequence number for versioning
440    #[serde(rename = "Sequence")]
441    pub sequence: u64,
442    /// Time-to-live in nanoseconds
443    #[serde(rename = "TTL")]
444    pub ttl: u64,
445    /// Public key (base64 encoded, optional if embedded in peer ID)
446    #[serde(rename = "PubKey", skip_serializing_if = "Option::is_none")]
447    pub pubkey: Option<String>,
448}
449
450impl IpnsRecord {
451    /// Create a new IPNS record
452    pub fn new(value: String, sequence: u64, validity: String, ttl: u64) -> Self {
453        Self {
454            value,
455            signature_v2: String::new(),
456            validity_type: IpnsValidityType::Eol,
457            validity,
458            sequence,
459            ttl,
460            pubkey: None,
461        }
462    }
463
464    /// Check if the record is expired based on validity timestamp
465    pub fn is_expired(&self) -> bool {
466        use std::time::SystemTime;
467
468        // Parse RFC3339 timestamp
469        if let Ok(validity_time) = chrono_parse_rfc3339(&self.validity) {
470            let now = SystemTime::now()
471                .duration_since(SystemTime::UNIX_EPOCH)
472                .unwrap_or_default()
473                .as_secs();
474            validity_time < now
475        } else {
476            true // Invalid timestamp = expired
477        }
478    }
479
480    /// Get the sequence number
481    pub fn get_sequence(&self) -> u64 {
482        self.sequence
483    }
484
485    /// Increment the sequence number
486    pub fn increment_sequence(&mut self) {
487        self.sequence += 1;
488    }
489}
490
491/// Simple RFC3339 timestamp parser (returns Unix timestamp)
492fn chrono_parse_rfc3339(s: &str) -> Result<u64, ()> {
493    // Basic RFC3339 parsing: 2024-01-15T12:00:00Z
494    // This is a simplified parser; production code would use chrono crate
495    if s.len() < 20 {
496        return Err(());
497    }
498
499    let parts: Vec<&str> = s.split('T').collect();
500    if parts.len() != 2 {
501        return Err(());
502    }
503
504    let date_parts: Vec<&str> = parts[0].split('-').collect();
505    if date_parts.len() != 3 {
506        return Err(());
507    }
508
509    let year: u64 = date_parts[0].parse().map_err(|_| ())?;
510    let month: u64 = date_parts[1].parse().map_err(|_| ())?;
511    let day: u64 = date_parts[2].parse().map_err(|_| ())?;
512
513    // Approximate days since Unix epoch
514    let days_since_epoch = (year - 1970) * 365 + (month - 1) * 30 + day;
515    Ok(days_since_epoch * 86400)
516}
517
518/// IPNS entry for display
519/// Ported from: core/commands/name/publish.go
520#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
521pub struct IpnsEntry {
522    /// The IPNS name (peer ID or key name)
523    #[serde(rename = "Name")]
524    pub name: String,
525    /// The value the name points to
526    #[serde(rename = "Value")]
527    pub value: String,
528}
529
530impl IpnsEntry {
531    /// Create a new IPNS entry
532    pub fn new(name: String, value: String) -> Self {
533        Self { name, value }
534    }
535}
536
537/// IPNS resolve result
538/// Ported from: core/commands/name/resolve.go
539#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
540pub struct IpnsResolveResult {
541    /// The resolved path
542    #[serde(rename = "Path")]
543    pub path: String,
544}
545
546impl IpnsResolveResult {
547    /// Create a new resolve result
548    pub fn new(path: String) -> Self {
549        Self { path }
550    }
551}
552
553/// IPNS record inspection result
554/// Ported from: core/commands/name/inspect.go
555#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
556pub struct IpnsInspectResult {
557    /// The record being inspected
558    #[serde(rename = "Record")]
559    pub record: IpnsRecord,
560    /// Validation result
561    #[serde(rename = "Validation")]
562    pub validation: IpnsValidation,
563}
564
565/// IPNS validation result
566#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
567pub struct IpnsValidation {
568    /// Whether the record is valid
569    #[serde(rename = "Valid")]
570    pub valid: bool,
571    /// Validation error message (if any)
572    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
573    pub error: Option<String>,
574    /// Whether the record is expired
575    #[serde(rename = "Expired")]
576    pub expired: bool,
577}
578
579impl IpnsValidation {
580    /// Create a valid validation result
581    pub fn valid(expired: bool) -> Self {
582        Self {
583            valid: true,
584            error: None,
585            expired,
586        }
587    }
588
589    /// Create an invalid validation result
590    pub fn invalid(error: String) -> Self {
591        Self {
592            valid: false,
593            error: Some(error),
594            expired: false,
595        }
596    }
597}
598
599// =============================================================================
600// Key Management Types (Phase 10)
601// Ported from: core/commands/keystore.go
602// =============================================================================
603
604/// Key type enumeration
605/// Ported from: core/commands/keystore.go
606#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
607#[derive(Default)]
608pub enum KeyType {
609    #[serde(rename = "ed25519")]
610    #[default]
611    Ed25519,
612    #[serde(rename = "rsa")]
613    Rsa,
614    #[serde(rename = "ecdsa")]
615    Ecdsa,
616    #[serde(rename = "secp256k1")]
617    Secp256k1,
618}
619
620
621impl std::fmt::Display for KeyType {
622    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623        match self {
624            Self::Ed25519 => write!(f, "ed25519"),
625            Self::Rsa => write!(f, "rsa"),
626            Self::Ecdsa => write!(f, "ecdsa"),
627            Self::Secp256k1 => write!(f, "secp256k1"),
628        }
629    }
630}
631
632impl std::str::FromStr for KeyType {
633    type Err = String;
634
635    fn from_str(s: &str) -> Result<Self, Self::Err> {
636        match s.to_lowercase().as_str() {
637            "ed25519" => Ok(Self::Ed25519),
638            "rsa" => Ok(Self::Rsa),
639            "ecdsa" => Ok(Self::Ecdsa),
640            "secp256k1" => Ok(Self::Secp256k1),
641            _ => Err(format!("unknown key type: {}", s)),
642        }
643    }
644}
645
646/// Key information
647/// Ported from: core/commands/keystore.go
648#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
649pub struct KeyInfo {
650    /// Key name
651    #[serde(rename = "Name")]
652    pub name: String,
653    /// Key ID (peer ID derived from public key)
654    #[serde(rename = "Id")]
655    pub id: String,
656}
657
658impl KeyInfo {
659    /// Create a new key info
660    pub fn new(name: String, id: String) -> Self {
661        Self { name, id }
662    }
663}
664
665/// Key list result
666/// Ported from: core/commands/keystore.go
667#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
668pub struct KeyList {
669    /// List of keys
670    #[serde(rename = "Keys")]
671    pub keys: Vec<KeyInfo>,
672}
673
674impl KeyList {
675    /// Create a new key list
676    pub fn new(keys: Vec<KeyInfo>) -> Self {
677        Self { keys }
678    }
679
680    /// Create an empty key list
681    pub fn empty() -> Self {
682        Self { keys: Vec::new() }
683    }
684
685    /// Add a key to the list
686    pub fn add(&mut self, key: KeyInfo) {
687        self.keys.push(key);
688    }
689
690    /// Get the number of keys
691    pub fn len(&self) -> usize {
692        self.keys.len()
693    }
694
695    /// Check if the list is empty
696    pub fn is_empty(&self) -> bool {
697        self.keys.is_empty()
698    }
699}
700
701/// Key generation result
702/// Ported from: core/commands/keystore.go
703#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
704pub struct KeyGenResult {
705    /// Key name
706    #[serde(rename = "Name")]
707    pub name: String,
708    /// Key ID (peer ID)
709    #[serde(rename = "Id")]
710    pub id: String,
711}
712
713impl KeyGenResult {
714    /// Create a new key generation result
715    pub fn new(name: String, id: String) -> Self {
716        Self { name, id }
717    }
718}
719
720/// Key rename result
721/// Ported from: core/commands/keystore.go
722#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
723pub struct KeyRenameResult {
724    /// Was the key renamed
725    #[serde(rename = "Was")]
726    pub was: String,
727    /// New name
728    #[serde(rename = "Now")]
729    pub now: String,
730    /// Key ID
731    #[serde(rename = "Id")]
732    pub id: String,
733    /// Whether the key was overwritten
734    #[serde(rename = "Overwrite")]
735    pub overwrite: bool,
736}
737
738impl KeyRenameResult {
739    /// Create a new key rename result
740    pub fn new(was: String, now: String, id: String, overwrite: bool) -> Self {
741        Self {
742            was,
743            now,
744            id,
745            overwrite,
746        }
747    }
748}
749
750/// Key removal result
751/// Ported from: core/commands/keystore.go
752#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
753pub struct KeyRmResult {
754    /// List of removed keys
755    #[serde(rename = "Keys")]
756    pub keys: Vec<KeyInfo>,
757}
758
759impl KeyRmResult {
760    /// Create a new key removal result
761    pub fn new(keys: Vec<KeyInfo>) -> Self {
762        Self { keys }
763    }
764
765    /// Create a single key removal result
766    pub fn single(name: String, id: String) -> Self {
767        Self {
768            keys: vec![KeyInfo::new(name, id)],
769        }
770    }
771}
772
773/// IPNS publish options
774/// Ported from: core/commands/name/publish.go
775#[derive(Debug, Clone)]
776pub struct IpnsPublishOptions {
777    /// Key to use for signing (default: "self")
778    pub key: String,
779    /// Resolve the given path before publishing
780    pub resolve: bool,
781    /// Time duration this record should be valid for (e.g., "24h")
782    pub lifetime: String,
783    /// Time-to-live for the record
784    pub ttl: Option<String>,
785    /// Quieter output
786    pub quieter: bool,
787    /// Allow offline operation
788    pub allow_offline: bool,
789}
790
791impl Default for IpnsPublishOptions {
792    fn default() -> Self {
793        Self {
794            key: "self".to_string(),
795            resolve: true,
796            lifetime: "24h".to_string(),
797            ttl: None,
798            quieter: false,
799            allow_offline: false,
800        }
801    }
802}
803
804/// IPNS resolve options
805/// Ported from: core/commands/name/resolve.go
806#[derive(Debug, Clone)]
807pub struct IpnsResolveOptions {
808    /// Resolve until the result is not an IPNS name
809    pub recursive: bool,
810    /// Do not use cached entries
811    pub nocache: bool,
812    /// Number of records to request for DHT resolution
813    pub dht_record_count: u32,
814    /// Timeout for DHT resolution
815    pub dht_timeout: Option<String>,
816    /// Stream resolution progress
817    pub stream: bool,
818}
819
820impl Default for IpnsResolveOptions {
821    fn default() -> Self {
822        Self {
823            recursive: true,
824            nocache: false,
825            dht_record_count: 16,
826            dht_timeout: None,
827            stream: false,
828        }
829    }
830}
831
832/// Default IPNS record TTL in nanoseconds (1 hour)
833pub const IPNS_DEFAULT_TTL: u64 = 3_600_000_000_000;
834
835/// Default IPNS record lifetime (24 hours)
836pub const IPNS_DEFAULT_LIFETIME: &str = "24h";
837
838/// IPNS protocol prefix
839pub const IPNS_PREFIX: &str = "/ipns/";
840
841/// IPFS protocol prefix
842pub const IPFS_PREFIX: &str = "/ipfs/";
843
844// =============================================================================
845// MFS Types (Phase 11)
846// Ported from: core/commands/files.go
847//
848// The Mutable File System (MFS) provides a Unix-like filesystem interface
849// on top of IPFS. It allows users to create, modify, and organize files
850// in a familiar hierarchical structure while maintaining IPFS's
851// content-addressed storage model.
852//
853// Key concepts:
854// - MFS maintains a root directory that can be modified over time
855// - All changes are tracked and can be published to IPNS
856// - Files and directories are stored as UnixFS objects
857// - The root CID changes when any content is modified
858// =============================================================================
859
860/// Type of a node in the Mutable File System.
861///
862/// MFS nodes can be either files or directories, mirroring the Unix
863/// filesystem model. This enum is used in stat results and internally
864/// to track node types.
865///
866/// # Kubo Equivalent
867///
868/// Corresponds to the type field in `ipfs files stat` output.
869/// See `core/commands/files.go:filesStatCmd`.
870///
871/// # Serialization
872///
873/// Serializes to lowercase strings: `"file"` or `"directory"`.
874///
875/// # Example
876///
877/// ```
878/// use ferripfs_network::MfsNodeType;
879///
880/// let file_type = MfsNodeType::File;
881/// let dir_type = MfsNodeType::Directory;
882///
883/// assert_eq!(format!("{}", file_type), "file");
884/// assert_eq!(format!("{}", dir_type), "directory");
885/// ```
886#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
887#[derive(Default)]
888pub enum MfsNodeType {
889    /// A regular file containing data.
890    #[serde(rename = "file")]
891    #[default]
892    File,
893    /// A directory containing other files and directories.
894    #[serde(rename = "directory")]
895    Directory,
896}
897
898
899impl std::fmt::Display for MfsNodeType {
900    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
901        match self {
902            Self::File => write!(f, "file"),
903            Self::Directory => write!(f, "directory"),
904        }
905    }
906}
907
908/// Result of an MFS stat operation.
909///
910/// Contains metadata about a file or directory in the Mutable File System,
911/// including its content hash, size, type information, and optional locality
912/// information indicating whether content is available locally.
913///
914/// # Kubo Equivalent
915///
916/// Corresponds to the output of `ipfs files stat` command.
917/// See `core/commands/files.go:filesStatCmd`.
918///
919/// # JSON Serialization
920///
921/// Field names are serialized in PascalCase to match Kubo's output format:
922/// - `Hash`: CID of the content
923/// - `Size`: Logical file size in bytes
924/// - `CumulativeSize`: Total size including block overhead
925/// - `Blocks`: Number of IPFS blocks
926/// - `Type`: `"file"` or `"directory"`
927///
928/// # Example
929///
930/// ```
931/// use ferripfs_network::{MfsStatResult, MfsNodeType};
932///
933/// let stat = MfsStatResult::file(
934///     "QmExample...".to_string(),
935///     1024,  // logical size
936///     1152,  // cumulative size with overhead
937///     2,     // number of blocks
938/// );
939///
940/// assert_eq!(stat.node_type, MfsNodeType::File);
941/// assert_eq!(stat.size, 1024);
942/// ```
943#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
944pub struct MfsStatResult {
945    /// CID hash of the file or directory root.
946    ///
947    /// For files, this is the CID of the UnixFS file node.
948    /// For directories, this is the CID of the directory node.
949    #[serde(rename = "Hash")]
950    pub hash: String,
951
952    /// Logical size of the content in bytes.
953    ///
954    /// For files, this is the actual file size.
955    /// For directories, this is typically 0.
956    #[serde(rename = "Size")]
957    pub size: u64,
958
959    /// Cumulative size including all blocks and metadata overhead.
960    ///
961    /// This includes the size of all blocks in the DAG, including
962    /// intermediate nodes for large files and any protobuf encoding overhead.
963    #[serde(rename = "CumulativeSize")]
964    pub cumulative_size: u64,
965
966    /// Number of blocks comprising this node.
967    ///
968    /// For small files this is 1. Larger files are split into multiple
969    /// blocks arranged in a DAG structure.
970    #[serde(rename = "Blocks")]
971    pub blocks: u64,
972
973    /// Type of the node: file or directory.
974    #[serde(rename = "Type")]
975    pub node_type: MfsNodeType,
976
977    /// Whether locality information was requested.
978    ///
979    /// When `true`, the `local` and `size_local` fields contain meaningful data.
980    /// Only present when `--with-local` flag is used.
981    #[serde(rename = "WithLocality", skip_serializing_if = "Option::is_none")]
982    pub with_locality: Option<bool>,
983
984    /// Whether all blocks for this content are available locally.
985    ///
986    /// Only meaningful when `with_locality` is `Some(true)`.
987    #[serde(rename = "Local", skip_serializing_if = "Option::is_none")]
988    pub local: Option<bool>,
989
990    /// Size of locally available data in bytes.
991    ///
992    /// Only meaningful when `with_locality` is `Some(true)`.
993    #[serde(rename = "SizeLocal", skip_serializing_if = "Option::is_none")]
994    pub size_local: Option<u64>,
995}
996
997impl MfsStatResult {
998    /// Create a new stat result for a file.
999    ///
1000    /// # Arguments
1001    ///
1002    /// * `hash` - The CID of the file's root block
1003    /// * `size` - The logical size of the file in bytes
1004    /// * `cumulative_size` - Total size including all blocks and metadata
1005    /// * `blocks` - Number of blocks comprising this file
1006    ///
1007    /// # Returns
1008    ///
1009    /// An `MfsStatResult` with `node_type` set to `MfsNodeType::File`
1010    /// and all locality fields set to `None`.
1011    pub fn file(hash: String, size: u64, cumulative_size: u64, blocks: u64) -> Self {
1012        Self {
1013            hash,
1014            size,
1015            cumulative_size,
1016            blocks,
1017            node_type: MfsNodeType::File,
1018            with_locality: None,
1019            local: None,
1020            size_local: None,
1021        }
1022    }
1023
1024    /// Create a new stat result for a directory.
1025    ///
1026    /// # Arguments
1027    ///
1028    /// * `hash` - The CID of the directory node
1029    /// * `size` - The logical size (typically 0 for directories)
1030    /// * `cumulative_size` - Total size including all content and metadata
1031    /// * `blocks` - Number of blocks in the directory node
1032    ///
1033    /// # Returns
1034    ///
1035    /// An `MfsStatResult` with `node_type` set to `MfsNodeType::Directory`
1036    /// and all locality fields set to `None`.
1037    pub fn directory(hash: String, size: u64, cumulative_size: u64, blocks: u64) -> Self {
1038        Self {
1039            hash,
1040            size,
1041            cumulative_size,
1042            blocks,
1043            node_type: MfsNodeType::Directory,
1044            with_locality: None,
1045            local: None,
1046            size_local: None,
1047        }
1048    }
1049}
1050
1051/// A single entry in an MFS directory listing.
1052///
1053/// Represents a file or directory within an MFS directory, containing
1054/// the name, type, size, and content hash.
1055///
1056/// # Kubo Equivalent
1057///
1058/// Corresponds to entries in the output of `ipfs files ls` command.
1059/// See `core/commands/files.go:filesLsCmd`.
1060///
1061/// # Type Field
1062///
1063/// The `entry_type` field uses integer values for compatibility with Kubo:
1064/// - `0` = file
1065/// - `1` = directory
1066///
1067/// Use the `is_file()` and `is_directory()` methods for type checking.
1068///
1069/// # Example
1070///
1071/// ```
1072/// use ferripfs_network::MfsLsEntry;
1073///
1074/// let file = MfsLsEntry::file("readme.txt".into(), 1024, "Qm...".into());
1075/// assert!(file.is_file());
1076/// assert_eq!(file.name, "readme.txt");
1077///
1078/// let dir = MfsLsEntry::directory("docs".into(), 0, "Qm...".into());
1079/// assert!(dir.is_directory());
1080/// ```
1081#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1082pub struct MfsLsEntry {
1083    /// Name of the file or directory.
1084    ///
1085    /// This is the basename, not a full path.
1086    #[serde(rename = "Name")]
1087    pub name: String,
1088
1089    /// Type indicator: 0 for file, 1 for directory.
1090    ///
1091    /// Use `is_file()` or `is_directory()` for type checking.
1092    #[serde(rename = "Type")]
1093    pub entry_type: i32,
1094
1095    /// Size of the entry in bytes.
1096    ///
1097    /// For files, this is the logical file size.
1098    /// For directories, this is typically 0.
1099    #[serde(rename = "Size")]
1100    pub size: u64,
1101
1102    /// CID hash of the entry's content.
1103    #[serde(rename = "Hash")]
1104    pub hash: String,
1105}
1106
1107impl MfsLsEntry {
1108    /// Create a directory listing entry for a file.
1109    ///
1110    /// # Arguments
1111    ///
1112    /// * `name` - The filename (basename only)
1113    /// * `size` - The logical file size in bytes
1114    /// * `hash` - The CID of the file's content
1115    pub fn file(name: String, size: u64, hash: String) -> Self {
1116        Self {
1117            name,
1118            entry_type: 0,
1119            size,
1120            hash,
1121        }
1122    }
1123
1124    /// Create a directory listing entry for a subdirectory.
1125    ///
1126    /// # Arguments
1127    ///
1128    /// * `name` - The directory name (basename only)
1129    /// * `size` - The size (typically 0 for directories)
1130    /// * `hash` - The CID of the directory node
1131    pub fn directory(name: String, size: u64, hash: String) -> Self {
1132        Self {
1133            name,
1134            entry_type: 1,
1135            size,
1136            hash,
1137        }
1138    }
1139
1140    /// Returns `true` if this entry is a file.
1141    pub fn is_file(&self) -> bool {
1142        self.entry_type == 0
1143    }
1144
1145    /// Returns `true` if this entry is a directory.
1146    pub fn is_directory(&self) -> bool {
1147        self.entry_type == 1
1148    }
1149}
1150
1151/// Result of an MFS directory listing operation.
1152///
1153/// Contains a list of entries (files and directories) within an MFS directory.
1154///
1155/// # Kubo Equivalent
1156///
1157/// Corresponds to the JSON output of `ipfs files ls` command.
1158/// See `core/commands/files.go:filesLsCmd`.
1159///
1160/// # Example
1161///
1162/// ```
1163/// use ferripfs_network::{MfsLsResult, MfsLsEntry};
1164///
1165/// let mut result = MfsLsResult::empty();
1166/// result.add(MfsLsEntry::file("readme.txt".into(), 1024, "Qm...".into()));
1167/// result.add(MfsLsEntry::directory("docs".into(), 0, "Qm...".into()));
1168///
1169/// assert_eq!(result.len(), 2);
1170/// assert!(!result.is_empty());
1171/// ```
1172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1173pub struct MfsLsResult {
1174    /// List of directory entries.
1175    ///
1176    /// May be empty for an empty directory.
1177    #[serde(rename = "Entries")]
1178    pub entries: Vec<MfsLsEntry>,
1179}
1180
1181impl MfsLsResult {
1182    /// Create a new directory listing result with the given entries.
1183    pub fn new(entries: Vec<MfsLsEntry>) -> Self {
1184        Self { entries }
1185    }
1186
1187    /// Create an empty directory listing result.
1188    ///
1189    /// Represents an empty directory.
1190    pub fn empty() -> Self {
1191        Self {
1192            entries: Vec::new(),
1193        }
1194    }
1195
1196    /// Add an entry to the directory listing.
1197    pub fn add(&mut self, entry: MfsLsEntry) {
1198        self.entries.push(entry);
1199    }
1200
1201    /// Returns the number of entries in the listing.
1202    pub fn len(&self) -> usize {
1203        self.entries.len()
1204    }
1205
1206    /// Returns `true` if the directory listing is empty.
1207    pub fn is_empty(&self) -> bool {
1208        self.entries.is_empty()
1209    }
1210}
1211
1212/// Result of an MFS flush operation.
1213///
1214/// Contains the CID of the flushed directory, which represents the
1215/// current state of the MFS tree after all pending changes have been
1216/// persisted to the blockstore.
1217///
1218/// # Kubo Equivalent
1219///
1220/// Corresponds to the output of `ipfs files flush` command.
1221/// See `core/commands/files.go:filesFlushCmd`.
1222///
1223/// # Example
1224///
1225/// ```
1226/// use ferripfs_network::MfsFlushResult;
1227///
1228/// let result = MfsFlushResult::new("QmRoot...".to_string());
1229/// println!("Flushed to CID: {}", result.cid);
1230/// ```
1231#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1232pub struct MfsFlushResult {
1233    /// CID of the flushed directory/file.
1234    ///
1235    /// This is the content hash after all pending changes have been
1236    /// written to the blockstore.
1237    #[serde(rename = "Cid")]
1238    pub cid: String,
1239}
1240
1241impl MfsFlushResult {
1242    /// Create a new flush result with the given CID.
1243    pub fn new(cid: String) -> Self {
1244        Self { cid }
1245    }
1246}
1247
1248/// Result of an MFS file read operation (internal).
1249///
1250/// Contains the data read from a file along with metadata about
1251/// the read operation.
1252///
1253/// # Note
1254///
1255/// This type is for internal use. The `files read` command writes
1256/// data directly to stdout rather than returning this struct.
1257///
1258/// # Kubo Reference
1259///
1260/// See `core/commands/files.go:filesReadCmd`.
1261#[derive(Debug, Clone)]
1262pub struct MfsReadResult {
1263    /// Raw data read from the file.
1264    pub data: Vec<u8>,
1265
1266    /// Byte offset where the read started.
1267    pub offset: u64,
1268
1269    /// Number of bytes read.
1270    pub length: u64,
1271}
1272
1273impl MfsReadResult {
1274    /// Create a new read result.
1275    ///
1276    /// The `length` field is automatically calculated from the data length.
1277    pub fn new(data: Vec<u8>, offset: u64) -> Self {
1278        let length = data.len() as u64;
1279        Self {
1280            data,
1281            offset,
1282            length,
1283        }
1284    }
1285}
1286
1287/// Options for an MFS file write operation.
1288///
1289/// Controls how data is written to a file in MFS, including whether
1290/// to create the file, where to write, and how to encode the content.
1291///
1292/// # Kubo Equivalent
1293///
1294/// Corresponds to the flags of `ipfs files write` command.
1295/// See `core/commands/files.go:filesWriteCmd`.
1296///
1297/// # Example
1298///
1299/// ```
1300/// use ferripfs_network::MfsWriteOptions;
1301///
1302/// // Create a file if it doesn't exist
1303/// let mut opts = MfsWriteOptions::default();
1304/// opts.create = true;
1305/// opts.parents = true;
1306///
1307/// assert!(opts.create);
1308/// assert!(opts.parents);
1309/// ```
1310#[derive(Debug, Clone)]
1311pub struct MfsWriteOptions {
1312    /// Create the file if it doesn't exist.
1313    ///
1314    /// Without this, writing to a non-existent file will fail.
1315    pub create: bool,
1316
1317    /// Create parent directories as needed.
1318    ///
1319    /// Similar to `mkdir -p` behavior.
1320    pub parents: bool,
1321
1322    /// Truncate the file before writing.
1323    ///
1324    /// When true, the file is cleared before new data is written.
1325    pub truncate: bool,
1326
1327    /// Maximum number of bytes to write from input.
1328    ///
1329    /// If `None`, all input data is written.
1330    pub count: Option<u64>,
1331
1332    /// Byte offset within the file to start writing at.
1333    ///
1334    /// Allows appending or overwriting at a specific position.
1335    pub offset: u64,
1336
1337    /// Use raw leaves in the UnixFS DAG.
1338    ///
1339    /// When true, leaf nodes contain raw data without UnixFS wrapping.
1340    pub raw_leaves: bool,
1341
1342    /// CID version to use for new blocks (0 or 1).
1343    pub cid_version: u8,
1344
1345    /// Hash algorithm for content addressing.
1346    ///
1347    /// Default is "sha2-256".
1348    pub hash: String,
1349}
1350
1351impl Default for MfsWriteOptions {
1352    fn default() -> Self {
1353        Self {
1354            create: false,
1355            parents: false,
1356            truncate: false,
1357            count: None,
1358            offset: 0,
1359            raw_leaves: false,
1360            cid_version: 0,
1361            hash: "sha2-256".to_string(),
1362        }
1363    }
1364}
1365
1366impl MfsWriteOptions {
1367    /// Create options for creating a new file
1368    pub fn create_new() -> Self {
1369        Self {
1370            create: true,
1371            truncate: true,
1372            ..Self::default()
1373        }
1374    }
1375
1376    /// Create options for appending to a file
1377    pub fn append() -> Self {
1378        Self {
1379            create: true,
1380            offset: u64::MAX, // Special value meaning "end of file"
1381            ..Self::default()
1382        }
1383    }
1384}
1385
1386/// Options for an MFS copy operation.
1387///
1388/// Controls how files are copied within MFS or from IPFS to MFS.
1389///
1390/// # Kubo Equivalent
1391///
1392/// Corresponds to the flags of `ipfs files cp` command.
1393/// See `core/commands/files.go:filesCpCmd`.
1394#[derive(Debug, Clone, Default)]
1395pub struct MfsCpOptions {
1396    /// Create parent directories in the destination path as needed.
1397    ///
1398    /// Similar to `mkdir -p` behavior before copying.
1399    pub parents: bool,
1400}
1401
1402/// Options for an MFS mkdir operation.
1403///
1404/// Controls how directories are created in MFS.
1405///
1406/// # Kubo Equivalent
1407///
1408/// Corresponds to the flags of `ipfs files mkdir` command.
1409/// See `core/commands/files.go:filesMkdirCmd`.
1410#[derive(Debug, Clone)]
1411pub struct MfsMkdirOptions {
1412    /// Create parent directories as needed.
1413    ///
1414    /// Similar to `mkdir -p` behavior.
1415    pub parents: bool,
1416
1417    /// CID version to use for the new directory (0 or 1).
1418    pub cid_version: u8,
1419
1420    /// Hash algorithm for content addressing.
1421    ///
1422    /// Default is "sha2-256".
1423    pub hash: String,
1424}
1425
1426impl Default for MfsMkdirOptions {
1427    fn default() -> Self {
1428        Self {
1429            parents: false,
1430            cid_version: 0,
1431            hash: "sha2-256".to_string(),
1432        }
1433    }
1434}
1435
1436/// Options for an MFS remove operation.
1437///
1438/// Controls how files and directories are removed from MFS.
1439///
1440/// # Kubo Equivalent
1441///
1442/// Corresponds to the flags of `ipfs files rm` command.
1443/// See `core/commands/files.go:filesRmCmd`.
1444#[derive(Debug, Clone, Default)]
1445pub struct MfsRmOptions {
1446    /// Recursively remove directories and their contents.
1447    ///
1448    /// Required to remove non-empty directories.
1449    pub recursive: bool,
1450
1451    /// Force removal, ignoring errors if the path doesn't exist.
1452    pub force: bool,
1453}
1454
1455/// Options for an MFS move/rename operation.
1456///
1457/// Controls how files and directories are moved within MFS.
1458///
1459/// # Kubo Equivalent
1460///
1461/// Corresponds to the flags of `ipfs files mv` command.
1462/// See `core/commands/files.go:filesMvCmd`.
1463#[derive(Debug, Clone, Default)]
1464pub struct MfsMvOptions {
1465    /// Flush changes to the blockstore after moving.
1466    pub flush: bool,
1467}
1468
1469/// Options for changing CID settings in MFS.
1470///
1471/// Controls the CID version and hash algorithm used for content
1472/// at a specific path in MFS.
1473///
1474/// # Kubo Equivalent
1475///
1476/// Corresponds to the flags of `ipfs files chcid` command.
1477/// See `core/commands/files.go:filesChcidCmd`.
1478#[derive(Debug, Clone)]
1479#[derive(Default)]
1480pub struct MfsChcidOptions {
1481    /// New CID version to use (0 or 1).
1482    ///
1483    /// If `None`, the existing version is kept.
1484    pub cid_version: Option<u8>,
1485
1486    /// New hash algorithm to use.
1487    ///
1488    /// If `None`, the existing algorithm is kept.
1489    pub hash: Option<String>,
1490}
1491
1492
1493/// Generic result for MFS operations.
1494///
1495/// Provides a simple success/failure indication with an optional
1496/// error message for failed operations.
1497///
1498/// # Example
1499///
1500/// ```
1501/// use ferripfs_network::MfsOpResult;
1502///
1503/// let success = MfsOpResult::success();
1504/// assert!(success.success);
1505///
1506/// let failure = MfsOpResult::failure("file not found".to_string());
1507/// assert!(!failure.success);
1508/// assert_eq!(failure.error, Some("file not found".to_string()));
1509/// ```
1510#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1511pub struct MfsOpResult {
1512    /// Whether the operation completed successfully.
1513    #[serde(rename = "Success")]
1514    pub success: bool,
1515
1516    /// Error message if the operation failed.
1517    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
1518    pub error: Option<String>,
1519}
1520
1521impl MfsOpResult {
1522    /// Create a successful operation result.
1523    pub fn success() -> Self {
1524        Self {
1525            success: true,
1526            error: None,
1527        }
1528    }
1529
1530    /// Create a failed operation result with an error message.
1531    pub fn failure(error: String) -> Self {
1532        Self {
1533            success: false,
1534            error: Some(error),
1535        }
1536    }
1537}
1538
1539/// The root path for MFS operations.
1540///
1541/// All MFS paths are absolute and start with this root.
1542pub const MFS_ROOT: &str = "/";
1543
1544/// The path separator character used in MFS paths.
1545pub const MFS_SEPARATOR: char = '/';
1546
1547// =============================================================================
1548// PubSub Types (Phase 12)
1549// Ported from: boxo/pubsub, core/commands/pubsub.go
1550// =============================================================================
1551
1552/// A message received from or published to a pubsub topic.
1553///
1554/// # Kubo Equivalent
1555///
1556/// Corresponds to the message format used in `ipfs pubsub sub` output.
1557/// See `core/commands/pubsub.go:pubsubSubCmd`.
1558///
1559/// # Example
1560///
1561/// ```
1562/// use ferripfs_network::PubsubMessage;
1563///
1564/// // Create a simple message with just sender, topic, and data
1565/// let msg = PubsubMessage::simple(
1566///     "QmPeerID".to_string(),
1567///     "my-topic".to_string(),
1568///     b"Hello, world!".to_vec(),
1569/// );
1570/// assert_eq!(msg.topic, "my-topic");
1571///
1572/// // Or create a message with all fields
1573/// let msg_full = PubsubMessage::new(
1574///     "QmPeerID".to_string(),
1575///     b"Hello!".to_vec(),
1576///     vec![1, 2, 3, 4],  // seqno
1577///     vec!["my-topic".to_string()],  // topic_ids
1578///     "my-topic".to_string(),
1579/// );
1580/// assert_eq!(msg_full.seqno, vec![1, 2, 3, 4]);
1581/// ```
1582#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1583pub struct PubsubMessage {
1584    /// Peer ID of the message sender.
1585    #[serde(rename = "from")]
1586    pub from: String,
1587
1588    /// Data payload (base64 encoded in JSON).
1589    #[serde(rename = "data", with = "base64_serde")]
1590    pub data: Vec<u8>,
1591
1592    /// Sequence number for ordering.
1593    #[serde(rename = "seqno", with = "base64_serde")]
1594    pub seqno: Vec<u8>,
1595
1596    /// Topic IDs the message was received on.
1597    #[serde(rename = "topicIDs")]
1598    pub topic_ids: Vec<String>,
1599
1600    /// Primary topic (convenience field).
1601    #[serde(skip)]
1602    pub topic: String,
1603}
1604
1605impl PubsubMessage {
1606    /// Create a new pubsub message with all fields.
1607    ///
1608    /// # Arguments
1609    ///
1610    /// * `from` - Peer ID of the sender
1611    /// * `data` - Message payload
1612    /// * `seqno` - Sequence number bytes
1613    /// * `topic_ids` - List of topic IDs
1614    /// * `topic` - Primary topic
1615    pub fn new(
1616        from: String,
1617        data: Vec<u8>,
1618        seqno: Vec<u8>,
1619        topic_ids: Vec<String>,
1620        topic: String,
1621    ) -> Self {
1622        Self {
1623            from,
1624            data,
1625            seqno,
1626            topic_ids,
1627            topic,
1628        }
1629    }
1630
1631    /// Create a simple message for a single topic.
1632    ///
1633    /// This is a convenience method that sets seqno to empty
1634    /// and derives topic_ids from the single topic.
1635    pub fn simple(from: String, topic: String, data: Vec<u8>) -> Self {
1636        Self {
1637            from,
1638            data,
1639            seqno: Vec::new(),
1640            topic_ids: vec![topic.clone()],
1641            topic,
1642        }
1643    }
1644
1645    /// Create a message with a sequence number.
1646    pub fn with_seqno(from: String, topic: String, data: Vec<u8>, seqno: Vec<u8>) -> Self {
1647        Self {
1648            from,
1649            data,
1650            seqno,
1651            topic_ids: vec![topic.clone()],
1652            topic,
1653        }
1654    }
1655
1656    /// Get the message data as a UTF-8 string (if valid).
1657    pub fn data_string(&self) -> Option<String> {
1658        String::from_utf8(self.data.clone()).ok()
1659    }
1660}
1661
1662/// Helper module for base64 serialization of byte vectors.
1663mod base64_serde {
1664    use base64::{engine::general_purpose::STANDARD, Engine};
1665    use serde::{Deserialize, Deserializer, Serializer};
1666
1667    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1668    where
1669        S: Serializer,
1670    {
1671        serializer.serialize_str(&STANDARD.encode(bytes))
1672    }
1673
1674    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1675    where
1676        D: Deserializer<'de>,
1677    {
1678        let s = String::deserialize(deserializer)?;
1679        STANDARD.decode(&s).map_err(serde::de::Error::custom)
1680    }
1681}
1682
1683/// Information about a peer subscribed to a pubsub topic.
1684///
1685/// # Kubo Equivalent
1686///
1687/// Corresponds to the output of `ipfs pubsub peers`.
1688/// See `core/commands/pubsub.go:pubsubPeersCmd`.
1689#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1690pub struct PubsubPeer {
1691    /// Peer ID of the subscriber.
1692    #[serde(rename = "ID")]
1693    pub id: String,
1694}
1695
1696impl PubsubPeer {
1697    /// Create a new pubsub peer.
1698    pub fn new(id: String) -> Self {
1699        Self { id }
1700    }
1701}
1702
1703/// Result of listing pubsub topics.
1704///
1705/// # Kubo Equivalent
1706///
1707/// Corresponds to the output of `ipfs pubsub ls`.
1708/// See `core/commands/pubsub.go:pubsubLsCmd`.
1709///
1710/// # Example
1711///
1712/// ```
1713/// use ferripfs_network::PubsubLsResult;
1714///
1715/// let mut result = PubsubLsResult::empty();
1716/// result.add("topic1".to_string());
1717/// result.add("topic2".to_string());
1718/// assert_eq!(result.len(), 2);
1719/// ```
1720#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1721pub struct PubsubLsResult {
1722    /// List of topic strings.
1723    #[serde(rename = "Strings")]
1724    pub strings: Vec<String>,
1725}
1726
1727impl PubsubLsResult {
1728    /// Create a new topic list.
1729    pub fn new(strings: Vec<String>) -> Self {
1730        Self { strings }
1731    }
1732
1733    /// Create an empty topic list.
1734    pub fn empty() -> Self {
1735        Self {
1736            strings: Vec::new(),
1737        }
1738    }
1739
1740    /// Add a topic to the list.
1741    pub fn add(&mut self, topic: String) {
1742        self.strings.push(topic);
1743    }
1744
1745    /// Get the number of topics.
1746    pub fn len(&self) -> usize {
1747        self.strings.len()
1748    }
1749
1750    /// Check if the list is empty.
1751    pub fn is_empty(&self) -> bool {
1752        self.strings.is_empty()
1753    }
1754}
1755
1756/// Result of listing peers for a pubsub topic.
1757///
1758/// # Kubo Equivalent
1759///
1760/// Corresponds to the output of `ipfs pubsub peers <topic>`.
1761/// See `core/commands/pubsub.go:pubsubPeersCmd`.
1762///
1763/// # Example
1764///
1765/// ```
1766/// use ferripfs_network::{PubsubPeersResult, PubsubPeer};
1767///
1768/// let mut result = PubsubPeersResult::empty();
1769/// result.add(PubsubPeer::new("QmPeer1".to_string()));
1770/// assert_eq!(result.len(), 1);
1771/// ```
1772#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1773pub struct PubsubPeersResult {
1774    /// List of peer strings (peer IDs).
1775    #[serde(rename = "Strings")]
1776    pub strings: Vec<String>,
1777}
1778
1779impl PubsubPeersResult {
1780    /// Create a new peers result.
1781    pub fn new(peers: Vec<String>) -> Self {
1782        Self { strings: peers }
1783    }
1784
1785    /// Create an empty peers result.
1786    pub fn empty() -> Self {
1787        Self {
1788            strings: Vec::new(),
1789        }
1790    }
1791
1792    /// Add a peer to the result.
1793    pub fn add(&mut self, peer: PubsubPeer) {
1794        self.strings.push(peer.id);
1795    }
1796
1797    /// Add a peer ID string directly.
1798    pub fn add_id(&mut self, id: String) {
1799        self.strings.push(id);
1800    }
1801
1802    /// Get the number of peers.
1803    pub fn len(&self) -> usize {
1804        self.strings.len()
1805    }
1806
1807    /// Check if empty.
1808    pub fn is_empty(&self) -> bool {
1809        self.strings.is_empty()
1810    }
1811}
1812
1813/// Result of publishing a message to a pubsub topic.
1814///
1815/// # Kubo Equivalent
1816///
1817/// Corresponds to the output of `ipfs pubsub pub`.
1818/// See `core/commands/pubsub.go:pubsubPubCmd`.
1819#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1820pub struct PubsubPublishResult {
1821    /// Whether the publish was successful.
1822    #[serde(rename = "Success")]
1823    pub success: bool,
1824
1825    /// Error message if publish failed.
1826    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
1827    pub error: Option<String>,
1828}
1829
1830impl PubsubPublishResult {
1831    /// Create a successful publish result.
1832    pub fn success() -> Self {
1833        Self {
1834            success: true,
1835            error: None,
1836        }
1837    }
1838
1839    /// Create a failed publish result.
1840    pub fn failure(error: String) -> Self {
1841        Self {
1842            success: false,
1843            error: Some(error),
1844        }
1845    }
1846}
1847
1848/// Subscription state for pubsub.
1849///
1850/// Tracks an active subscription to a topic.
1851#[derive(Debug, Clone)]
1852pub struct PubsubSubscription {
1853    /// Topic being subscribed to.
1854    pub topic: String,
1855
1856    /// Whether the subscription is active.
1857    pub active: bool,
1858}
1859
1860impl PubsubSubscription {
1861    /// Create a new subscription.
1862    pub fn new(topic: String) -> Self {
1863        Self {
1864            topic,
1865            active: true,
1866        }
1867    }
1868
1869    /// Cancel the subscription.
1870    pub fn cancel(&mut self) {
1871        self.active = false;
1872    }
1873}
1874
1875/// PubSub router type enumeration.
1876///
1877/// # Kubo Equivalent
1878///
1879/// Corresponds to the router types in Kubo's pubsub configuration.
1880/// GossipSub is the default and recommended router.
1881#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1882#[serde(rename_all = "lowercase")]
1883pub enum PubsubRouter {
1884    /// GossipSub router (default, recommended).
1885    #[default]
1886    #[serde(rename = "gossipsub")]
1887    Gossipsub,
1888
1889    /// FloodSub router (simple flooding).
1890    #[serde(rename = "floodsub")]
1891    Floodsub,
1892}
1893
1894impl std::fmt::Display for PubsubRouter {
1895    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1896        match self {
1897            Self::Gossipsub => write!(f, "gossipsub"),
1898            Self::Floodsub => write!(f, "floodsub"),
1899        }
1900    }
1901}
1902
1903impl std::str::FromStr for PubsubRouter {
1904    type Err = String;
1905
1906    fn from_str(s: &str) -> Result<Self, Self::Err> {
1907        match s.to_lowercase().as_str() {
1908            "gossipsub" => Ok(Self::Gossipsub),
1909            "floodsub" => Ok(Self::Floodsub),
1910            _ => Err(format!("unknown pubsub router: {}", s)),
1911        }
1912    }
1913}
1914
1915/// PubSub statistics.
1916///
1917/// # Kubo Equivalent
1918///
1919/// Statistics about pubsub operation, similar to what would be
1920/// returned by a hypothetical `ipfs stats pubsub` command.
1921#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1922pub struct PubsubStats {
1923    /// Number of topics subscribed to.
1924    #[serde(rename = "NumTopics")]
1925    pub num_topics: u64,
1926
1927    /// Number of peers across all topics.
1928    #[serde(rename = "NumPeers")]
1929    pub num_peers: u64,
1930
1931    /// Number of messages received.
1932    #[serde(rename = "MessagesReceived")]
1933    pub messages_received: u64,
1934
1935    /// Number of messages sent.
1936    #[serde(rename = "MessagesSent")]
1937    pub messages_sent: u64,
1938
1939    /// Total bytes received.
1940    #[serde(rename = "BytesReceived")]
1941    pub bytes_received: u64,
1942
1943    /// Total bytes sent.
1944    #[serde(rename = "BytesSent")]
1945    pub bytes_sent: u64,
1946}
1947
1948impl PubsubStats {
1949    /// Create new stats with all values specified.
1950    ///
1951    /// # Arguments
1952    ///
1953    /// * `num_topics` - Number of subscribed topics
1954    /// * `num_peers` - Number of connected peers
1955    /// * `messages_received` - Count of received messages
1956    /// * `messages_sent` - Count of sent messages
1957    /// * `bytes_received` - Total bytes received
1958    /// * `bytes_sent` - Total bytes sent
1959    pub fn new(
1960        num_topics: u64,
1961        num_peers: u64,
1962        messages_received: u64,
1963        messages_sent: u64,
1964        bytes_received: u64,
1965        bytes_sent: u64,
1966    ) -> Self {
1967        Self {
1968            num_topics,
1969            num_peers,
1970            messages_received,
1971            messages_sent,
1972            bytes_received,
1973            bytes_sent,
1974        }
1975    }
1976
1977    /// Create new empty stats.
1978    pub fn empty() -> Self {
1979        Self::default()
1980    }
1981
1982    /// Record a received message.
1983    pub fn record_received(&mut self, bytes: u64) {
1984        self.messages_received += 1;
1985        self.bytes_received += bytes;
1986    }
1987
1988    /// Record a sent message.
1989    pub fn record_sent(&mut self, bytes: u64) {
1990        self.messages_sent += 1;
1991        self.bytes_sent += bytes;
1992    }
1993}
1994
1995/// Protocol version for pubsub.
1996pub const PUBSUB_PROTOCOL_VERSION: &str = "/meshsub/1.1.0";
1997
1998/// Default GossipSub protocol ID.
1999pub const GOSSIPSUB_PROTOCOL_ID: &str = "/meshsub/1.1.0";
2000
2001/// FloodSub protocol ID.
2002pub const FLOODSUB_PROTOCOL_ID: &str = "/floodsub/1.0.0";
2003
2004// =============================================================================
2005// HTTP Gateway Types (Phase 13)
2006// =============================================================================
2007
2008/// Default gateway port.
2009pub const DEFAULT_GATEWAY_PORT: u16 = 8080;
2010
2011/// Default gateway address.
2012pub const DEFAULT_GATEWAY_ADDRESS: &str = "127.0.0.1:8080";
2013
2014/// IPFS path prefix.
2015pub const IPFS_PATH_PREFIX: &str = "/ipfs/";
2016
2017/// IPNS path prefix.
2018pub const IPNS_PATH_PREFIX: &str = "/ipns/";
2019
2020/// Parsed gateway path representing an IPFS or IPNS request.
2021///
2022/// # Kubo Equivalent
2023///
2024/// Corresponds to path parsing in `boxo/gateway/handler.go`.
2025///
2026/// # Example
2027///
2028/// ```
2029/// use ferripfs_network::GatewayPath;
2030///
2031/// let path = GatewayPath::parse("/ipfs/QmTest123/file.txt").unwrap();
2032/// assert!(path.is_ipfs());
2033/// assert_eq!(path.root(), "QmTest123");
2034/// assert_eq!(path.remainder(), Some("file.txt".to_string()));
2035/// ```
2036#[derive(Debug, Clone, PartialEq, Eq)]
2037pub struct GatewayPath {
2038    /// Path type (ipfs or ipns)
2039    pub path_type: GatewayPathType,
2040    /// Root identifier (CID for IPFS, name for IPNS)
2041    pub root: String,
2042    /// Remaining path after root (e.g., "/file.txt")
2043    pub remainder: Option<String>,
2044}
2045
2046impl GatewayPath {
2047    /// Parse a gateway path string.
2048    ///
2049    /// # Arguments
2050    ///
2051    /// * `path` - Path string like "/ipfs/Qm.../file.txt"
2052    ///
2053    /// # Returns
2054    ///
2055    /// Parsed path or None if invalid
2056    pub fn parse(path: &str) -> Option<Self> {
2057        let path = path.trim_start_matches('/');
2058
2059        if let Some(rest) = path.strip_prefix("ipfs/") {
2060            Self::parse_with_type(rest, GatewayPathType::Ipfs)
2061        } else if let Some(rest) = path.strip_prefix("ipns/") {
2062            Self::parse_with_type(rest, GatewayPathType::Ipns)
2063        } else {
2064            None
2065        }
2066    }
2067
2068    fn parse_with_type(rest: &str, path_type: GatewayPathType) -> Option<Self> {
2069        if rest.is_empty() {
2070            return None;
2071        }
2072
2073        let parts: Vec<&str> = rest.splitn(2, '/').collect();
2074        let root = parts[0].to_string();
2075
2076        if root.is_empty() {
2077            return None;
2078        }
2079
2080        let remainder = if parts.len() > 1 && !parts[1].is_empty() {
2081            Some(parts[1].to_string())
2082        } else {
2083            None
2084        };
2085
2086        Some(Self {
2087            path_type,
2088            root,
2089            remainder,
2090        })
2091    }
2092
2093    /// Check if this is an IPFS path.
2094    pub fn is_ipfs(&self) -> bool {
2095        self.path_type == GatewayPathType::Ipfs
2096    }
2097
2098    /// Check if this is an IPNS path.
2099    pub fn is_ipns(&self) -> bool {
2100        self.path_type == GatewayPathType::Ipns
2101    }
2102
2103    /// Get the root identifier.
2104    pub fn root(&self) -> &str {
2105        &self.root
2106    }
2107
2108    /// Get the remainder path.
2109    pub fn remainder(&self) -> Option<String> {
2110        self.remainder.clone()
2111    }
2112
2113    /// Get the full path string.
2114    pub fn to_path_string(&self) -> String {
2115        let prefix = match self.path_type {
2116            GatewayPathType::Ipfs => IPFS_PATH_PREFIX,
2117            GatewayPathType::Ipns => IPNS_PATH_PREFIX,
2118        };
2119        match &self.remainder {
2120            Some(r) => format!("{}{}/{}", prefix, self.root, r),
2121            None => format!("{}{}", prefix, self.root),
2122        }
2123    }
2124}
2125
2126/// Type of gateway path (IPFS or IPNS).
2127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2128pub enum GatewayPathType {
2129    /// Content-addressed path (/ipfs/...)
2130    Ipfs,
2131    /// Name-addressed path (/ipns/...)
2132    Ipns,
2133}
2134
2135impl std::fmt::Display for GatewayPathType {
2136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2137        match self {
2138            GatewayPathType::Ipfs => write!(f, "ipfs"),
2139            GatewayPathType::Ipns => write!(f, "ipns"),
2140        }
2141    }
2142}
2143
2144/// HTTP method for gateway requests.
2145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2146pub enum GatewayMethod {
2147    /// GET request
2148    Get,
2149    /// HEAD request
2150    Head,
2151    /// OPTIONS request (CORS preflight)
2152    Options,
2153}
2154
2155impl std::fmt::Display for GatewayMethod {
2156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2157        match self {
2158            GatewayMethod::Get => write!(f, "GET"),
2159            GatewayMethod::Head => write!(f, "HEAD"),
2160            GatewayMethod::Options => write!(f, "OPTIONS"),
2161        }
2162    }
2163}
2164
2165/// Content type for gateway responses.
2166///
2167/// # Kubo Equivalent
2168///
2169/// Corresponds to content type handling in `boxo/gateway/handler.go`.
2170#[derive(Debug, Clone, PartialEq, Eq)]
2171pub enum GatewayContentType {
2172    /// Raw bytes (application/octet-stream)
2173    Raw,
2174    /// JSON (application/json)
2175    Json,
2176    /// CBOR (application/cbor)
2177    Cbor,
2178    /// CAR archive (application/vnd.ipld.car)
2179    Car,
2180    /// HTML (text/html)
2181    Html,
2182    /// Plain text (text/plain)
2183    Text,
2184    /// Directory listing (text/html)
2185    Directory,
2186    /// Custom MIME type
2187    Custom(String),
2188}
2189
2190impl GatewayContentType {
2191    /// Get the MIME type string.
2192    pub fn mime_type(&self) -> &str {
2193        match self {
2194            GatewayContentType::Raw => "application/octet-stream",
2195            GatewayContentType::Json => "application/json",
2196            GatewayContentType::Cbor => "application/cbor",
2197            GatewayContentType::Car => "application/vnd.ipld.car",
2198            GatewayContentType::Html => "text/html; charset=utf-8",
2199            GatewayContentType::Text => "text/plain; charset=utf-8",
2200            GatewayContentType::Directory => "text/html; charset=utf-8",
2201            GatewayContentType::Custom(s) => s,
2202        }
2203    }
2204
2205    /// Parse from Accept header value.
2206    pub fn from_accept(accept: &str) -> Self {
2207        let accept_lower = accept.to_lowercase();
2208        if accept_lower.contains("application/vnd.ipld.car") {
2209            GatewayContentType::Car
2210        } else if accept_lower.contains("application/vnd.ipld.raw") {
2211            GatewayContentType::Raw
2212        } else if accept_lower.contains("application/json") {
2213            GatewayContentType::Json
2214        } else if accept_lower.contains("application/cbor") {
2215            GatewayContentType::Cbor
2216        } else if accept_lower.contains("text/html") {
2217            GatewayContentType::Html
2218        } else {
2219            GatewayContentType::Raw
2220        }
2221    }
2222}
2223
2224impl std::fmt::Display for GatewayContentType {
2225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2226        write!(f, "{}", self.mime_type())
2227    }
2228}
2229
2230/// Gateway request information.
2231///
2232/// # Kubo Equivalent
2233///
2234/// Corresponds to request handling in `boxo/gateway/handler.go`.
2235#[derive(Debug, Clone)]
2236pub struct GatewayRequest {
2237    /// HTTP method
2238    pub method: GatewayMethod,
2239    /// Parsed path
2240    pub path: GatewayPath,
2241    /// Accept header for content negotiation
2242    pub accept: Option<String>,
2243    /// Range header for partial content
2244    pub range: Option<String>,
2245    /// Host header (for subdomain gateway)
2246    pub host: Option<String>,
2247    /// Query parameters
2248    pub query: std::collections::HashMap<String, String>,
2249}
2250
2251impl GatewayRequest {
2252    /// Create a new gateway request.
2253    pub fn new(method: GatewayMethod, path: GatewayPath) -> Self {
2254        Self {
2255            method,
2256            path,
2257            accept: None,
2258            range: None,
2259            host: None,
2260            query: std::collections::HashMap::new(),
2261        }
2262    }
2263
2264    /// Set the accept header.
2265    pub fn with_accept(mut self, accept: String) -> Self {
2266        self.accept = Some(accept);
2267        self
2268    }
2269
2270    /// Set the range header.
2271    pub fn with_range(mut self, range: String) -> Self {
2272        self.range = Some(range);
2273        self
2274    }
2275
2276    /// Set the host header.
2277    pub fn with_host(mut self, host: String) -> Self {
2278        self.host = Some(host);
2279        self
2280    }
2281
2282    /// Get the desired content type based on Accept header.
2283    pub fn content_type(&self) -> GatewayContentType {
2284        match &self.accept {
2285            Some(accept) => GatewayContentType::from_accept(accept),
2286            None => GatewayContentType::Raw,
2287        }
2288    }
2289
2290    /// Check if this is a subdomain gateway request.
2291    pub fn is_subdomain_request(&self) -> bool {
2292        if let Some(host) = &self.host {
2293            // Subdomain format: <cid>.ipfs.<domain> or <name>.ipns.<domain>
2294            host.contains(".ipfs.") || host.contains(".ipns.")
2295        } else {
2296            false
2297        }
2298    }
2299}
2300
2301/// Gateway response data.
2302///
2303/// # Kubo Equivalent
2304///
2305/// Corresponds to response handling in `boxo/gateway/handler.go`.
2306#[derive(Debug, Clone)]
2307pub struct GatewayResponse {
2308    /// HTTP status code
2309    pub status: u16,
2310    /// Content type
2311    pub content_type: GatewayContentType,
2312    /// Response body
2313    pub body: Vec<u8>,
2314    /// Response headers
2315    pub headers: std::collections::HashMap<String, String>,
2316    /// Content length (if known)
2317    pub content_length: Option<u64>,
2318}
2319
2320impl GatewayResponse {
2321    /// Create a successful response.
2322    pub fn ok(content_type: GatewayContentType, body: Vec<u8>) -> Self {
2323        let content_length = Some(body.len() as u64);
2324        Self {
2325            status: 200,
2326            content_type,
2327            body,
2328            headers: std::collections::HashMap::new(),
2329            content_length,
2330        }
2331    }
2332
2333    /// Create a not found response.
2334    pub fn not_found(message: &str) -> Self {
2335        Self {
2336            status: 404,
2337            content_type: GatewayContentType::Text,
2338            body: message.as_bytes().to_vec(),
2339            headers: std::collections::HashMap::new(),
2340            content_length: Some(message.len() as u64),
2341        }
2342    }
2343
2344    /// Create a bad request response.
2345    pub fn bad_request(message: &str) -> Self {
2346        Self {
2347            status: 400,
2348            content_type: GatewayContentType::Text,
2349            body: message.as_bytes().to_vec(),
2350            headers: std::collections::HashMap::new(),
2351            content_length: Some(message.len() as u64),
2352        }
2353    }
2354
2355    /// Create an internal error response.
2356    pub fn internal_error(message: &str) -> Self {
2357        Self {
2358            status: 500,
2359            content_type: GatewayContentType::Text,
2360            body: message.as_bytes().to_vec(),
2361            headers: std::collections::HashMap::new(),
2362            content_length: Some(message.len() as u64),
2363        }
2364    }
2365
2366    /// Create a redirect response.
2367    pub fn redirect(location: &str) -> Self {
2368        let mut headers = std::collections::HashMap::new();
2369        headers.insert("Location".to_string(), location.to_string());
2370        Self {
2371            status: 301,
2372            content_type: GatewayContentType::Text,
2373            body: Vec::new(),
2374            headers,
2375            content_length: Some(0),
2376        }
2377    }
2378
2379    /// Add a header to the response.
2380    pub fn with_header(mut self, key: &str, value: &str) -> Self {
2381        self.headers.insert(key.to_string(), value.to_string());
2382        self
2383    }
2384
2385    /// Add CORS headers to the response.
2386    pub fn with_cors(self) -> Self {
2387        self.with_header("Access-Control-Allow-Origin", "*")
2388            .with_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
2389            .with_header(
2390                "Access-Control-Allow-Headers",
2391                "Content-Type, Range, X-Requested-With",
2392            )
2393            .with_header(
2394                "Access-Control-Expose-Headers",
2395                "Content-Range, Content-Length, X-Ipfs-Path, X-Ipfs-Roots",
2396            )
2397    }
2398
2399    /// Add cache control headers.
2400    pub fn with_cache_control(self, immutable: bool) -> Self {
2401        if immutable {
2402            // IPFS content is immutable, cache forever
2403            self.with_header("Cache-Control", "public, max-age=31536000, immutable")
2404        } else {
2405            // IPNS content may change
2406            self.with_header("Cache-Control", "public, max-age=60")
2407        }
2408    }
2409
2410    /// Add IPFS-specific headers.
2411    pub fn with_ipfs_headers(self, path: &GatewayPath) -> Self {
2412        self.with_header("X-Ipfs-Path", &path.to_path_string())
2413    }
2414}
2415
2416/// Gateway statistics.
2417///
2418/// # Kubo Equivalent
2419///
2420/// Corresponds to gateway metrics.
2421#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
2422pub struct GatewayStats {
2423    /// Total requests served
2424    #[serde(rename = "TotalRequests")]
2425    pub total_requests: u64,
2426
2427    /// Successful requests (2xx)
2428    #[serde(rename = "SuccessfulRequests")]
2429    pub successful_requests: u64,
2430
2431    /// Failed requests (4xx, 5xx)
2432    #[serde(rename = "FailedRequests")]
2433    pub failed_requests: u64,
2434
2435    /// Total bytes sent
2436    #[serde(rename = "BytesSent")]
2437    pub bytes_sent: u64,
2438
2439    /// IPFS path requests
2440    #[serde(rename = "IpfsRequests")]
2441    pub ipfs_requests: u64,
2442
2443    /// IPNS path requests
2444    #[serde(rename = "IpnsRequests")]
2445    pub ipns_requests: u64,
2446
2447    /// Subdomain requests
2448    #[serde(rename = "SubdomainRequests")]
2449    pub subdomain_requests: u64,
2450}
2451
2452impl GatewayStats {
2453    /// Create new empty stats.
2454    pub fn new() -> Self {
2455        Self::default()
2456    }
2457
2458    /// Record a request.
2459    pub fn record_request(&mut self, path: &GatewayPath, subdomain: bool) {
2460        self.total_requests += 1;
2461        if path.is_ipfs() {
2462            self.ipfs_requests += 1;
2463        } else {
2464            self.ipns_requests += 1;
2465        }
2466        if subdomain {
2467            self.subdomain_requests += 1;
2468        }
2469    }
2470
2471    /// Record a successful response.
2472    pub fn record_success(&mut self, bytes: u64) {
2473        self.successful_requests += 1;
2474        self.bytes_sent += bytes;
2475    }
2476
2477    /// Record a failed response.
2478    pub fn record_failure(&mut self) {
2479        self.failed_requests += 1;
2480    }
2481}
2482
2483/// Directory entry for gateway directory listing.
2484///
2485/// # Kubo Equivalent
2486///
2487/// Corresponds to directory listing in `boxo/gateway/handler_unixfs_dir.go`.
2488#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2489pub struct GatewayDirEntry {
2490    /// Entry name
2491    #[serde(rename = "Name")]
2492    pub name: String,
2493
2494    /// Entry type (file or directory)
2495    #[serde(rename = "Type")]
2496    pub entry_type: GatewayEntryType,
2497
2498    /// Size in bytes (for files)
2499    #[serde(rename = "Size")]
2500    pub size: u64,
2501
2502    /// CID of the entry
2503    #[serde(rename = "Cid")]
2504    pub cid: String,
2505}
2506
2507impl GatewayDirEntry {
2508    /// Create a file entry.
2509    pub fn file(name: String, size: u64, cid: String) -> Self {
2510        Self {
2511            name,
2512            entry_type: GatewayEntryType::File,
2513            size,
2514            cid,
2515        }
2516    }
2517
2518    /// Create a directory entry.
2519    pub fn directory(name: String, cid: String) -> Self {
2520        Self {
2521            name,
2522            entry_type: GatewayEntryType::Directory,
2523            size: 0,
2524            cid,
2525        }
2526    }
2527}
2528
2529/// Entry type for directory listings.
2530#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2531#[serde(rename_all = "lowercase")]
2532pub enum GatewayEntryType {
2533    /// Regular file
2534    File,
2535    /// Directory
2536    Directory,
2537}
2538
2539impl std::fmt::Display for GatewayEntryType {
2540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2541        match self {
2542            GatewayEntryType::File => write!(f, "file"),
2543            GatewayEntryType::Directory => write!(f, "directory"),
2544        }
2545    }
2546}
2547
2548/// Directory listing result for gateway.
2549///
2550/// # Kubo Equivalent
2551///
2552/// Corresponds to directory listing output in `boxo/gateway/handler_unixfs_dir.go`.
2553#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2554pub struct GatewayDirListing {
2555    /// Directory path
2556    #[serde(rename = "Path")]
2557    pub path: String,
2558
2559    /// Directory CID
2560    #[serde(rename = "Cid")]
2561    pub cid: String,
2562
2563    /// Entries in the directory
2564    #[serde(rename = "Entries")]
2565    pub entries: Vec<GatewayDirEntry>,
2566}
2567
2568impl GatewayDirListing {
2569    /// Create a new directory listing.
2570    pub fn new(path: String, cid: String, entries: Vec<GatewayDirEntry>) -> Self {
2571        Self { path, cid, entries }
2572    }
2573
2574    /// Render as HTML.
2575    pub fn to_html(&self) -> String {
2576        let mut html = String::new();
2577        html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
2578        html.push_str(&format!("<title>Index of {}</title>\n", self.path));
2579        html.push_str("<style>body{font-family:monospace;} table{border-collapse:collapse;} td,th{padding:4px 8px;text-align:left;}</style>\n");
2580        html.push_str("</head>\n<body>\n");
2581        html.push_str(&format!("<h1>Index of {}</h1>\n", self.path));
2582        html.push_str("<table>\n<tr><th>Name</th><th>Size</th><th>CID</th></tr>\n");
2583
2584        // Parent directory link if not root
2585        if self.path != "/" && !self.path.is_empty() {
2586            html.push_str("<tr><td><a href=\"..\">..</a></td><td>-</td><td>-</td></tr>\n");
2587        }
2588
2589        for entry in &self.entries {
2590            let link = if entry.entry_type == GatewayEntryType::Directory {
2591                format!("{}/", entry.name)
2592            } else {
2593                entry.name.clone()
2594            };
2595            let size_str = if entry.entry_type == GatewayEntryType::Directory {
2596                "-".to_string()
2597            } else {
2598                format_bytes(entry.size)
2599            };
2600            html.push_str(&format!(
2601                "<tr><td><a href=\"{}\">{}</a></td><td>{}</td><td>{}</td></tr>\n",
2602                link,
2603                entry.name,
2604                size_str,
2605                &entry.cid[..12.min(entry.cid.len())]
2606            ));
2607        }
2608
2609        html.push_str("</table>\n</body>\n</html>");
2610        html
2611    }
2612
2613    /// Render as JSON.
2614    pub fn to_json(&self) -> String {
2615        serde_json::to_string_pretty(self).unwrap_or_default()
2616    }
2617}
2618
2619/// Format bytes as human-readable string (for directory listings).
2620fn format_bytes(bytes: u64) -> String {
2621    const KB: u64 = 1024;
2622    const MB: u64 = KB * 1024;
2623    const GB: u64 = MB * 1024;
2624
2625    if bytes >= GB {
2626        format!("{:.1}G", bytes as f64 / GB as f64)
2627    } else if bytes >= MB {
2628        format!("{:.1}M", bytes as f64 / MB as f64)
2629    } else if bytes >= KB {
2630        format!("{:.1}K", bytes as f64 / KB as f64)
2631    } else {
2632        format!("{}B", bytes)
2633    }
2634}
2635
2636// ============================================================================
2637// Phase 14: HTTP RPC API Types
2638// ============================================================================
2639
2640/// Default RPC API port (matches Kubo).
2641pub const DEFAULT_API_PORT: u16 = 5001;
2642
2643/// Default RPC API address.
2644pub const DEFAULT_API_ADDRESS: &str = "127.0.0.1:5001";
2645
2646/// API version string.
2647pub const API_VERSION: &str = "v0";
2648
2649/// RPC API request.
2650///
2651/// # Kubo Equivalent
2652///
2653/// Corresponds to HTTP requests in `core/corehttp/corehttp.go`.
2654#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2655pub struct ApiRequest {
2656    /// The API endpoint being called (e.g., "/api/v0/id")
2657    #[serde(rename = "Endpoint")]
2658    pub endpoint: String,
2659
2660    /// HTTP method (usually POST for Kubo RPC)
2661    #[serde(rename = "Method")]
2662    pub method: String,
2663
2664    /// Request arguments
2665    #[serde(rename = "Arguments")]
2666    pub arguments: std::collections::HashMap<String, String>,
2667
2668    /// Request body (for file uploads, etc.)
2669    #[serde(rename = "Body", skip_serializing_if = "Option::is_none")]
2670    pub body: Option<Vec<u8>>,
2671}
2672
2673impl ApiRequest {
2674    /// Create a new API request.
2675    pub fn new(endpoint: &str) -> Self {
2676        Self {
2677            endpoint: endpoint.to_string(),
2678            method: "POST".to_string(),
2679            arguments: std::collections::HashMap::new(),
2680            body: None,
2681        }
2682    }
2683
2684    /// Add an argument to the request.
2685    pub fn with_arg(mut self, key: &str, value: &str) -> Self {
2686        self.arguments.insert(key.to_string(), value.to_string());
2687        self
2688    }
2689
2690    /// Set the request body.
2691    pub fn with_body(mut self, body: Vec<u8>) -> Self {
2692        self.body = Some(body);
2693        self
2694    }
2695
2696    /// Build the full URL for this request.
2697    pub fn build_url(&self, base_url: &str) -> String {
2698        let mut url = format!("{}/api/{}{}", base_url, API_VERSION, self.endpoint);
2699        if !self.arguments.is_empty() {
2700            url.push('?');
2701            let params: Vec<String> = self
2702                .arguments
2703                .iter()
2704                .map(|(k, v)| format!("{}={}", k, urlencoding(v)))
2705                .collect();
2706            url.push_str(&params.join("&"));
2707        }
2708        url
2709    }
2710}
2711
2712/// Simple URL encoding for API parameters.
2713fn urlencoding(s: &str) -> String {
2714    s.replace('%', "%25")
2715        .replace(' ', "%20")
2716        .replace('&', "%26")
2717        .replace('=', "%3D")
2718        .replace('?', "%3F")
2719        .replace('#', "%23")
2720}
2721
2722/// RPC API response.
2723///
2724/// # Kubo Equivalent
2725///
2726/// Corresponds to HTTP responses from Kubo's RPC API.
2727#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2728pub struct ApiResponse {
2729    /// HTTP status code
2730    #[serde(rename = "StatusCode")]
2731    pub status_code: u16,
2732
2733    /// Response headers
2734    #[serde(rename = "Headers")]
2735    pub headers: std::collections::HashMap<String, String>,
2736
2737    /// Response body (JSON-encoded for most endpoints)
2738    #[serde(rename = "Body")]
2739    pub body: Vec<u8>,
2740
2741    /// Error message if the request failed
2742    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
2743    pub error: Option<ApiError>,
2744}
2745
2746impl ApiResponse {
2747    /// Create a successful response.
2748    pub fn ok(body: Vec<u8>) -> Self {
2749        Self {
2750            status_code: 200,
2751            headers: std::collections::HashMap::new(),
2752            body,
2753            error: None,
2754        }
2755    }
2756
2757    /// Create an error response.
2758    pub fn error(status_code: u16, message: &str, code: i32) -> Self {
2759        Self {
2760            status_code,
2761            headers: std::collections::HashMap::new(),
2762            body: Vec::new(),
2763            error: Some(ApiError {
2764                message: message.to_string(),
2765                code,
2766                error_type: "error".to_string(),
2767            }),
2768        }
2769    }
2770
2771    /// Create a not found response.
2772    pub fn not_found(message: &str) -> Self {
2773        Self::error(404, message, 0)
2774    }
2775
2776    /// Create a bad request response.
2777    pub fn bad_request(message: &str) -> Self {
2778        Self::error(400, message, 0)
2779    }
2780
2781    /// Create an internal error response.
2782    pub fn internal_error(message: &str) -> Self {
2783        Self::error(500, message, 0)
2784    }
2785
2786    /// Check if the response is successful.
2787    pub fn is_success(&self) -> bool {
2788        self.status_code >= 200 && self.status_code < 300
2789    }
2790
2791    /// Get the body as a string.
2792    pub fn body_string(&self) -> String {
2793        String::from_utf8_lossy(&self.body).to_string()
2794    }
2795
2796    /// Parse the body as JSON.
2797    pub fn body_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
2798        serde_json::from_slice(&self.body)
2799    }
2800
2801    /// Add a header to the response.
2802    pub fn with_header(mut self, key: &str, value: &str) -> Self {
2803        self.headers.insert(key.to_string(), value.to_string());
2804        self
2805    }
2806
2807    /// Add content-type header.
2808    pub fn with_content_type(self, content_type: &str) -> Self {
2809        self.with_header("Content-Type", content_type)
2810    }
2811
2812    /// Add CORS headers.
2813    pub fn with_cors(self) -> Self {
2814        self.with_header("Access-Control-Allow-Origin", "*")
2815            .with_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
2816            .with_header(
2817                "Access-Control-Allow-Headers",
2818                "Content-Type, Authorization",
2819            )
2820    }
2821}
2822
2823/// API error structure.
2824///
2825/// # Kubo Equivalent
2826///
2827/// Corresponds to error responses from Kubo's RPC API.
2828#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2829pub struct ApiError {
2830    /// Error message
2831    #[serde(rename = "Message")]
2832    pub message: String,
2833
2834    /// Error code
2835    #[serde(rename = "Code")]
2836    pub code: i32,
2837
2838    /// Error type
2839    #[serde(rename = "Type")]
2840    pub error_type: String,
2841}
2842
2843impl std::fmt::Display for ApiError {
2844    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2845        write!(f, "{}", self.message)
2846    }
2847}
2848
2849impl std::error::Error for ApiError {}
2850
2851/// API endpoint information.
2852///
2853/// Describes an available API endpoint.
2854#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2855pub struct ApiEndpoint {
2856    /// Endpoint path (e.g., "/id")
2857    #[serde(rename = "Path")]
2858    pub path: String,
2859
2860    /// Endpoint description
2861    #[serde(rename = "Description")]
2862    pub description: String,
2863
2864    /// HTTP methods supported
2865    #[serde(rename = "Methods")]
2866    pub methods: Vec<String>,
2867
2868    /// Required arguments
2869    #[serde(rename = "Arguments")]
2870    pub arguments: Vec<ApiArgument>,
2871
2872    /// Whether the endpoint requires authentication
2873    #[serde(rename = "RequiresAuth")]
2874    pub requires_auth: bool,
2875}
2876
2877impl ApiEndpoint {
2878    /// Create a new endpoint definition.
2879    pub fn new(path: &str, description: &str) -> Self {
2880        Self {
2881            path: path.to_string(),
2882            description: description.to_string(),
2883            methods: vec!["POST".to_string()],
2884            arguments: Vec::new(),
2885            requires_auth: false,
2886        }
2887    }
2888
2889    /// Add an argument to this endpoint.
2890    pub fn with_arg(mut self, arg: ApiArgument) -> Self {
2891        self.arguments.push(arg);
2892        self
2893    }
2894
2895    /// Mark as requiring authentication.
2896    pub fn with_auth(mut self) -> Self {
2897        self.requires_auth = true;
2898        self
2899    }
2900}
2901
2902/// API argument definition.
2903#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2904pub struct ApiArgument {
2905    /// Argument name
2906    #[serde(rename = "Name")]
2907    pub name: String,
2908
2909    /// Argument description
2910    #[serde(rename = "Description")]
2911    pub description: String,
2912
2913    /// Argument type (string, bool, int, etc.)
2914    #[serde(rename = "Type")]
2915    pub arg_type: String,
2916
2917    /// Whether the argument is required
2918    #[serde(rename = "Required")]
2919    pub required: bool,
2920
2921    /// Default value if not provided
2922    #[serde(rename = "Default", skip_serializing_if = "Option::is_none")]
2923    pub default: Option<String>,
2924}
2925
2926impl ApiArgument {
2927    /// Create a required argument.
2928    pub fn required(name: &str, arg_type: &str, description: &str) -> Self {
2929        Self {
2930            name: name.to_string(),
2931            description: description.to_string(),
2932            arg_type: arg_type.to_string(),
2933            required: true,
2934            default: None,
2935        }
2936    }
2937
2938    /// Create an optional argument.
2939    pub fn optional(name: &str, arg_type: &str, description: &str) -> Self {
2940        Self {
2941            name: name.to_string(),
2942            description: description.to_string(),
2943            arg_type: arg_type.to_string(),
2944            required: false,
2945            default: None,
2946        }
2947    }
2948
2949    /// Set default value.
2950    pub fn with_default(mut self, default: &str) -> Self {
2951        self.default = Some(default.to_string());
2952        self
2953    }
2954}
2955
2956/// API server configuration.
2957///
2958/// # Kubo Equivalent
2959///
2960/// Corresponds to `core/corehttp/corehttp.go` server configuration.
2961#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2962pub struct ApiServerConfig {
2963    /// Listen address
2964    #[serde(rename = "Address")]
2965    pub address: String,
2966
2967    /// Enable CORS
2968    #[serde(rename = "EnableCORS")]
2969    pub enable_cors: bool,
2970
2971    /// Allowed origins for CORS
2972    #[serde(rename = "AllowedOrigins")]
2973    pub allowed_origins: Vec<String>,
2974
2975    /// API timeout in seconds
2976    #[serde(rename = "Timeout")]
2977    pub timeout: u64,
2978
2979    /// Maximum request body size
2980    #[serde(rename = "MaxBodySize")]
2981    pub max_body_size: u64,
2982
2983    /// Enable authentication
2984    #[serde(rename = "EnableAuth")]
2985    pub enable_auth: bool,
2986}
2987
2988impl Default for ApiServerConfig {
2989    fn default() -> Self {
2990        Self {
2991            address: DEFAULT_API_ADDRESS.to_string(),
2992            enable_cors: true,
2993            allowed_origins: vec!["*".to_string()],
2994            timeout: 60,
2995            max_body_size: 50 * 1024 * 1024, // 50MB default
2996            enable_auth: false,
2997        }
2998    }
2999}
3000
3001impl ApiServerConfig {
3002    /// Create a new server config with the given address.
3003    pub fn new(address: &str) -> Self {
3004        Self {
3005            address: address.to_string(),
3006            ..Default::default()
3007        }
3008    }
3009}
3010
3011/// API server statistics.
3012///
3013/// # Kubo Equivalent
3014///
3015/// Corresponds to API server metrics.
3016#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
3017pub struct ApiStats {
3018    /// Total requests received
3019    #[serde(rename = "TotalRequests")]
3020    pub total_requests: u64,
3021
3022    /// Successful requests
3023    #[serde(rename = "SuccessfulRequests")]
3024    pub successful_requests: u64,
3025
3026    /// Failed requests
3027    #[serde(rename = "FailedRequests")]
3028    pub failed_requests: u64,
3029
3030    /// Total bytes received
3031    #[serde(rename = "BytesReceived")]
3032    pub bytes_received: u64,
3033
3034    /// Total bytes sent
3035    #[serde(rename = "BytesSent")]
3036    pub bytes_sent: u64,
3037
3038    /// Currently active connections
3039    #[serde(rename = "ActiveConnections")]
3040    pub active_connections: u32,
3041
3042    /// Average response time in milliseconds
3043    #[serde(rename = "AvgResponseTimeMs")]
3044    pub avg_response_time_ms: f64,
3045}
3046
3047impl ApiStats {
3048    /// Create new empty stats.
3049    pub fn new() -> Self {
3050        Self::default()
3051    }
3052
3053    /// Record a request.
3054    pub fn record_request(&mut self, bytes_in: u64) {
3055        self.total_requests += 1;
3056        self.bytes_received += bytes_in;
3057    }
3058
3059    /// Record a successful response.
3060    pub fn record_success(&mut self, bytes_out: u64, response_time_ms: f64) {
3061        self.successful_requests += 1;
3062        self.bytes_sent += bytes_out;
3063        // Update rolling average
3064        let total = self.successful_requests as f64;
3065        self.avg_response_time_ms =
3066            (self.avg_response_time_ms * (total - 1.0) + response_time_ms) / total;
3067    }
3068
3069    /// Record a failed request.
3070    pub fn record_failure(&mut self) {
3071        self.failed_requests += 1;
3072    }
3073}
3074
3075/// API endpoint listing result.
3076///
3077/// # Kubo Equivalent
3078///
3079/// Corresponds to `/api/v0/commands` output.
3080#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3081pub struct ApiCommandsResult {
3082    /// List of available commands/endpoints
3083    #[serde(rename = "Commands")]
3084    pub commands: Vec<ApiCommandInfo>,
3085}
3086
3087/// Information about a single API command.
3088#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3089pub struct ApiCommandInfo {
3090    /// Command name
3091    #[serde(rename = "Name")]
3092    pub name: String,
3093
3094    /// Full path to command
3095    #[serde(rename = "Path")]
3096    pub path: String,
3097
3098    /// Subcommands
3099    #[serde(rename = "Subcommands", skip_serializing_if = "Vec::is_empty")]
3100    pub subcommands: Vec<ApiCommandInfo>,
3101
3102    /// Options
3103    #[serde(rename = "Options", skip_serializing_if = "Vec::is_empty")]
3104    pub options: Vec<ApiOptionInfo>,
3105}
3106
3107impl ApiCommandInfo {
3108    /// Create a new command info.
3109    pub fn new(name: &str, path: &str) -> Self {
3110        Self {
3111            name: name.to_string(),
3112            path: path.to_string(),
3113            subcommands: Vec::new(),
3114            options: Vec::new(),
3115        }
3116    }
3117
3118    /// Add a subcommand.
3119    pub fn with_subcommand(mut self, subcmd: ApiCommandInfo) -> Self {
3120        self.subcommands.push(subcmd);
3121        self
3122    }
3123
3124    /// Add an option.
3125    pub fn with_option(mut self, opt: ApiOptionInfo) -> Self {
3126        self.options.push(opt);
3127        self
3128    }
3129}
3130
3131/// Information about a command option.
3132#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3133pub struct ApiOptionInfo {
3134    /// Option name
3135    #[serde(rename = "Name")]
3136    pub name: String,
3137
3138    /// Option description
3139    #[serde(rename = "Description")]
3140    pub description: String,
3141}
3142
3143impl ApiOptionInfo {
3144    /// Create a new option info.
3145    pub fn new(name: &str, description: &str) -> Self {
3146        Self {
3147            name: name.to_string(),
3148            description: description.to_string(),
3149        }
3150    }
3151}
3152
3153/// API client for connecting to a running IPFS daemon.
3154///
3155/// # Kubo Equivalent
3156///
3157/// Corresponds to HTTP client functionality in `client/rpc/api.go`.
3158#[derive(Debug, Clone)]
3159pub struct ApiClient {
3160    /// Base URL of the API server
3161    pub base_url: String,
3162
3163    /// Authentication token (if any)
3164    pub auth_token: Option<String>,
3165
3166    /// Timeout in seconds
3167    pub timeout: u64,
3168}
3169
3170impl ApiClient {
3171    /// Create a new API client.
3172    pub fn new(base_url: &str) -> Self {
3173        Self {
3174            base_url: base_url.trim_end_matches('/').to_string(),
3175            auth_token: None,
3176            timeout: 60,
3177        }
3178    }
3179
3180    /// Create a client from the API file in a repo.
3181    pub fn from_repo(repo_path: &std::path::Path) -> Option<Self> {
3182        let api_file = repo_path.join("api");
3183        if api_file.exists() {
3184            let content = std::fs::read_to_string(&api_file).ok()?;
3185            let addr = content.trim();
3186            // Convert multiaddr to HTTP URL if needed
3187            let url = if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
3188                multiaddr_to_url(addr)
3189            } else {
3190                addr.to_string()
3191            };
3192            Some(Self::new(&url))
3193        } else {
3194            None
3195        }
3196    }
3197
3198    /// Set authentication token.
3199    pub fn with_auth(mut self, token: &str) -> Self {
3200        self.auth_token = Some(token.to_string());
3201        self
3202    }
3203
3204    /// Set timeout.
3205    pub fn with_timeout(mut self, timeout: u64) -> Self {
3206        self.timeout = timeout;
3207        self
3208    }
3209
3210    /// Build URL for an endpoint.
3211    pub fn url(&self, endpoint: &str) -> String {
3212        format!("{}/api/{}{}", self.base_url, API_VERSION, endpoint)
3213    }
3214
3215    /// Check if the daemon is reachable.
3216    pub fn is_online(&self) -> bool {
3217        // In a full implementation, this would make an HTTP request
3218        // For now, we just check if the base_url is valid
3219        !self.base_url.is_empty()
3220    }
3221}
3222
3223/// Convert a multiaddr to an HTTP URL.
3224fn multiaddr_to_url(addr: &str) -> String {
3225    // Parse multiaddr like /ip4/127.0.0.1/tcp/5001
3226    let parts: Vec<&str> = addr.split('/').filter(|s| !s.is_empty()).collect();
3227
3228    let mut host = "127.0.0.1";
3229    let mut port = "5001";
3230
3231    let mut i = 0;
3232    while i < parts.len() {
3233        match parts[i] {
3234            "ip4" | "ip6" => {
3235                if i + 1 < parts.len() {
3236                    host = parts[i + 1];
3237                    i += 2;
3238                } else {
3239                    i += 1;
3240                }
3241            }
3242            "tcp" => {
3243                if i + 1 < parts.len() {
3244                    port = parts[i + 1];
3245                    i += 2;
3246                } else {
3247                    i += 1;
3248                }
3249            }
3250            _ => i += 1,
3251        }
3252    }
3253
3254    format!("http://{}:{}", host, port)
3255}
3256
3257/// Result of API version query.
3258///
3259/// # Kubo Equivalent
3260///
3261/// Corresponds to `/api/v0/version` output.
3262#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3263pub struct ApiVersionResult {
3264    /// Version string
3265    #[serde(rename = "Version")]
3266    pub version: String,
3267
3268    /// Commit hash
3269    #[serde(rename = "Commit")]
3270    pub commit: String,
3271
3272    /// Repository version
3273    #[serde(rename = "Repo")]
3274    pub repo: String,
3275
3276    /// System information
3277    #[serde(rename = "System")]
3278    pub system: String,
3279
3280    /// Golang version (or Rust version for ferripfs)
3281    #[serde(rename = "Golang")]
3282    pub golang: String,
3283}
3284
3285impl ApiVersionResult {
3286    /// Create version info for ferripfs.
3287    pub fn ferripfs() -> Self {
3288        Self {
3289            version: "0.1.0".to_string(),
3290            commit: "unknown".to_string(),
3291            repo: "18".to_string(),
3292            system: format!("{}/{}", std::env::consts::ARCH, std::env::consts::OS),
3293            golang: "rust".to_string(),
3294        }
3295    }
3296}
3297
3298#[cfg(test)]
3299mod tests {
3300    use super::*;
3301
3302    #[test]
3303    fn test_agent_version() {
3304        assert!(AGENT_VERSION.starts_with("ferripfs/"));
3305    }
3306
3307    #[test]
3308    fn test_api_request_building() {
3309        let req = ApiRequest::new("/id").with_arg("format", "json");
3310
3311        let url = req.build_url("http://localhost:5001");
3312        assert!(url.contains("/api/v0/id"));
3313        assert!(url.contains("format=json"));
3314    }
3315
3316    #[test]
3317    fn test_api_response() {
3318        let resp = ApiResponse::ok(b"test".to_vec());
3319        assert!(resp.is_success());
3320        assert_eq!(resp.body_string(), "test");
3321
3322        let err = ApiResponse::error(500, "internal error", 1);
3323        assert!(!err.is_success());
3324        assert!(err.error.is_some());
3325    }
3326
3327    #[test]
3328    fn test_multiaddr_to_url() {
3329        assert_eq!(
3330            multiaddr_to_url("/ip4/127.0.0.1/tcp/5001"),
3331            "http://127.0.0.1:5001"
3332        );
3333        assert_eq!(
3334            multiaddr_to_url("/ip4/0.0.0.0/tcp/8080"),
3335            "http://0.0.0.0:8080"
3336        );
3337    }
3338
3339    #[test]
3340    fn test_api_stats() {
3341        let mut stats = ApiStats::new();
3342        stats.record_request(100);
3343        stats.record_success(200, 50.0);
3344        stats.record_failure();
3345
3346        assert_eq!(stats.total_requests, 1);
3347        assert_eq!(stats.successful_requests, 1);
3348        assert_eq!(stats.failed_requests, 1);
3349        assert_eq!(stats.bytes_received, 100);
3350        assert_eq!(stats.bytes_sent, 200);
3351    }
3352}