pub(super) mod function { use std::{ ffi::{OsStr, OsString}, io::{BufRead, BufReader, Read}, process::{Command, Stdio}, }; use anyhow::{Context, bail}; use gix::bstr::ByteSlice; use regex::bytes::Regex; use std::sync::LazyLock; pub fn check_mode() -> anyhow::Result<()> { let root = find_root()?; let mut any_mismatch = false; let mut child = git_on(&root) .args(["ls-files", "-sz", "--", "*.sh"]) .stdout(Stdio::piped()) .spawn() .context("Can't start `git` subprocess to list index")?; let stdout = child.stdout.take().expect("should have captured stdout"); for result in BufReader::new(stdout).split(0) { let record = result.context(r"Can't read '\0'-terminated record")?; any_mismatch |= check_for_mismatch(&root, &record)?; } let status = child.wait().context("Failure running `git` subprocess to list index")?; if !status.success() { bail!("`git` subprocess to list index did not complete successfully"); } if any_mismatch { bail!("Mismatch found - scan completed, finding at least one `#!` vs. `+x` mismatch"); } Ok(()) } /// Find the top-level directory of the current repository working tree. fn find_root() -> anyhow::Result<OsString> { let output = Command::new(gix::path::env::exe_invocation()) .args(["rev-parse", "--show-toplevel"]) .output() .context("Can't run `git` to find worktree root")?; if !output.status.success() { bail!("`git` failed to find worktree root"); } let root = output .stdout .strip_suffix(b"\n") .context("Can't parse worktree root")? .to_os_str()? .to_owned(); Ok(root) } /// Prepare a `git` command, passing `root` as an operand to `-C`. /// /// This is suitable when `git` gave us the path `root`. Then it should already be in a form /// where `git -C` will be able to use it, without alteration, regardless of the platform. /// (Otherwise, it may be preferable to set `root` as the `cwd` of the `git` process instead.) fn git_on(root: &OsStr) -> Command { let mut cmd = Command::new(gix::path::env::exe_invocation()); cmd.arg("-C").arg(root); cmd } /// On mismatch, report it and return `Some(true)`. fn check_for_mismatch(root: &OsStr, record: &[u8]) -> anyhow::Result<bool> { static RECORD_REGEX: LazyLock<Regex> = LazyLock::new(|| { let pattern = r"(?-u)\A([0-7]+) ([[:xdigit:]]+) [[:digit:]]+\t(.+)\z"; Regex::new(pattern).expect("regex should be valid") }); let fields = RECORD_REGEX.captures(record).context("Malformed record from `git`")?; let mode = fields.get(1).expect("match should get mode").as_bytes(); let oid = fields .get(2) .expect("match should get oid") .as_bytes() .to_os_str() .expect("oid field verified as hex digits, should be valid OsStr"); let path = fields.get(3).expect("match should get path").as_bytes().as_bstr(); let is_in_fuzz_dir = path.split_str("/").any(|component| component == b"fuzz"); match mode { b"100644" if !is_in_fuzz_dir && blob_has_shebang(root, oid)? => { println!("mode -x but has shebang: {path:?}"); Ok(true) } b"100755" if !blob_has_shebang(root, oid)? => { println!("mode +x but no shebang: {path:?}"); Ok(true) } _ => Ok(false), } } fn blob_has_shebang(root: &OsStr, oid: &OsStr) -> anyhow::Result<bool> { let mut buf = [0u8; 2]; let mut child = git_on(root) .args(["cat-file", "blob"]) .arg(oid) .stdout(Stdio::piped()) .spawn() .context("Can't start `git` subprocess to read blob")?; let mut stdout = child.stdout.take().expect("should have captured stdout"); let count = stdout.read(&mut buf).context("Error reading data from blob")?; drop(stdout); // Let the pipe break rather than waiting for the rest of the blob. // TODO: Maybe check status? On Unix, it should be 0 or SIGPIPE. Not sure about Windows. _ = child.wait().context("Failure running `git` subprocess to read blob")?; Ok(&buf[..count] == b"#!") }}
pub(super) mod function {
use std::{
ffi::{OsStr, OsString},
io::{BufRead, BufReader, Read},
process::{Command, Stdio},
};
use anyhow::{Context, bail};
use gix::bstr::ByteSlice;
use regex::bytes::Regex;
use std::sync::LazyLock;
pub fn check_mode() -> anyhow::Result<()> {
let root = find_root()?;
let mut any_mismatch = false;
let mut child = git_on(&root)
.args(["ls-files", "-sz", "--", "*.sh"])
.stdout(Stdio::piped())
.spawn()
.context("Can't start `git` subprocess to list index")?;
let stdout = child.stdout.take().expect("should have captured stdout");
for result in BufReader::new(stdout).split(0) {
let record = result.context(r"Can't read '\0'-terminated record")?;
any_mismatch |= check_for_mismatch(&root, &record)?;
}
let status = child.wait().context("Failure running `git` subprocess to list index")?;
if !status.success() {
bail!("`git` subprocess to list index did not complete successfully");
}
if any_mismatch {
bail!("Mismatch found - scan completed, finding at least one `#!` vs. `+x` mismatch");
}
Ok(())
}
/// Find the top-level directory of the current repository working tree.
fn find_root() -> anyhow::Result<OsString> {
let output = Command::new(gix::path::env::exe_invocation())
.args(["rev-parse", "--show-toplevel"])
.output()
.context("Can't run `git` to find worktree root")?;
if !output.status.success() {
bail!("`git` failed to find worktree root");
}
let root = output
.stdout
.strip_suffix(b"\n")
.context("Can't parse worktree root")?
.to_os_str()?
.to_owned();
Ok(root)
}
/// Prepare a `git` command, passing `root` as an operand to `-C`.
///
/// This is suitable when `git` gave us the path `root`. Then it should already be in a form
/// where `git -C` will be able to use it, without alteration, regardless of the platform.
/// (Otherwise, it may be preferable to set `root` as the `cwd` of the `git` process instead.)
fn git_on(root: &OsStr) -> Command {
let mut cmd = Command::new(gix::path::env::exe_invocation());
cmd.arg("-C").arg(root);
cmd
}
/// On mismatch, report it and return `Some(true)`.
fn check_for_mismatch(root: &OsStr, record: &[u8]) -> anyhow::Result<bool> {
static RECORD_REGEX: LazyLock<Regex> = LazyLock::new(|| {
let pattern = r"(?-u)\A([0-7]+) ([[:xdigit:]]+) [[:digit:]]+\t(.+)\z";
Regex::new(pattern).expect("regex should be valid")
});
let fields = RECORD_REGEX.captures(record).context("Malformed record from `git`")?;
let mode = fields.get(1).expect("match should get mode").as_bytes();
let oid = fields
.get(2)
.expect("match should get oid")
.as_bytes()
.to_os_str()
.expect("oid field verified as hex digits, should be valid OsStr");
let path = fields.get(3).expect("match should get path").as_bytes().as_bstr();
let is_in_fuzz_dir = path.split_str("/").any(|component| component == b"fuzz");
match mode {
b"100644" if !is_in_fuzz_dir && blob_has_shebang(root, oid)? => {
println!("mode -x but has shebang: {path:?}");
Ok(true)
}
b"100755" if !blob_has_shebang(root, oid)? => {
println!("mode +x but no shebang: {path:?}");
Ok(true)
}
_ => Ok(false),
}
}
fn blob_has_shebang(root: &OsStr, oid: &OsStr) -> anyhow::Result<bool> {
let mut buf = [0u8; 2];
let mut child = git_on(root)
.args(["cat-file", "blob"])
.arg(oid)
.stdout(Stdio::piped())
.spawn()
.context("Can't start `git` subprocess to read blob")?;
let mut stdout = child.stdout.take().expect("should have captured stdout");
let count = stdout.read(&mut buf).context("Error reading data from blob")?;
drop(stdout); // Let the pipe break rather than waiting for the rest of the blob.
// TODO: Maybe check status? On Unix, it should be 0 or SIGPIPE. Not sure about Windows.
_ = child.wait().context("Failure running `git` subprocess to read blob")?;
Ok(&buf[..count] == b"#!")
}
}