1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde::Deserialize;
6
7use crate::{Error, Result};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
11pub enum DaemonEndpoint {
12 Unix(PathBuf),
13 Tcp { host: String, port: u16 },
14}
15
16#[derive(Debug, Deserialize)]
17struct EndpointDescriptor {
18 v: u32,
19 transport: String,
20 #[serde(default)]
21 path: String,
22 #[serde(default)]
23 host: String,
24 #[serde(default)]
25 port: u16,
26}
27
28pub fn discover_endpoint(cccc_home: Option<&Path>) -> Result<DaemonEndpoint> {
30 let home = match cccc_home {
31 Some(path) => path.to_path_buf(),
32 None => default_cccc_home()?,
33 };
34 let descriptor_path = home.join("daemon").join("ccccd.addr.json");
35
36 if let Ok(contents) = fs::read_to_string(&descriptor_path) {
37 let descriptor: EndpointDescriptor = serde_json::from_str(&contents)
38 .map_err(|error| Error::InvalidEndpoint(error.to_string()))?;
39 return endpoint_from_descriptor(descriptor);
40 }
41
42 #[cfg(unix)]
43 {
44 let socket = home.join("daemon").join("ccccd.sock");
45 if socket.exists() {
46 return Ok(DaemonEndpoint::Unix(socket));
47 }
48 }
49
50 Err(Error::EndpointNotFound(
51 descriptor_path.display().to_string(),
52 ))
53}
54
55fn default_cccc_home() -> Result<PathBuf> {
56 if let Some(path) = env::var_os("CCCC_HOME").filter(|value| !value.is_empty()) {
57 return Ok(PathBuf::from(path));
58 }
59 let user_home = env::var_os("HOME")
60 .or_else(|| env::var_os("USERPROFILE"))
61 .ok_or_else(|| Error::EndpointNotFound("HOME/USERPROFILE is not set".into()))?;
62 Ok(PathBuf::from(user_home).join(".cccc"))
63}
64
65fn endpoint_from_descriptor(descriptor: EndpointDescriptor) -> Result<DaemonEndpoint> {
66 if descriptor.v != 1 {
67 return Err(Error::InvalidEndpoint(format!(
68 "descriptor version must be 1, got {}",
69 descriptor.v
70 )));
71 }
72 match descriptor.transport.as_str() {
73 "unix" if !descriptor.path.is_empty() => {
74 Ok(DaemonEndpoint::Unix(PathBuf::from(descriptor.path)))
75 }
76 "tcp" if descriptor.port > 0 => Ok(DaemonEndpoint::Tcp {
77 host: normalize_tcp_host(&descriptor.host),
78 port: descriptor.port,
79 }),
80 transport => Err(Error::InvalidEndpoint(format!(
81 "invalid transport or address: {transport}"
82 ))),
83 }
84}
85
86fn normalize_tcp_host(host: &str) -> String {
87 let value = host.trim();
88 let value = value
89 .strip_prefix('[')
90 .and_then(|value| value.strip_suffix(']'))
91 .unwrap_or(value)
92 .trim();
93 match value {
94 "" | "0.0.0.0" | "localhost" => "127.0.0.1".into(),
95 "::" => "::1".into(),
96 value => value.into(),
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::time::{SystemTime, UNIX_EPOCH};
104
105 fn temp_home() -> PathBuf {
106 let nonce = SystemTime::now()
107 .duration_since(UNIX_EPOCH)
108 .expect("clock")
109 .as_nanos();
110 env::temp_dir().join(format!("cccc-sdk-endpoint-{}-{nonce}", std::process::id()))
111 }
112
113 #[test]
114 fn discovers_and_normalizes_tcp_descriptor() {
115 let home = temp_home();
116 let daemon = home.join("daemon");
117 fs::create_dir_all(&daemon).expect("create daemon dir");
118 fs::write(
119 daemon.join("ccccd.addr.json"),
120 r#"{"v":1,"transport":"tcp","host":"0.0.0.0","port":43123}"#,
121 )
122 .expect("write descriptor");
123
124 let endpoint = discover_endpoint(Some(&home)).expect("discover endpoint");
125 assert_eq!(
126 endpoint,
127 DaemonEndpoint::Tcp {
128 host: "127.0.0.1".into(),
129 port: 43123,
130 }
131 );
132 fs::remove_dir_all(home).expect("cleanup");
133 }
134
135 #[test]
136 fn preserves_connectable_ipv6_and_normalizes_only_the_wildcard() {
137 assert_eq!(normalize_tcp_host("::1"), "::1");
138 assert_eq!(normalize_tcp_host("2001:db8::1"), "2001:db8::1");
139 assert_eq!(normalize_tcp_host("[::1]"), "::1");
140 assert_eq!(normalize_tcp_host("::"), "::1");
141 assert_eq!(normalize_tcp_host("[::]"), "::1");
142 }
143
144 #[test]
145 fn rejects_unknown_descriptor_versions() {
146 let result = endpoint_from_descriptor(EndpointDescriptor {
147 v: 2,
148 transport: "tcp".into(),
149 path: String::new(),
150 host: "127.0.0.1".into(),
151 port: 43123,
152 });
153 assert!(matches!(result, Err(Error::InvalidEndpoint(_))));
154 }
155}