1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde::Deserialize;
6
7pub use crate::error::ConfigError;
8
9#[derive(Debug, Default, Deserialize)]
11pub struct AnytypeHeadlessConfig {
12 #[serde(default, rename = "accountKey")]
13 pub account_key: Option<String>,
14
15 #[serde(default, rename = "sessionToken")]
16 pub session_token: Option<String>,
17
18 #[serde(default, rename = "accountId")]
19 pub account_id: Option<String>,
20}
21
22pub fn default_headless_config_path() -> Result<PathBuf, ConfigError> {
24 let home = env::var_os("HOME")
25 .filter(|value| !value.is_empty())
26 .or({
27 #[cfg(windows)]
28 {
29 env::var_os("USERPROFILE").filter(|value| !value.is_empty())
30 }
31 #[cfg(not(windows))]
32 {
33 None
34 }
35 })
36 .ok_or(ConfigError::MissingHome)?;
37 Ok(PathBuf::from(home).join(".anytype").join("config.json"))
38}
39
40pub fn load_headless_config(
46 path: Option<&Path>,
47) -> Result<Option<AnytypeHeadlessConfig>, ConfigError> {
48 let path = match path {
49 Some(path) => path.to_path_buf(),
50 None => default_headless_config_path()?,
51 };
52
53 let content = match fs::read_to_string(&path) {
54 Ok(content) => content,
55 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
56 match fs::symlink_metadata(&path) {
57 Err(metadata_error) if metadata_error.kind() == std::io::ErrorKind::NotFound => {
58 return Ok(None);
59 }
60 _ => return Err(error.into()),
61 }
62 }
63 Err(error) => return Err(error.into()),
64 };
65 let config = serde_json::from_str(&content)?;
66 Ok(Some(config))
67}
68
69#[cfg(test)]
70mod tests {
71 use std::{
72 fs,
73 sync::atomic::{AtomicU64, Ordering},
74 };
75
76 use super::*;
77
78 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
79
80 #[test]
81 fn missing_config_is_distinct_from_unreadable_config() {
82 let root = temp_path("missing-vs-unreadable");
83 fs::create_dir(&root).expect("create test directory");
84
85 assert!(
86 load_headless_config(Some(&root.join("missing.json")))
87 .expect("missing config is allowed")
88 .is_none()
89 );
90 assert!(load_headless_config(Some(&root)).is_err());
91
92 fs::remove_dir(root).expect("remove test directory");
93 }
94
95 #[cfg(unix)]
96 #[test]
97 fn broken_config_symlink_is_not_treated_as_missing() {
98 use std::os::unix::fs::symlink;
99
100 let root = temp_path("broken-symlink");
101 fs::create_dir(&root).expect("create test directory");
102 let path = root.join("config.json");
103 symlink(root.join("absent-target.json"), &path).expect("create broken symlink");
104
105 assert!(load_headless_config(Some(&path)).is_err());
106
107 fs::remove_dir_all(root).expect("remove test directory");
108 }
109
110 #[test]
111 fn loads_account_credentials_from_headless_config() {
112 let path = temp_path("credentials");
113 fs::write(
114 &path,
115 r#"{"accountId":"account-id","accountKey":"account-key","sessionToken":"session-token"}"#,
116 )
117 .expect("write config");
118
119 let config = load_headless_config(Some(&path))
120 .expect("load config")
121 .expect("config exists");
122 assert_eq!(config.account_id.as_deref(), Some("account-id"));
123 assert_eq!(config.account_key.as_deref(), Some("account-key"));
124 assert_eq!(config.session_token.as_deref(), Some("session-token"));
125
126 fs::remove_file(path).expect("remove config");
127 }
128
129 fn temp_path(label: &str) -> PathBuf {
130 let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
131 std::env::temp_dir().join(format!(
132 "anytype-rpc-config-{label}-{}-{unique}",
133 std::process::id()
134 ))
135 }
136}