1use std::fs;
12use std::io::Write;
13use std::path::{Path, PathBuf};
14
15use crate::error::{Error, Result};
16
17fn home() -> Result<PathBuf> {
18 std::env::var_os("HOME")
19 .map(PathBuf::from)
20 .ok_or_else(|| Error::invalid("HOME is not set"))
21}
22
23fn env_path(name: &str) -> Option<PathBuf> {
24 std::env::var_os(name)
25 .filter(|v| !v.is_empty())
26 .map(PathBuf::from)
27}
28
29pub fn config_dir() -> Result<PathBuf> {
30 if let Some(p) = env_path("GRABCLI_CONFIG_DIR") {
31 return Ok(p);
32 }
33 if let Some(p) = env_path("XDG_CONFIG_HOME") {
34 return Ok(p.join("grabcli"));
35 }
36 Ok(home()?.join(".config").join("grabcli"))
37}
38
39pub fn state_dir() -> Result<PathBuf> {
40 if let Some(p) = env_path("GRABCLI_STATE_DIR") {
41 return Ok(p);
42 }
43 if let Some(p) = env_path("XDG_STATE_HOME") {
44 return Ok(p.join("grabcli"));
45 }
46 Ok(home()?.join(".local").join("state").join("grabcli"))
47}
48
49pub fn cache_dir() -> Result<PathBuf> {
50 if let Some(p) = env_path("GRABCLI_CACHE_DIR") {
51 return Ok(p);
52 }
53 if let Some(p) = env_path("XDG_CACHE_HOME") {
54 return Ok(p.join("grabcli"));
55 }
56 Ok(home()?.join(".cache").join("grabcli"))
57}
58
59pub fn session_path() -> Result<PathBuf> {
61 if let Some(p) = env_path("GRABCLI_SESSION") {
62 return Ok(p);
63 }
64 Ok(config_dir()?.join("session.json"))
65}
66
67pub fn config_path() -> Result<PathBuf> {
69 if let Some(p) = env_path("GRABCLI_CONFIG") {
70 return Ok(p);
71 }
72 Ok(config_dir()?.join("config.toml"))
73}
74
75pub fn ensure_private_dir(dir: &Path) -> Result<()> {
77 if !dir.exists() {
78 fs::create_dir_all(dir)?;
79 #[cfg(unix)]
80 {
81 use std::os::unix::fs::PermissionsExt;
82 fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
83 }
84 }
85 Ok(())
86}
87
88fn tmp_path(path: &Path) -> PathBuf {
89 let name = path
90 .file_name()
91 .map(|n| n.to_string_lossy().into_owned())
92 .unwrap_or_default();
93 path.with_file_name(format!(".{name}.tmp.{}", std::process::id()))
94}
95
96pub fn write_private(path: &Path, data: &[u8]) -> Result<()> {
99 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
100 ensure_private_dir(parent)?;
101 }
102 let tmp = tmp_path(path);
103 {
104 let mut opts = fs::OpenOptions::new();
105 opts.write(true).create(true).truncate(true);
106 #[cfg(unix)]
107 {
108 use std::os::unix::fs::OpenOptionsExt;
109 opts.mode(0o600);
110 }
111 let mut f = opts.open(&tmp)?;
112 f.write_all(data)?;
113 f.sync_all()?;
114 }
115 fs::rename(&tmp, path)?;
116 Ok(())
117}
118
119pub fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
121 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
122 fs::create_dir_all(parent)?;
123 }
124 let tmp = tmp_path(path);
125 {
126 let mut f = fs::File::create(&tmp)?;
127 f.write_all(data)?;
128 f.sync_all()?;
129 }
130 fs::rename(&tmp, path)?;
131 Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 #[cfg(unix)]
140 fn private_write_is_0600() -> Result<()> {
141 use std::os::unix::fs::PermissionsExt;
142 let dir = tempfile::tempdir()?;
143 let p = dir.path().join("sub").join("s.json");
144 write_private(&p, b"{}")?;
145 assert_eq!(fs::metadata(&p)?.permissions().mode() & 0o777, 0o600);
146 assert_eq!(
147 fs::metadata(p.parent().ok_or_else(|| Error::invalid("no parent"))?)?
148 .permissions()
149 .mode()
150 & 0o777,
151 0o700
152 );
153 assert_eq!(fs::read(&p)?, b"{}");
154 Ok(())
155 }
156}