Skip to content

Commit 12f672f

Browse files
authored
Merge pull request #1764 from EliahKagan/finalize-entry
Refine how `finalize_entry` sets executable bits
2 parents 7ec21bb + 4d5e656 commit 12f672f

File tree

5 files changed

+172
-8
lines changed

5 files changed

+172
-8
lines changed

gix-worktree-state/src/checkout/entry.rs

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::borrow::Cow;
21
use std::{
3-
fs::OpenOptions,
2+
borrow::Cow,
3+
fs::{OpenOptions, Permissions},
44
io::Write,
55
path::{Path, PathBuf},
66
};
@@ -286,14 +286,114 @@ pub(crate) fn finalize_entry(
286286
// For possibly existing, overwritten files, we must change the file mode explicitly.
287287
#[cfg(unix)]
288288
if let Some(path) = set_executable_after_creation {
289-
use std::os::unix::fs::PermissionsExt;
290-
let mut perm = std::fs::symlink_metadata(path)?.permissions();
291-
perm.set_mode(0o777);
292-
std::fs::set_permissions(path, perm)?;
289+
let old_perm = std::fs::symlink_metadata(path)?.permissions();
290+
if let Some(new_perm) = set_mode_executable(old_perm) {
291+
std::fs::set_permissions(path, new_perm)?;
292+
}
293293
}
294294
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
295295
// revisit this once there is a bug to fix.
296296
entry.stat = Stat::from_fs(&gix_index::fs::Metadata::from_file(&file)?)?;
297297
file.close()?;
298298
Ok(())
299299
}
300+
301+
#[cfg(unix)]
302+
fn set_mode_executable(mut perm: Permissions) -> Option<Permissions> {
303+
use std::os::unix::fs::PermissionsExt;
304+
let mut mode = perm.mode();
305+
if mode & 0o170000 != 0o100000 {
306+
return None; // Stop if we don't have a regular file anymore.
307+
}
308+
mode &= 0o777; // Clear non-rwx bits (setuid, setgid, sticky).
309+
mode |= (mode & 0o444) >> 2; // Let readers also execute.
310+
perm.set_mode(mode);
311+
Some(perm)
312+
}
313+
314+
#[cfg(all(test, unix))]
315+
mod tests {
316+
fn pretty(maybe_mode: Option<u32>) -> String {
317+
match maybe_mode {
318+
Some(mode) => format!("Some({mode:04o})"),
319+
None => "None".into(),
320+
}
321+
}
322+
323+
#[test]
324+
fn set_mode_executable() {
325+
let cases = [
326+
// Common cases:
327+
(0o100755, Some(0o755)),
328+
(0o100644, Some(0o755)),
329+
(0o100750, Some(0o750)),
330+
(0o100640, Some(0o750)),
331+
(0o100700, Some(0o700)),
332+
(0o100600, Some(0o700)),
333+
(0o100775, Some(0o775)),
334+
(0o100664, Some(0o775)),
335+
(0o100770, Some(0o770)),
336+
(0o100660, Some(0o770)),
337+
(0o100764, Some(0o775)),
338+
(0o100760, Some(0o770)),
339+
// Less common:
340+
(0o100674, Some(0o775)),
341+
(0o100670, Some(0o770)),
342+
(0o100000, Some(0o000)),
343+
(0o100400, Some(0o500)),
344+
(0o100440, Some(0o550)),
345+
(0o100444, Some(0o555)),
346+
(0o100462, Some(0o572)),
347+
(0o100242, Some(0o252)),
348+
(0o100167, Some(0o177)),
349+
// With set-user-ID, set-group-ID, and sticky bits:
350+
(0o104755, Some(0o755)),
351+
(0o104644, Some(0o755)),
352+
(0o102755, Some(0o755)),
353+
(0o102644, Some(0o755)),
354+
(0o101755, Some(0o755)),
355+
(0o101644, Some(0o755)),
356+
(0o106755, Some(0o755)),
357+
(0o106644, Some(0o755)),
358+
(0o104750, Some(0o750)),
359+
(0o104640, Some(0o750)),
360+
(0o102750, Some(0o750)),
361+
(0o102640, Some(0o750)),
362+
(0o101750, Some(0o750)),
363+
(0o101640, Some(0o750)),
364+
(0o106750, Some(0o750)),
365+
(0o106640, Some(0o750)),
366+
(0o107644, Some(0o755)),
367+
(0o107000, Some(0o000)),
368+
(0o106400, Some(0o500)),
369+
(0o102462, Some(0o572)),
370+
// Where it was replaced with a directory due to a race:
371+
(0o040755, None),
372+
(0o040644, None),
373+
(0o040600, None),
374+
(0o041755, None),
375+
(0o041644, None),
376+
(0o046644, None),
377+
// Where it was replaced with a symlink due to a race:
378+
(0o120777, None),
379+
(0o120644, None),
380+
// Where it was replaced with some other non-regular file due to a race:
381+
(0o140644, None),
382+
(0o060644, None),
383+
(0o020644, None),
384+
(0o010644, None),
385+
];
386+
for (old_mode, expected) in cases {
387+
use std::os::unix::fs::PermissionsExt;
388+
let old_perm = std::fs::Permissions::from_mode(old_mode);
389+
let actual = super::set_mode_executable(old_perm).map(|perm| perm.mode());
390+
assert_eq!(
391+
actual,
392+
expected,
393+
"{old_mode:06o} should become {}, became {}",
394+
pretty(expected),
395+
pretty(actual)
396+
);
397+
}
398+
}
399+
}

