Skip to content

Commit 810b5cf

Browse files
authored
Merge pull request #1803 from EliahKagan/run-ci/fchmod
When adding +x, get and set mode through the file descriptor
2 parents 847eda2 + ee7b10c commit 810b5cf

File tree

4 files changed

+98
-97
lines changed

4 files changed

+98
-97
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-worktree-state/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ gix-filter = { version = "^0.17.0", path = "../gix-filter" }
2929
io-close = "0.3.7"
3030
thiserror = "2.0.0"
3131
bstr = { version = "1.3.0", default-features = false }
32+
33+
[target.'cfg(unix)'.dependencies]
34+
rustix = { version = "0.38.20", default-features = false, features = [
35+
"std",
36+
"fs",
37+
] }

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ where
177177
// We process each key and do as the filter process tells us, while collecting data about the overall progress.
178178
let keys: BTreeSet<_> = delayed_filter_results.iter().map(|d| d.key.clone()).collect();
179179
let mut unknown_paths = Vec::new();
180-
let mut rela_path_as_path = Default::default();
181180
for key in keys {
182181
loop {
183182
let rela_paths = ctx.filters.driver_state_mut().list_delayed_paths(&key)?;
@@ -229,10 +228,7 @@ where
229228
entry::finalize_entry(
230229
delayed.entry,
231230
write.inner.into_inner().map_err(std::io::IntoInnerError::into_error)?,
232-
set_executable_after_creation.then(|| {
233-
rela_path_as_path = gix_path::from_bstr(delayed.entry_path);
234-
rela_path_as_path.as_ref()
235-
}),
231+
set_executable_after_creation,
236232
)?;
237233
delayed_files += 1;
238234
files.fetch_add(1, Ordering::Relaxed);

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

Lines changed: 90 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ where
135135
};
136136

137137
// For possibly existing, overwritten files, we must change the file mode explicitly.
138-
finalize_entry(entry, file, set_executable_after_creation.then_some(dest))?;
138+
finalize_entry(entry, file, set_executable_after_creation)?;
139139
num_bytes
140140
}
141141
gix_index::entry::Mode::SYMLINK => {
@@ -275,20 +275,16 @@ pub(crate) fn open_file(
275275
try_op_or_unlink(path, overwrite_existing, |p| options.open(p)).map(|f| (f, set_executable_after_creation))
276276
}
277277

278-
/// Close `file` and store its stats in `entry`, possibly setting `file` executable depending on `set_executable_after_creation`.
279-
#[cfg_attr(windows, allow(unused_variables))]
278+
/// Close `file` and store its stats in `entry`, possibly setting `file` executable.
280279
pub(crate) fn finalize_entry(
281280
entry: &mut gix_index::Entry,
282281
file: std::fs::File,
283-
set_executable_after_creation: Option<&Path>,
282+
#[cfg_attr(windows, allow(unused_variables))] set_executable_after_creation: bool,
284283
) -> Result<(), crate::checkout::Error> {
285284
// For possibly existing, overwritten files, we must change the file mode explicitly.
286285
#[cfg(unix)]
287-
if let Some(path) = set_executable_after_creation {
288-
let old_perm = std::fs::symlink_metadata(path)?.permissions();
289-
if let Some(new_perm) = set_mode_executable(old_perm) {
290-
std::fs::set_permissions(path, new_perm)?;
291-
}
286+
if set_executable_after_creation {
287+
set_executable(&file)?;
292288
}
293289
// NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
294290
// revisit this once there is a bug to fix.
@@ -297,102 +293,104 @@ pub(crate) fn finalize_entry(
297293
Ok(())
298294
}
299295

296+
/// Use `fstat` and `fchmod` on a file descriptor to make a regular file executable.
297+
///
298+
/// See `let_readers_execute` for the exact details of how the mode is transformed.
300299
#[cfg(unix)]
301-
fn set_mode_executable(mut perm: std::fs::Permissions) -> Option<std::fs::Permissions> {
302-
use std::os::unix::fs::PermissionsExt;
303-
let mut mode = perm.mode();
304-
if mode & 0o170000 != 0o100000 {
305-
return None; // Stop if we don't have a regular file anymore.
306-
}
307-
mode &= 0o777; // Clear non-rwx bits (setuid, setgid, sticky).
308-
mode |= (mode & 0o444) >> 2; // Let readers also execute.
309-
perm.set_mode(mode);
310-
Some(perm)
300+
fn set_executable(file: &std::fs::File) -> Result<(), std::io::Error> {
301+
let old_raw_mode = rustix::fs::fstat(file)?.st_mode;
302+
let new_mode = let_readers_execute(old_raw_mode);
303+
rustix::fs::fchmod(file, new_mode)?;
304+
Ok(())
305+
}
306+
307+
/// Given the st_mode of a regular file, compute the mode with executable bits safely added.
308+
///
309+
/// Currently this adds executable bits for whoever has read bits already. It doesn't use the umask.
310+
/// Set-user-ID and set-group-ID bits are unset for safety. The sticky bit is also unset.
311+
///
312+
/// This returns only mode bits, not file type. The return value can be passed to chmod or fchmod.
313+
#[cfg(unix)]
314+
fn let_readers_execute(mut raw_mode: rustix::fs::RawMode) -> rustix::fs::Mode {
315+
assert_eq!(
316+
raw_mode & 0o170000,
317+
0o100000,
318+
"bug in caller if not from a regular file"
319+
);
320+
raw_mode &= 0o777; // Clear type, non-rwx mode bits (setuid, setgid, sticky).
321+
raw_mode |= (raw_mode & 0o444) >> 2; // Let readers also execute.
322+
rustix::fs::Mode::from_bits(raw_mode).expect("all bits recognized")
311323
}
312324

313325
#[cfg(all(test, unix))]
314326
mod tests {
315-
fn pretty(maybe_mode: Option<u32>) -> String {
316-
match maybe_mode {
317-
Some(mode) => format!("Some({mode:04o})"),
318-
None => "None".into(),
319-
}
320-
}
321-
322327
#[test]
323-
fn set_mode_executable() {
328+
fn let_readers_execute() {
324329
let cases = [
325330
// Common cases:
326-
(0o100755, Some(0o755)),
327-
(0o100644, Some(0o755)),
328-
(0o100750, Some(0o750)),
329-
(0o100640, Some(0o750)),
330-
(0o100700, Some(0o700)),
331-
(0o100600, Some(0o700)),
332-
(0o100775, Some(0o775)),
333-
(0o100664, Some(0o775)),
334-
(0o100770, Some(0o770)),
335-
(0o100660, Some(0o770)),
336-
(0o100764, Some(0o775)),
337-
(0o100760, Some(0o770)),
331+
(0o100755, 0o755),
332+
(0o100644, 0o755),
333+
(0o100750, 0o750),
334+
(0o100640, 0o750),
335+
(0o100700, 0o700),
336+
(0o100600, 0o700),
337+
(0o100775, 0o775),
338+
(0o100664, 0o775),
339+
(0o100770, 0o770),
340+
(0o100660, 0o770),
341+
(0o100764, 0o775),
342+
(0o100760, 0o770),
338343
// Less common:
339-
(0o100674, Some(0o775)),
340-
(0o100670, Some(0o770)),
341-
(0o100000, Some(0o000)),
342-
(0o100400, Some(0o500)),
343-
(0o100440, Some(0o550)),
344-
(0o100444, Some(0o555)),
345-
(0o100462, Some(0o572)),
346-
(0o100242, Some(0o252)),
347-
(0o100167, Some(0o177)),
344+
(0o100674, 0o775),
345+
(0o100670, 0o770),
346+
(0o100000, 0o000),
347+
(0o100400, 0o500),
348+
(0o100440, 0o550),
349+
(0o100444, 0o555),
350+
(0o100462, 0o572),
351+
(0o100242, 0o252),
352+
(0o100167, 0o177),
348353
// With set-user-ID, set-group-ID, and sticky bits:
349-
(0o104755, Some(0o755)),
350-
(0o104644, Some(0o755)),
351-
(0o102755, Some(0o755)),
352-
(0o102644, Some(0o755)),
353-
(0o101755, Some(0o755)),
354-
(0o101644, Some(0o755)),
355-
(0o106755, Some(0o755)),
356-
(0o106644, Some(0o755)),
357-
(0o104750, Some(0o750)),
358-
(0o104640, Some(0o750)),
359-
(0o102750, Some(0o750)),
360-
(0o102640, Some(0o750)),
361-
(0o101750, Some(0o750)),
362-
(0o101640, Some(0o750)),
363-
(0o106750, Some(0o750)),
364-
(0o106640, Some(0o750)),
365-
(0o107644, Some(0o755)),
366-
(0o107000, Some(0o000)),
367-
(0o106400, Some(0o500)),
368-
(0o102462, Some(0o572)),
369-
// Where it was replaced with a directory due to a race:
370-
(0o040755, None),
371-
(0o040644, None),
372-
(0o040600, None),
373-
(0o041755, None),
374-
(0o041644, None),
375-
(0o046644, None),
376-
// Where it was replaced with a symlink due to a race:
377-
(0o120777, None),
378-
(0o120644, None),
379-
// Where it was replaced with some other non-regular file due to a race:
380-
(0o140644, None),
381-
(0o060644, None),
382-
(0o020644, None),
383-
(0o010644, None),
354+
(0o104755, 0o755),
355+
(0o104644, 0o755),
356+
(0o102755, 0o755),
357+
(0o102644, 0o755),
358+
(0o101755, 0o755),
359+
(0o101644, 0o755),
360+
(0o106755, 0o755),
361+
(0o106644, 0o755),
362+
(0o104750, 0o750),
363+
(0o104640, 0o750),
364+
(0o102750, 0o750),
365+
(0o102640, 0o750),
366+
(0o101750, 0o750),
367+
(0o101640, 0o750),
368+
(0o106750, 0o750),
369+
(0o106640, 0o750),
370+
(0o107644, 0o755),
371+
(0o107000, 0o000),
372+
(0o106400, 0o500),
373+
(0o102462, 0o572),
384374
];
385-
for (old_mode, expected) in cases {
386-
use std::os::unix::fs::PermissionsExt;
387-
let old_perm = std::fs::Permissions::from_mode(old_mode);
388-
let actual = super::set_mode_executable(old_perm).map(|perm| perm.mode());
375+
for (st_mode, raw_expected) in cases {
376+
let expected = rustix::fs::Mode::from_bits(raw_expected).expect("expected mode is a mode");
377+
let actual = super::let_readers_execute(st_mode);
389378
assert_eq!(
390-
actual,
391-
expected,
392-
"{old_mode:06o} should become {}, became {}",
393-
pretty(expected),
394-
pretty(actual)
379+
actual, expected,
380+
"{st_mode:06o} should become {expected:04o}, became {actual:04o}"
395381
);
396382
}
397383
}
384+
385+
#[test]
386+
#[should_panic]
387+
fn let_readers_execute_panics_on_directory() {
388+
super::let_readers_execute(0o040644);
389+
}
390+
391+
#[test]
392+
#[should_panic]
393+
fn let_readers_execute_should_panic_on_symlink() {
394+
super::let_readers_execute(0o120644);
395+
}
398396
}

0 commit comments

Comments
 (0)