1use anyhow::{Context, Result};
9use std::path::Path;
10
11pub fn write_atomic_restricted(
35 path: &Path,
36 contents: &[u8],
37 file_mode: u32,
38 dir_mode: u32,
39) -> Result<()> {
40 let parent = path
41 .parent()
42 .with_context(|| format!("{} has no parent directory", path.display()))?;
43
44 create_dir_with_mode(parent, dir_mode)
45 .with_context(|| format!("creating {}", parent.display()))?;
46
47 let file_name = path
51 .file_name()
52 .with_context(|| format!("{} has no file name", path.display()))?
53 .to_os_string();
54 let mut tmp_name = file_name;
55 tmp_name.push(format!(".tmp.{}", std::process::id()));
56 let tmp_path = parent.join(&tmp_name);
57
58 write_file_with_mode(&tmp_path, contents, file_mode)
59 .with_context(|| format!("writing {}", tmp_path.display()))?;
60
61 let persist_result = std::fs::rename(&tmp_path, path)
62 .with_context(|| format!("renaming {} -> {}", tmp_path.display(), path.display()));
63
64 if persist_result.is_err() {
65 let _ = std::fs::remove_file(&tmp_path);
68 }
69 persist_result?;
70
71 Ok(())
72}
73
74pub fn remove_if_exists(path: &Path) -> Result<()> {
82 match std::fs::remove_file(path) {
83 Ok(()) => Ok(()),
84 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
85 Err(e) => Err(e).with_context(|| format!("removing {}", path.display())),
86 }
87}
88
89#[cfg(unix)]
90fn create_dir_with_mode(dir: &Path, mode: u32) -> std::io::Result<()> {
91 use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
92 if dir.exists() {
93 let current = std::fs::metadata(dir)?.permissions().mode() & 0o777;
94 if current != mode {
95 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(mode))?;
96 }
97 } else {
98 std::fs::DirBuilder::new()
99 .recursive(true)
100 .mode(mode)
101 .create(dir)?;
102 }
103 Ok(())
104}
105
106#[cfg(not(unix))]
107fn create_dir_with_mode(dir: &Path, _mode: u32) -> std::io::Result<()> {
108 std::fs::create_dir_all(dir)
109}
110
111#[cfg(unix)]
112fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> std::io::Result<()> {
113 use std::io::Write;
114 use std::os::unix::fs::OpenOptionsExt;
115 let mut f = std::fs::OpenOptions::new()
116 .write(true)
117 .create(true)
118 .truncate(true)
119 .mode(mode)
120 .open(path)?;
121 f.write_all(contents)?;
122 f.sync_all()?;
123 Ok(())
124}
125
126#[cfg(not(unix))]
127fn write_file_with_mode(path: &Path, contents: &[u8], _mode: u32) -> std::io::Result<()> {
128 std::fs::write(path, contents)
129}
130
131#[cfg(all(test, unix))]
132#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
133mod tests {
134 use super::*;
135 use std::os::unix::fs::PermissionsExt;
136
137 #[test]
138 fn writes_file_and_dir_with_requested_modes() {
139 let tmp = tempfile::tempdir().unwrap();
140 let target = tmp.path().join("sub").join("creds");
141 write_atomic_restricted(&target, b"hello", 0o600, 0o700).unwrap();
142
143 assert_eq!(std::fs::read(&target).unwrap(), b"hello");
144 let file_mode = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
145 let dir_mode = std::fs::metadata(target.parent().unwrap())
146 .unwrap()
147 .permissions()
148 .mode()
149 & 0o777;
150 assert_eq!(
151 file_mode, 0o600,
152 "file mode must be 0o600, got {file_mode:o}"
153 );
154 assert_eq!(dir_mode, 0o700, "dir mode must be 0o700, got {dir_mode:o}");
155 }
156
157 #[test]
158 fn overwrites_existing_file_preserving_mode() {
159 let tmp = tempfile::tempdir().unwrap();
160 let target = tmp.path().join("creds");
161 write_atomic_restricted(&target, b"v1", 0o600, 0o700).unwrap();
162 write_atomic_restricted(&target, b"v2", 0o600, 0o700).unwrap();
163
164 assert_eq!(std::fs::read(&target).unwrap(), b"v2");
165 let mode = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
166 assert_eq!(mode, 0o600);
167 }
168
169 #[test]
170 fn tightens_existing_dir_with_looser_mode() {
171 let tmp = tempfile::tempdir().unwrap();
172 let dir = tmp.path().join("loose");
173 std::fs::create_dir(&dir).unwrap();
174 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
175
176 let target = dir.join("creds");
177 write_atomic_restricted(&target, b"x", 0o600, 0o700).unwrap();
178
179 let dir_mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
180 assert_eq!(dir_mode, 0o700);
181 }
182
183 #[test]
184 fn remove_if_exists_is_idempotent() {
185 let tmp = tempfile::tempdir().unwrap();
186 let target = tmp.path().join("nothing");
187 remove_if_exists(&target).unwrap();
188 std::fs::write(&target, "x").unwrap();
189 remove_if_exists(&target).unwrap();
190 assert!(!target.exists());
191 }
192}