gglib_core/paths/
private.rs1use std::fs;
18use std::io;
19use std::path::Path;
20
21pub fn create_private_dir(dir: &Path) -> io::Result<()> {
29 create_dir(dir)?;
30 make_private(dir);
31 Ok(())
32}
33
34pub fn create_private_file(file: &Path) -> io::Result<()> {
45 match create_file(file) {
46 Ok(_) => Ok(()),
47 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
48 make_private(file);
49 Ok(())
50 }
51 Err(e) => Err(e),
52 }
53}
54
55pub fn make_private(path: &Path) {
61 match tighten(path) {
62 Ok(()) => {}
63 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
64 Err(e) => tracing::warn!(
65 path = %path.display(),
66 error = %e,
67 "could not take group and other's access away; other accounts may be able to read this"
68 ),
69 }
70}
71
72#[cfg(unix)]
75fn create_dir(dir: &Path) -> io::Result<()> {
76 use std::os::unix::fs::DirBuilderExt;
77 fs::DirBuilder::new()
78 .recursive(true)
79 .mode(0o700)
80 .create(dir)
81}
82
83#[cfg(not(unix))]
85fn create_dir(dir: &Path) -> io::Result<()> {
86 fs::create_dir_all(dir)
87}
88
89#[cfg(unix)]
92fn create_file(file: &Path) -> io::Result<fs::File> {
93 use std::os::unix::fs::OpenOptionsExt;
94 fs::OpenOptions::new()
95 .write(true)
96 .create_new(true)
97 .mode(0o600)
98 .open(file)
99}
100
101#[cfg(not(unix))]
103fn create_file(file: &Path) -> io::Result<fs::File> {
104 fs::OpenOptions::new()
105 .write(true)
106 .create_new(true)
107 .open(file)
108}
109
110#[cfg(unix)]
114#[allow(clippy::verbose_bit_mask)] fn tighten(path: &Path) -> io::Result<()> {
116 use std::os::unix::fs::PermissionsExt;
117 let mode = fs::metadata(path)?.permissions().mode();
118 if mode & 0o077 == 0 {
119 return Ok(());
120 }
121 fs::set_permissions(path, fs::Permissions::from_mode(mode & 0o7700))
122}
123
124#[cfg(not(unix))]
126#[allow(clippy::unnecessary_wraps, clippy::missing_const_for_fn)] fn tighten(_path: &Path) -> io::Result<()> {
128 Ok(())
129}
130
131#[cfg(all(test, unix))]
132mod tests {
133 use std::os::unix::fs::PermissionsExt;
134
135 use super::*;
136 use crate::paths::test_utils::{ENV_LOCK, EnvVarGuard};
137 use crate::paths::{database_path, remote_identity_path};
138
139 fn mode(path: &Path) -> u32 {
140 fs::metadata(path).expect("metadata").permissions().mode() & 0o777
141 }
142
143 fn set_mode(path: &Path, mode: u32) {
144 fs::set_permissions(path, fs::Permissions::from_mode(mode)).expect("chmod");
145 }
146
147 #[test]
155 fn a_new_directory_is_private_before_any_chmod() {
156 let root = tempfile::tempdir().expect("tempdir");
157 let dir = root.path().join("data");
158
159 create_dir(&dir).expect("create");
160
161 assert_eq!(mode(&dir) & 0o077, 0, "{:o}", mode(&dir));
162 }
163
164 #[test]
167 fn a_new_file_is_private_before_any_chmod() {
168 let root = tempfile::tempdir().expect("tempdir");
169 let file = root.path().join("gglib.db");
170
171 create_file(&file).expect("create");
172
173 assert_eq!(mode(&file) & 0o077, 0, "{:o}", mode(&file));
174 }
175
176 #[test]
179 fn an_existing_database_keeps_every_byte_and_loses_what_others_had() {
180 let root = tempfile::tempdir().expect("tempdir");
181 let file = root.path().join("gglib.db");
182 fs::write(&file, b"SQLite format 3\0and the rest").expect("write");
183 set_mode(&file, 0o644);
184
185 create_private_file(&file).expect("an existing file is not an error");
186
187 assert_eq!(
188 fs::read(&file).expect("read"),
189 b"SQLite format 3\0and the rest"
190 );
191 assert_eq!(mode(&file), 0o600);
192 }
193
194 #[test]
198 fn database_path_and_remote_identity_path_leave_data_private() {
199 let _lock = ENV_LOCK.lock().unwrap();
200 let root = tempfile::tempdir().expect("tempdir");
201 let _env = EnvVarGuard::set("GGLIB_DATA_DIR", root.path().to_string_lossy().as_ref());
202 let data = root.path().join("data");
203
204 database_path().expect("database_path");
205 assert_eq!(mode(&data) & 0o077, 0, "made: {:o}", mode(&data));
206
207 set_mode(&data, 0o755);
208 remote_identity_path().expect("remote_identity_path");
209 assert_eq!(mode(&data), 0o700, "found");
210 }
211
212 #[test]
215 fn tightening_keeps_the_setgid_and_sticky_bits() {
216 let root = tempfile::tempdir().expect("tempdir");
217 let dir = root.path().join("shared");
218 fs::create_dir(&dir).expect("mkdir");
219 set_mode(&dir, 0o3775);
220
221 make_private(&dir);
222
223 let kept = fs::metadata(&dir).expect("metadata").permissions().mode() & 0o7777;
224 assert_eq!(kept, 0o3700, "{kept:o}");
225 }
226}