1use std::{
12 env,
13 ffi::OsStr,
14 path::{Path, PathBuf},
15};
16
17const TERMINFO_DIRS: &[&str] = &[
18 "/etc/terminfo",
19 "/lib/terminfo",
20 "/usr/share/terminfo",
21 "/usr/lib/terminfo",
22 "/boot/system/data/terminfo", ];
24
25#[derive(thiserror::Error, Debug, PartialEq)]
27#[non_exhaustive]
28pub enum Error {
29 #[error("InvalidTerminalName")]
31 InvalidTerminalName,
32 #[error("File not found")]
34 FileNotFound,
35}
36
37fn find_in_directory(term_name: &OsStr, dir: &Path) -> Result<PathBuf, Error> {
38 let Some(first_byte) = term_name.as_encoded_bytes().first() else {
39 return Err(Error::InvalidTerminalName);
40 };
41
42 let first_char = *first_byte as char;
44 let filename = dir.join(first_char.to_string()).join(term_name);
45 if filename.exists() {
46 return Ok(filename);
47 }
48
49 let first_byte_hex = format!("{:02x}", *first_byte);
52 let filename = dir.join(first_byte_hex).join(term_name);
53 if filename.exists() {
54 return Ok(filename);
55 }
56
57 Err(Error::FileNotFound)
58}
59
60pub fn search_directories() -> Vec<PathBuf> {
66 let mut search_dirs = vec![];
67
68 let mut default_dirs = TERMINFO_DIRS.iter().map(PathBuf::from);
70
71 if let Ok(dir) = env::var("TERMINFO") {
73 search_dirs.push(PathBuf::from(&dir));
74 }
75
76 if let Some(home_dir) = env::home_dir() {
78 let dir = home_dir.join(".terminfo");
79 search_dirs.push(dir);
80 }
81
82 if let Ok(dirs) = env::var("TERMINFO_DIRS") {
85 for dir in dirs.split(':') {
86 if dir.is_empty() {
87 search_dirs.extend(&mut default_dirs);
89 } else {
90 search_dirs.push(PathBuf::from(dir));
91 }
92 }
93 }
94
95 search_dirs.extend(&mut default_dirs);
97
98 search_dirs
99}
100
101pub fn locate(term_name: impl AsRef<OsStr>) -> Result<PathBuf, Error> {
109 for dir in search_directories() {
110 match find_in_directory(term_name.as_ref(), &dir) {
111 Ok(file) => return Ok(file),
112 Err(Error::FileNotFound) => {}
113 Err(err) => return Err(err),
114 }
115 }
116
117 Err(Error::FileNotFound)
118}
119
120#[cfg(test)]
121mod test {
122 use std::fs::{File, create_dir, exists};
123
124 use tempfile::tempdir;
125
126 use super::*;
127
128 const TERM_NAME: &str = "no-such-terminal-123";
129
130 #[test]
131 fn empty_name() {
132 assert_eq!(locate(""), Err(Error::InvalidTerminalName));
133 }
134
135 #[test]
136 fn missing_file() {
137 assert_eq!(locate("no-such-terminal-1"), Err(Error::FileNotFound));
140 }
141
142 #[test]
143 fn found_xterm() {
144 let found_file = locate("xterm");
145 assert!(found_file.is_ok());
146 assert!(exists(found_file.unwrap()).unwrap());
147 }
148
149 #[test]
150 fn found_standard_layout_terminfo_dirs() {
151 let temp_dir = tempdir().unwrap();
152 let temp_dir = temp_dir.path();
153 let leaf_dir = temp_dir.join("n");
154 let terminfo_file = leaf_dir.join(TERM_NAME);
155 create_dir(leaf_dir).unwrap();
156 File::create(&terminfo_file).unwrap();
157 let terminfo_dirs = format!("foo:{}:bar", temp_dir.display());
158
159 temp_env::with_vars(
160 [("TERMINFO_DIRS", Some(terminfo_dirs)), ("TERMINFO", None)],
161 || {
162 assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
163 },
164 );
165 }
166
167 #[test]
168 fn found_hex_layout_terminfo_dirs() {
169 let temp_dir = tempdir().unwrap();
170 let temp_dir = temp_dir.path();
171 let leaf_dir = temp_dir.join("6e");
172 let terminfo_file = leaf_dir.join(TERM_NAME);
173 create_dir(leaf_dir).unwrap();
174 File::create(&terminfo_file).unwrap();
175 let terminfo_dirs = format!("foo:{}:bar", temp_dir.display());
176
177 temp_env::with_vars(
178 [("TERMINFO_DIRS", Some(terminfo_dirs)), ("TERMINFO", None)],
179 || {
180 assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
181 },
182 );
183 }
184
185 #[test]
186 fn found_standard_layout_terminfo_variable() {
187 let temp_dir = tempdir().unwrap();
188 let temp_dir = temp_dir.path();
189 let leaf_dir = temp_dir.join("n");
190 let terminfo_file = leaf_dir.join(TERM_NAME);
191 create_dir(leaf_dir).unwrap();
192 File::create(&terminfo_file).unwrap();
193
194 temp_env::with_vars(
195 [("TERMINFO_DIRS", None), ("TERMINFO", Some(temp_dir))],
196 || {
197 assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
198 },
199 );
200 }
201
202 #[test]
203 fn dot_terminfo_standard_layout() {
204 let temp_dir = tempdir().unwrap();
205 let temp_dir = temp_dir.path();
206 let dot_terminfo = temp_dir.join(".terminfo");
207 let leaf_dir = dot_terminfo.join("n");
208 let terminfo_file = leaf_dir.join(TERM_NAME);
209 create_dir(dot_terminfo).unwrap();
210 create_dir(leaf_dir).unwrap();
211 File::create(&terminfo_file).unwrap();
212
213 temp_env::with_vars(
214 [
215 ("TERMINFO_DIRS", None),
216 ("TERMINFO", None),
217 ("HOME", Some(temp_dir)),
218 ],
219 || {
220 assert_eq!(locate(TERM_NAME), Ok(terminfo_file));
221 },
222 );
223 }
224
225 #[test]
226 fn search_order() {
227 let expected_dirs: Vec<PathBuf> = [
228 "/my/terminfo",
229 "/home/user/.terminfo",
230 "/my/terminfo1",
231 "/my/terminfo2",
232 "/etc/terminfo",
233 "/lib/terminfo",
234 "/usr/share/terminfo",
235 "/usr/lib/terminfo",
236 "/boot/system/data/terminfo",
237 ]
238 .iter()
239 .map(PathBuf::from)
240 .collect();
241
242 temp_env::with_vars(
243 [
244 ("TERMINFO_DIRS", Some("/my/terminfo1:/my/terminfo2")),
245 ("TERMINFO", Some("/my/terminfo")),
246 ("HOME", Some("/home/user")),
247 ],
248 || {
249 assert_eq!(search_directories(), expected_dirs);
250 },
251 );
252 }
253
254 #[test]
255 fn search_order_with_empty_element() {
256 let expected_dirs: Vec<PathBuf> = [
257 "/my/terminfo",
258 "/home/user/.terminfo",
259 "/my/terminfo1",
260 "/etc/terminfo",
261 "/lib/terminfo",
262 "/usr/share/terminfo",
263 "/usr/lib/terminfo",
264 "/boot/system/data/terminfo",
265 "/my/terminfo2",
266 ]
267 .iter()
268 .map(PathBuf::from)
269 .collect();
270
271 temp_env::with_vars(
272 [
273 ("TERMINFO_DIRS", Some("/my/terminfo1::/my/terminfo2")),
274 ("TERMINFO", Some("/my/terminfo")),
275 ("HOME", Some("/home/user")),
276 ],
277 || {
278 assert_eq!(search_directories(), expected_dirs);
279 },
280 );
281 }
282}