kmp_embedded/
memory_selection_refusal.rs1use std::fmt;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum SelectionRefusal {
13 Empty,
15 Unexpanded {
19 subject: String,
20 value: String,
21 home: Option<PathBuf>,
22 },
23 Relative(String),
26 NotQuotable(String),
28 NotADirectory(PathBuf),
30 Incompatible { path: PathBuf, reason: String },
32}
33
34impl SelectionRefusal {
35 pub fn unexpanded(subject: impl Into<String>, value: impl Into<String>) -> Self {
37 Self::Unexpanded {
38 subject: subject.into(),
39 value: value.into(),
40 home: user_home(),
41 }
42 }
43
44 pub fn concerns_an_existing_store(&self) -> bool {
47 matches!(self, Self::Incompatible { .. } | Self::NotADirectory(_))
48 }
49}
50
51fn user_home() -> Option<PathBuf> {
54 ["HOME", "USERPROFILE"]
55 .into_iter()
56 .find_map(std::env::var_os)
57 .map(PathBuf::from)
58 .filter(|path| !path.as_os_str().is_empty())
59}
60
61impl fmt::Display for SelectionRefusal {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Empty => write!(
65 formatter,
66 "a user memory selection needs an absolute directory path"
67 ),
68 Self::Unexpanded {
69 subject,
70 value,
71 home,
72 } => {
73 write!(
74 formatter,
75 "{subject} `{value}` starts with `~`, but MCP host configuration does not \
76 expand shell paths"
77 )?;
78 match home
79 .as_ref()
80 .and_then(|home| strip_home_prefix(home, value))
81 {
82 Some(expanded) => write!(formatter, "; use `{}`", expanded.display()),
83 None => write!(formatter, "; use an absolute path instead"),
84 }
85 }
86 Self::Relative(value) => write!(
87 formatter,
88 "memory selection `{value}` is relative; a saved selection must be an absolute \
89 path or it would name a different store from every working directory"
90 ),
91 Self::NotQuotable(value) => write!(
92 formatter,
93 "memory selection `{value}` contains a double quote or a line break, which the \
94 settings file cannot carry back unchanged"
95 ),
96 Self::NotADirectory(path) => write!(
97 formatter,
98 "`{}` exists and is not a directory; the file was left untouched",
99 path.display()
100 ),
101 Self::Incompatible { path, reason } => write!(
102 formatter,
103 "`{}` holds memory this engine cannot open: {reason}; every file in it was left \
104 exactly as it was, and nothing was migrated, moved or converted",
105 path.display()
106 ),
107 }
108 }
109}
110
111fn strip_home_prefix(home: &Path, value: &str) -> Option<PathBuf> {
112 Path::new(value)
113 .strip_prefix("~")
114 .ok()
115 .map(|suffix| home.join(suffix))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn an_incompatible_store_promises_it_was_left_alone() {
124 let refusal = SelectionRefusal::Incompatible {
125 path: PathBuf::from("/old/store"),
126 reason: "format version 2 is not supported".to_string(),
127 };
128 let message = refusal.to_string();
129 assert!(message.contains("/old/store"), "{message}");
130 assert!(message.contains("format version 2"), "{message}");
131 assert!(message.contains("left exactly as it was"), "{message}");
132 assert!(message.contains("nothing was migrated"), "{message}");
133 assert!(refusal.concerns_an_existing_store());
134 }
135
136 #[test]
137 fn a_relative_or_shell_path_says_why_it_cannot_be_saved() {
138 let relative = SelectionRefusal::Relative("memory".to_string()).to_string();
139 assert!(relative.contains("absolute path"), "{relative}");
140
141 let expandable = SelectionRefusal::Unexpanded {
142 subject: "KMP_MCP_DATA_DIR value".to_string(),
143 value: "~/memory".to_string(),
144 home: Some(PathBuf::from("/home/u")),
145 }
146 .to_string();
147 assert!(
148 expandable.starts_with("KMP_MCP_DATA_DIR value `~/memory` starts with `~`"),
149 "{expandable}"
150 );
151 assert!(
152 expandable.contains("MCP host configuration does not expand shell paths"),
153 "{expandable}"
154 );
155 assert!(expandable.contains("use `/home/u/memory`"), "{expandable}");
156
157 let homeless = SelectionRefusal::Unexpanded {
158 subject: "memory selection".to_string(),
159 value: "~/memory".to_string(),
160 home: None,
161 }
162 .to_string();
163 assert!(homeless.contains("absolute path instead"), "{homeless}");
164 assert!(!SelectionRefusal::Empty.concerns_an_existing_store());
165 }
166}