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 match host.trim() {
88 "" | "0.0.0.0" | "::" | "[::]" | "localhost" => "127.0.0.1".into(),
89 value => value.into(),
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::time::{SystemTime, UNIX_EPOCH};
97
98 fn temp_home() -> PathBuf {
99 let nonce = SystemTime::now()
100 .duration_since(UNIX_EPOCH)
101 .expect("clock")
102 .as_nanos();
103 env::temp_dir().join(format!("cccc-sdk-endpoint-{}-{nonce}", std::process::id()))
104 }
105
106 #[test]
107 fn discovers_and_normalizes_tcp_descriptor() {
108 let home = temp_home();
109 let daemon = home.join("daemon");
110 fs::create_dir_all(&daemon).expect("create daemon dir");
111 fs::write(
112 daemon.join("ccccd.addr.json"),
113 r#"{"v":1,"transport":"tcp","host":"0.0.0.0","port":43123}"#,
114 )
115 .expect("write descriptor");
116
117 let endpoint = discover_endpoint(Some(&home)).expect("discover endpoint");
118 assert_eq!(
119 endpoint,
120 DaemonEndpoint::Tcp {
121 host: "127.0.0.1".into(),
122 port: 43123,
123 }
124 );
125 fs::remove_dir_all(home).expect("cleanup");
126 }
127
128 #[test]
129 fn rejects_unknown_descriptor_versions() {
130 let result = endpoint_from_descriptor(EndpointDescriptor {
131 v: 2,
132 transport: "tcp".into(),
133 path: String::new(),
134 host: "127.0.0.1".into(),
135 port: 43123,
136 });
137 assert!(matches!(result, Err(Error::InvalidEndpoint(_))));
138 }
139}