Skip to content

Commit 2d7d1a6

Browse files
refactor(hm-util): Replace bare (file_mode: u32, dir_mode: u32) pair with distinct mode newtypes to prevent swapped-argument bugs (#122)
1 parent d466802 commit 2d7d1a6

3 files changed

Lines changed: 48 additions & 24 deletions

File tree

crates/hm-config/src/creds.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ fn load() -> CredentialFile {
3333
fn save(file: &CredentialFile) -> Result<()> {
3434
let p = path()?;
3535
let serialized = toml::to_string_pretty(file).context("serializing credentials")?;
36-
hm_util::os::fs::blocking::write_atomic_restricted(&p, serialized.as_bytes(), 0o600, 0o700)
37-
.with_context(|| format!("writing {}", p.display()))?;
36+
hm_util::os::fs::blocking::write_atomic_restricted(
37+
&p,
38+
serialized.as_bytes(),
39+
hm_util::os::fs::FileMode(0o600),
40+
hm_util::os::fs::DirMode(0o700),
41+
)
42+
.with_context(|| format!("writing {}", p.display()))?;
3843
Ok(())
3944
}
4045

crates/hm-config/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ impl Config {
134134
hm_util::os::fs::blocking::write_atomic_restricted(
135135
path,
136136
serialized.as_bytes(),
137-
0o644,
138-
0o700,
137+
hm_util::os::fs::FileMode(0o644),
138+
hm_util::os::fs::DirMode(0o700),
139139
)
140140
.with_context(|| format!("writing {}", path.display()))
141141
}

crates/hm-util/src/os/fs.rs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,35 @@
1212
use std::io;
1313
use std::path::Path;
1414

15-
/// Write `contents` to `path` atomically with `file_mode`, ensuring the
16-
/// parent directory exists and is set to `dir_mode`.
15+
/// Unix mode bits for a file (e.g. `0o600`).
16+
///
17+
/// A distinct newtype from [`DirMode`] so the file- and directory-mode
18+
/// arguments of [`write_atomic_restricted`] cannot be transposed: passing
19+
/// them in the wrong order is a compile error rather than a silent
20+
/// security regression (a secrets file landing at `0o700`, say).
21+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22+
pub struct FileMode(pub u32);
23+
24+
/// Unix mode bits for a directory (e.g. `0o700`).
25+
///
26+
/// See [`FileMode`] for why this is a distinct newtype.
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28+
pub struct DirMode(pub u32);
29+
30+
/// Write `contents` to `path` atomically with `file`, ensuring the
31+
/// parent directory exists and is set to `dir`.
1732
///
1833
/// # Errors
1934
///
2035
/// Returns an error if `path` has no parent or no file-name component,
21-
/// the parent directory cannot be created or chmod'd to `dir_mode`, the
22-
/// tempfile cannot be opened with `file_mode` or written, or the final
36+
/// the parent directory cannot be created or chmod'd to `dir`, the
37+
/// tempfile cannot be opened with `file` or written, or the final
2338
/// `rename` over `path` fails.
2439
pub async fn write_atomic_restricted(
2540
path: impl AsRef<Path>,
2641
contents: impl AsRef<[u8]>,
27-
file_mode: u32,
28-
dir_mode: u32,
42+
file: FileMode,
43+
dir: DirMode,
2944
) -> io::Result<()> {
3045
let path = path.as_ref().to_owned();
3146
let contents = contents.as_ref().to_vec();
@@ -40,7 +55,7 @@ pub async fn write_atomic_restricted(
4055
})?
4156
.to_owned();
4257

43-
create_dir_with_mode(&parent, dir_mode).await?;
58+
create_dir_with_mode(&parent, dir.0).await?;
4459

4560
let file_name = path
4661
.file_name()
@@ -55,7 +70,7 @@ pub async fn write_atomic_restricted(
5570
tmp_name.push(format!(".tmp.{}", std::process::id()));
5671
let tmp_path = parent.join(&tmp_name);
5772

58-
write_file_with_mode(&tmp_path, &contents, file_mode).await?;
73+
write_file_with_mode(&tmp_path, &contents, file.0).await?;
5974

6075
let rename_result = atomic_rename_over(&tmp_path, &path).await;
6176
if rename_result.is_err() {
@@ -189,6 +204,7 @@ async fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Re
189204
/// `tokio::task::block_in_place`. Safe to call from sync contexts
190205
/// that run inside a tokio runtime (e.g. extism `host_fn` callbacks).
191206
pub mod blocking {
207+
use super::{DirMode, FileMode};
192208
use std::io;
193209
use std::path::Path;
194210

@@ -203,18 +219,16 @@ pub mod blocking {
203219
/// # Errors
204220
///
205221
/// Returns an error if `path` has no parent or no file-name component,
206-
/// the parent directory cannot be created or chmod'd to `dir_mode`, the
207-
/// tempfile cannot be opened with `file_mode` or written, or the final
222+
/// the parent directory cannot be created or chmod'd to `dir`, the
223+
/// tempfile cannot be opened with `file` or written, or the final
208224
/// `rename` over `path` fails.
209225
pub fn write_atomic_restricted(
210226
path: impl AsRef<Path>,
211227
contents: impl AsRef<[u8]>,
212-
file_mode: u32,
213-
dir_mode: u32,
228+
file: FileMode,
229+
dir: DirMode,
214230
) -> io::Result<()> {
215-
block_on(super::write_atomic_restricted(
216-
path, contents, file_mode, dir_mode,
217-
))
231+
block_on(super::write_atomic_restricted(path, contents, file, dir))
218232
}
219233

220234
/// Blocking counterpart of [`super::remove_file_if_exists`].
@@ -241,9 +255,14 @@ mod tests {
241255
let dir = tmp.path().join("hm");
242256
let file = dir.join("credentials.toml");
243257

244-
write_atomic_restricted(&file, b"token = \"hunter2\"\n", 0o600, 0o700)
245-
.await
246-
.unwrap();
258+
write_atomic_restricted(
259+
&file,
260+
b"token = \"hunter2\"\n",
261+
FileMode(0o600),
262+
DirMode(0o700),
263+
)
264+
.await
265+
.unwrap();
247266

248267
let fmode = std::fs::metadata(&file).unwrap().permissions().mode() & 0o777;
249268
assert_eq!(fmode, 0o600, "file mode must be 0o600, got {fmode:o}");
@@ -257,10 +276,10 @@ mod tests {
257276
async fn rewrite_preserves_0600() {
258277
let tmp = tempfile::tempdir().unwrap();
259278
let file = tmp.path().join("credentials.toml");
260-
write_atomic_restricted(&file, b"a", 0o600, 0o700)
279+
write_atomic_restricted(&file, b"a", FileMode(0o600), DirMode(0o700))
261280
.await
262281
.unwrap();
263-
write_atomic_restricted(&file, b"bb", 0o600, 0o700)
282+
write_atomic_restricted(&file, b"bb", FileMode(0o600), DirMode(0o700))
264283
.await
265284
.unwrap();
266285
let fmode = std::fs::metadata(&file).unwrap().permissions().mode() & 0o777;

0 commit comments

Comments
 (0)