1use std::borrow::Cow;
2use std::env;
3use std::path::PathBuf;
4
5use eyre::{Result, eyre};
6
7use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
8use getrandom::getrandom;
9use uuid::Uuid;
10
11pub fn crypto_random_bytes<const N: usize>() -> [u8; N] {
13 let mut ret = [0u8; N];
16
17 getrandom(&mut ret).expect("Failed to generate random bytes!");
18
19 ret
20}
21
22pub fn crypto_random_string<const N: usize>() -> String {
24 let bytes = crypto_random_bytes::<N>();
25
26 BASE64_URL_SAFE_NO_PAD.encode(bytes)
29}
30
31pub fn uuid_v7() -> Uuid {
32 Uuid::now_v7()
33}
34
35pub fn uuid_v4() -> String {
36 Uuid::new_v4().as_simple().to_string()
37}
38
39pub fn has_git_dir(path: &str) -> bool {
40 let mut gitdir = PathBuf::from(path);
41 gitdir.push(".git");
42
43 gitdir.exists()
44}
45
46pub fn in_git_repo(path: &str) -> Option<PathBuf> {
50 let mut gitdir = PathBuf::from(path);
51
52 while gitdir.parent().is_some() && !has_git_dir(gitdir.to_str().unwrap()) {
53 gitdir.pop();
54 }
55
56 if gitdir.parent().is_some() {
58 return Some(gitdir);
59 }
60
61 None
62}
63
64#[cfg(not(target_os = "windows"))]
69pub fn home_dir() -> PathBuf {
70 let home = std::env::var("HOME").expect("$HOME not found");
71 PathBuf::from(home)
72}
73
74#[cfg(target_os = "windows")]
75pub fn home_dir() -> PathBuf {
76 let home = std::env::var("USERPROFILE").expect("%userprofile% not found");
77 PathBuf::from(home)
78}
79
80pub fn config_dir() -> PathBuf {
81 let config_dir =
82 std::env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
83 config_dir.join("atuin")
84}
85
86pub fn data_dir() -> PathBuf {
87 let data_dir = std::env::var("XDG_DATA_HOME")
88 .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
89
90 data_dir.join("atuin")
91}
92
93pub fn runtime_dir() -> PathBuf {
94 std::env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from)
95}
96
97pub fn dotfiles_cache_dir() -> PathBuf {
98 let data_dir = std::env::var("XDG_DATA_HOME")
100 .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
101
102 data_dir.join("atuin").join("dotfiles").join("cache")
103}
104
105pub fn get_current_dir() -> String {
106 match env::var("PWD") {
108 Ok(v) => v,
109 Err(_) => match env::current_dir() {
110 Ok(dir) => dir.display().to_string(),
111 Err(_) => String::from(""),
112 },
113 }
114}
115
116pub fn broken_symlink<P: Into<PathBuf>>(path: P) -> bool {
117 let path = path.into();
118 path.is_symlink() && !path.exists()
119}
120
121pub trait Escapable: AsRef<str> {
128 fn escape_control(&self) -> Cow<'_, str> {
129 if !self.as_ref().contains(|c: char| c.is_ascii_control()) {
130 self.as_ref().into()
131 } else {
132 let mut remaining = self.as_ref();
133 let mut buf = String::with_capacity(remaining.len());
135 while let Some(i) = remaining.find(|c: char| c.is_ascii_control()) {
136 buf.push_str(&remaining[..i]);
138 buf.push('^');
139 buf.push(match remaining.as_bytes()[i] {
140 0x7F => '?',
141 code => char::from_u32(u32::from(code) + 64).unwrap(),
142 });
143 remaining = &remaining[i + 1..];
144 }
145 buf.push_str(remaining);
146 buf.into()
147 }
148 }
149}
150
151pub fn unquote(s: &str) -> Result<String> {
152 if s.chars().count() < 2 {
153 return Err(eyre!("not enough chars"));
154 }
155
156 let quote = s.chars().next().unwrap();
157
158 if quote != '"' && quote != '\'' && quote != '`' {
160 return Ok(s.to_string());
161 }
162
163 if s.chars().last().unwrap() != quote {
164 return Err(eyre!("unexpected eof, quotes do not match"));
165 }
166
167 let s = &s[1..s.len() - 1];
171
172 Ok(s.to_string())
173}
174
175impl<T: AsRef<str>> Escapable for T {}
176
177#[allow(unsafe_code)]
178#[cfg(test)]
179mod tests {
180 use pretty_assertions::assert_ne;
181
182 use super::*;
183
184 use std::collections::HashSet;
185
186 #[cfg(not(windows))]
187 #[test]
188 fn test_dirs() {
189 test_config_dir_xdg();
191 test_config_dir();
192 test_data_dir_xdg();
193 test_data_dir();
194 }
195
196 #[cfg(not(windows))]
197 fn test_config_dir_xdg() {
198 unsafe { env::remove_var("HOME") };
200 unsafe { env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config") };
202 assert_eq!(
203 config_dir(),
204 PathBuf::from("/home/user/custom_config/atuin")
205 );
206 unsafe { env::remove_var("XDG_CONFIG_HOME") };
208 }
209
210 #[cfg(not(windows))]
211 fn test_config_dir() {
212 unsafe { env::set_var("HOME", "/home/user") };
214 unsafe { env::remove_var("XDG_CONFIG_HOME") };
216
217 assert_eq!(config_dir(), PathBuf::from("/home/user/.config/atuin"));
218
219 unsafe { env::remove_var("HOME") };
221 }
222
223 #[cfg(not(windows))]
224 fn test_data_dir_xdg() {
225 unsafe { env::remove_var("HOME") };
227 unsafe { env::set_var("XDG_DATA_HOME", "/home/user/custom_data") };
229 assert_eq!(data_dir(), PathBuf::from("/home/user/custom_data/atuin"));
230 unsafe { env::remove_var("XDG_DATA_HOME") };
232 }
233
234 #[cfg(not(windows))]
235 fn test_data_dir() {
236 unsafe { env::set_var("HOME", "/home/user") };
238 unsafe { env::remove_var("XDG_DATA_HOME") };
240 assert_eq!(data_dir(), PathBuf::from("/home/user/.local/share/atuin"));
241 unsafe { env::remove_var("HOME") };
243 }
244
245 #[test]
246 fn uuid_is_unique() {
247 let how_many: usize = 1000000;
248
249 let mut uuids: HashSet<Uuid> = HashSet::with_capacity(how_many);
251
252 for _ in 0..how_many {
254 let uuid = uuid_v7();
255 uuids.insert(uuid);
256 }
257
258 assert_eq!(uuids.len(), how_many);
259 }
260
261 #[test]
262 fn escape_control_characters() {
263 use super::Escapable;
264 assert_eq!("\x1b[31mfoo".escape_control(), "^[[31mfoo");
266
267 assert_eq!("foo\tbar".escape_control(), "foo^Ibar");
269
270 assert_eq!("two words".escape_control(), "two words");
272
273 let s = "🐢\x1b[32m🦀";
275 assert_eq!(s.escape_control(), s.replace("\x1b", "^["));
276 }
277
278 #[test]
279 fn escape_no_control_characters() {
280 use super::Escapable as _;
281 assert!(matches!(
282 "no control characters".escape_control(),
283 Cow::Borrowed(_)
284 ));
285 assert!(matches!(
286 "with \x1b[31mcontrol\x1b[0m characters".escape_control(),
287 Cow::Owned(_)
288 ));
289 }
290
291 #[test]
292 fn dumb_random_test() {
293 assert_ne!(crypto_random_string::<1>(), crypto_random_string::<1>());
297 assert_ne!(crypto_random_string::<2>(), crypto_random_string::<2>());
298 assert_ne!(crypto_random_string::<4>(), crypto_random_string::<4>());
299 assert_ne!(crypto_random_string::<8>(), crypto_random_string::<8>());
300 assert_ne!(crypto_random_string::<16>(), crypto_random_string::<16>());
301 assert_ne!(crypto_random_string::<32>(), crypto_random_string::<32>());
302 }
303}