gix-worktree-state/tests/state/checkout.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,32 @@ fn overwriting_files_and_lone_directories_works() -> crate::Result {
220220

221221
let meta = std::fs::symlink_metadata(exe)?;
222222
assert!(meta.is_file());
223+
#[cfg(unix)]
223224
if opts.fs.executable_bit {
224-
#[cfg(unix)]
225-
assert_eq!(meta.mode() & 0o700, 0o700, "the executable bit is set where supported");
225+
let mode = meta.mode();
226+
assert_eq!(
227+
mode & 0o100,
228+
0o100,
229+
"executable bit set where supported ({:04o} & {:04o} = {:04o} should be {:04o})",
230+
mode,
231+
0o100,
232+
mode & 0o100,
233+
0o100
234+
);
235+
let umask_write = gix_testtools::umask() & 0o222;
236+
assert_eq!(
237+
mode & umask_write,
238+
0,
239+
"no excessive write bits are set ({:04o} & {:04o} = {:04o} should be {:04o})",
240+
mode,
241+
umask_write,
242+
mode & umask_write,
243+
0
244+
);
245+
assert_ne!(
246+
umask_write, 0,
247+
"test not meaningful unless runner umask restricts some writes"
248+
);
226249
}
227250

228251
assert_eq!(

tests/tools/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,19 @@ pub fn size_ok(actual_size: usize, expected_64_bit_size: usize) -> bool {
944944
return actual_size <= expected_64_bit_size;
945945
}
946946

947+
/// Get the umask in a way that is safe, but may be too slow for use outside of tests.
948+
#[cfg(unix)]
949+
pub fn umask() -> u32 {
950+
let output = std::process::Command::new("/bin/sh")
951+
.args(["-c", "umask"])
952+
.output()
953+
.expect("can execute `sh -c umask`");
954+
assert!(output.status.success(), "`sh -c umask` failed");
955+
assert!(output.stderr.is_empty(), "`sh -c umask` unexpected message");
956+
let text = output.stdout.to_str().expect("valid Unicode").trim();
957+
u32::from_str_radix(text, 8).expect("parses as octal number")
958+
}
959+
947960
#[cfg(test)]
948961
mod tests {
949962
use super::*;

tests/tools/src/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
use std::{fs, io, io::prelude::*, path::PathBuf};
2+
23
fn mess_in_the_middle(path: PathBuf) -> io::Result<()> {
34
let mut file = fs::OpenOptions::new().read(false).write(true).open(path)?;
45
file.seek(io::SeekFrom::Start(file.metadata()?.len() / 2))?;
56
file.write_all(b"hello")?;
67
Ok(())
78
}
89

10+
#[cfg(unix)]
11+
fn umask() -> io::Result<()> {
12+
println!("{:04o}", gix_testtools::umask());
13+
Ok(())
14+
}
15+
916
fn main() -> Result<(), Box<dyn std::error::Error>> {
1017
let mut args = std::env::args().skip(1);
1118
let scmd = args.next().expect("sub command");
1219
match &*scmd {
1320
"mess-in-the-middle" => mess_in_the_middle(PathBuf::from(args.next().expect("path to file to mess with")))?,
21+
#[cfg(unix)]
22+
"umask" => umask()?,
1423
_ => unreachable!("Unknown subcommand: {}", scmd),
1524
};
1625
Ok(())

tests/tools/tests/umask.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#[test]
2+
#[cfg(unix)]
3+
#[cfg_attr(not(target_os = "linux"), ignore = "The test itself uses /proc")]
4+
fn umask() {
5+
use std::fs::File;
6+
use std::io::{BufRead, BufReader};
7+
8+
use bstr::ByteSlice;
9+
// Check against the umask obtained via a less portable but also completely safe method.
10+
let less_portable = BufReader::new(File::open("/proc/self/status").expect("can open"))
11+
.split(b'\n')
12+
.find_map(|line| line.expect("can read").strip_prefix(b"Umask:\t").map(ToOwned::to_owned))
13+
.expect("has umask line")
14+
.to_str()
15+
.expect("umask line is valid UTF-8")
16+
.to_owned();
17+
let more_portable = format!("{:04o}", gix_testtools::umask());
18+
assert_eq!(more_portable, less_portable);
19+
}

0 commit comments

Comments
 (0)