use crate::config::parse_bool;
use crate::diagnostics::{self, DiagnosticSink};
#[cfg(unix)]
use std::fs;
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub const PERM_UMASK: i32 = 0;
const OLD_PERM_GROUP: i32 = 1;
const OLD_PERM_EVERYBODY: i32 = 2;
pub const PERM_GROUP: i32 = 0o660;
pub const PERM_EVERYBODY: i32 = 0o664;
pub fn git_config_perm(var: &str, value: &str) -> Result<i32, String> {
git_config_perm_with_diagnostics(var, value, None)
}
pub fn git_config_perm_with_diagnostics(
var: &str,
value: &str,
diagnostics: Option<&dyn DiagnosticSink>,
) -> Result<i32, String> {
let value = value.trim();
if value.eq_ignore_ascii_case("umask") {
return Ok(PERM_UMASK);
}
if value.eq_ignore_ascii_case("group") {
return Ok(PERM_GROUP);
}
if value.eq_ignore_ascii_case("all")
|| value.eq_ignore_ascii_case("world")
|| value.eq_ignore_ascii_case("everybody")
{
return Ok(PERM_EVERYBODY);
}
let (octal_prefix, rest) = split_octal_prefix(value);
if rest.is_empty() && !octal_prefix.is_empty() {
if let Ok(i) = i32::from_str_radix(octal_prefix, 8) {
return Ok(match i {
PERM_UMASK => PERM_UMASK,
OLD_PERM_GROUP => PERM_GROUP,
OLD_PERM_EVERYBODY => PERM_EVERYBODY,
_ => {
if (i & 0o600) != 0o600 {
return Err(format!(
"problem with core.sharedRepository filemode value (0{i:o}).\n\
The owner of files must always have read and write permissions."
));
}
-(i & 0o666)
}
});
}
}
match parse_bool(value) {
Ok(true) => Ok(PERM_GROUP),
Ok(false) => Ok(PERM_UMASK),
Err(_) => {
if let Some(d) = diagnostics {
d.warn(diagnostics::Warning::BadBooleanConfig {
key: var.to_owned(),
value: value.to_owned(),
});
}
Ok(PERM_UMASK)
}
}
}
fn split_octal_prefix(s: &str) -> (&str, &str) {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() && matches!(bytes[i], b'0'..=b'7') {
i += 1;
}
(&s[..i], &s[i..])
}
#[must_use]
pub fn calc_shared_perm(shared_repo: i32, mode: u32) -> u32 {
let tweak = if shared_repo < 0 {
(-shared_repo) as u32
} else {
shared_repo as u32
};
let mut new_mode = if shared_repo < 0 {
(mode & !0o777) | tweak
} else {
mode | tweak
};
if mode & 0o200 == 0 {
new_mode &= !0o222;
}
if mode & 0o100 != 0 {
new_mode |= (new_mode & 0o444) >> 2;
}
new_mode
}
#[cfg(unix)]
pub fn adjust_shared_perm_path(shared_repo: i32, path: &Path) -> std::io::Result<()> {
if shared_repo == 0 {
return Ok(());
}
let meta = fs::symlink_metadata(path)?;
if meta.file_type().is_symlink() {
return Ok(());
}
let old_mode = meta.permissions().mode();
let mut new_mode = calc_shared_perm(shared_repo, old_mode);
if meta.is_dir() {
new_mode |= (new_mode & 0o444) >> 2;
if shared_repo < 0 {
const S_ISGID: u32 = 0o002000;
if (new_mode & 0o060) != 0 {
new_mode |= S_ISGID;
}
}
}
let new_perm = fs::Permissions::from_mode(new_mode & 0o7777);
if (old_mode & 0o7777) != (new_mode & 0o7777) {
fs::set_permissions(path, new_perm)?;
}
Ok(())
}
#[cfg(not(unix))]
pub fn adjust_shared_perm_path(_shared_repo: i32, _path: &Path) -> std::io::Result<()> {
Ok(())
}
pub fn shared_repository_from_config_value(raw: Option<&str>) -> Result<i32, String> {
let Some(v) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
return Ok(PERM_UMASK);
};
git_config_perm("core.sharedRepository", v)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::{CollectingDiagnostics, Warning};
#[test]
fn git_config_perm_parses_git_shared_variants() {
assert_eq!(
git_config_perm("core.sharedRepository", "umask").expect("umask"),
PERM_UMASK
);
assert_eq!(
git_config_perm("core.sharedRepository", "group").expect("group"),
PERM_GROUP
);
for v in ["all", "world", "everybody", "ALL"] {
assert_eq!(
git_config_perm("core.sharedRepository", v).expect(v),
PERM_EVERYBODY
);
}
assert_eq!(
git_config_perm("core.sharedRepository", "true").expect("true"),
PERM_GROUP
);
assert_eq!(
git_config_perm("core.sharedRepository", "false").expect("false"),
PERM_UMASK
);
let neg = git_config_perm("core.sharedRepository", "0660").expect("0660");
assert!(neg < 0);
assert_eq!(
git_config_perm("core.sharedRepository", "1").expect("1"),
PERM_GROUP
);
assert_eq!(
git_config_perm("core.sharedRepository", "2").expect("2"),
PERM_EVERYBODY
);
assert!(git_config_perm("core.sharedRepository", "0444").is_err());
}
#[test]
fn git_config_perm_bad_bool_emits_diagnostic() {
let cap = CollectingDiagnostics::new();
assert_eq!(
git_config_perm_with_diagnostics("core.sharedRepository", "maybe", Some(&cap))
.expect("fallback"),
PERM_UMASK
);
assert!(cap
.warnings()
.iter()
.any(|w| matches!(w, Warning::BadBooleanConfig { .. })));
}
#[test]
fn split_octal_prefix_and_calc_shared_perm() {
assert_eq!(split_octal_prefix("0660x"), ("0660", "x"));
assert_eq!(split_octal_prefix("abc"), ("", "abc"));
let mode = 0o100644;
assert_eq!(calc_shared_perm(PERM_GROUP, mode) & 0o777, 0o664);
let neg = git_config_perm("core.sharedRepository", "0660").expect("0660");
let adjusted = calc_shared_perm(neg, mode);
assert_ne!(adjusted & 0o777, 0);
assert_eq!(calc_shared_perm(PERM_UMASK, mode), mode);
}
#[test]
fn shared_repository_from_config_value_empty() {
assert_eq!(
shared_repository_from_config_value(None).expect("none"),
PERM_UMASK
);
assert_eq!(
shared_repository_from_config_value(Some(" ")).expect("blank"),
PERM_UMASK
);
assert_eq!(
shared_repository_from_config_value(Some("group")).expect("group"),
PERM_GROUP
);
}
#[cfg(unix)]
#[test]
fn adjust_shared_perm_path_updates_file_mode() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let file = dir.path().join("obj");
std::fs::write(&file, b"x")?;
let shared = git_config_perm("core.sharedRepository", "0660").expect("0660");
adjust_shared_perm_path(shared, &file)?;
let sub = dir.path().join("sub");
std::fs::create_dir(&sub)?;
adjust_shared_perm_path(shared, &sub)?;
let link = dir.path().join("link");
std::os::unix::fs::symlink(&file, &link)?;
adjust_shared_perm_path(shared, &link)?;
adjust_shared_perm_path(0, &file)?;
Ok(())
}
}