use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::config::{ConfigFile, ConfigScope};
use crate::error::Result;
use crate::objects::{parse_commit, parse_tree, ObjectId, ObjectKind, TreeEntry};
use crate::odb::Odb;
use crate::pack::read_pack_index;
use url::{Host, Url};
#[must_use]
pub fn looks_like_command_line_option(s: &str) -> bool {
!s.is_empty() && s.as_bytes().first() == Some(&b'-')
}
#[must_use]
pub fn tree_entry_is_gitmodules_blob(mode: u32, name: &[u8]) -> bool {
if mode == 0o120000 {
return false;
}
let Ok(name_str) = std::str::from_utf8(name) else {
return false;
};
is_hfs_dot_gitmodules(name_str) || is_ntfs_dot_gitmodules(name_str)
}
fn next_hfs_char(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<char> {
loop {
let ch = chars.next()?;
match ch {
'\u{200c}' | '\u{200d}' | '\u{200e}' | '\u{200f}' => continue,
'\u{202a}'..='\u{202e}' => continue,
'\u{206a}'..='\u{206f}' => continue,
'\u{feff}' => continue,
_ => return Some(ch),
}
}
}
fn is_hfs_dot_generic(path: &str, needle: &str) -> bool {
let mut chars = path.chars().peekable();
let mut c = match next_hfs_char(&mut chars) {
Some(x) => x,
None => return false,
};
if c != '.' {
return false;
}
for nc in needle.chars() {
c = match next_hfs_char(&mut chars) {
Some(x) => x,
None => return false,
};
if c as u32 > 127 {
return false;
}
if !c.eq_ignore_ascii_case(&nc) {
return false;
}
}
match next_hfs_char(&mut chars) {
None => true,
Some(ch) if ch == '/' => true,
Some(_) => false,
}
}
fn is_hfs_dot_gitmodules(path: &str) -> bool {
is_hfs_dot_generic(path, "gitmodules")
}
fn only_spaces_and_periods(name: &str, mut i: usize) -> bool {
let b = name.as_bytes();
loop {
let c = *b.get(i).unwrap_or(&0);
if c == 0 || c == b':' {
return true;
}
if c != b' ' && c != b'.' {
return false;
}
i += 1;
}
}
fn is_ntfs_dot_generic(name: &str, dotgit_name: &str, short_prefix: &str) -> bool {
let b = name.as_bytes();
let len = dotgit_name.len();
if !b.is_empty()
&& b[0] == b'.'
&& name.len() > len
&& name[1..1 + len].eq_ignore_ascii_case(dotgit_name)
{
let i = len + 1;
return only_spaces_and_periods(name, i);
}
if b.len() >= 8
&& name[..6].eq_ignore_ascii_case(&dotgit_name[..6])
&& b[6] == b'~'
&& (b[7] >= b'1' && b[7] <= b'4')
{
return only_spaces_and_periods(name, 8);
}
let mut i = 0usize;
let mut saw_tilde = false;
while i < 8 {
let c = *b.get(i).unwrap_or(&0);
if c == 0 {
return false;
}
if saw_tilde {
if !c.is_ascii_digit() {
return false;
}
} else if c == b'~' {
i += 1;
let d = *b.get(i).unwrap_or(&0);
if !(b'1'..=b'9').contains(&d) {
return false;
}
saw_tilde = true;
} else if i >= 6 {
return false;
} else if c & 0x80 != 0 {
return false;
} else {
let sc = short_prefix.as_bytes().get(i).copied().unwrap_or(0);
if (c as char).to_ascii_lowercase() != sc as char {
return false;
}
}
i += 1;
}
only_spaces_and_periods(name, i)
}
fn is_ntfs_dot_gitmodules(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitmodules", "gi7eba")
}
fn is_hfs_dot_gitattributes(path: &str) -> bool {
is_hfs_dot_generic(path, "gitattributes")
}
fn is_ntfs_dot_gitattributes(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitattributes", "gi7d29")
}
fn is_hfs_dot_gitignore(path: &str) -> bool {
is_hfs_dot_generic(path, "gitignore")
}
fn is_ntfs_dot_gitignore(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitignore", "gi250a")
}
fn is_hfs_dot_mailmap(path: &str) -> bool {
is_hfs_dot_generic(path, "mailmap")
}
fn is_ntfs_dot_mailmap(name: &str) -> bool {
is_ntfs_dot_generic(name, "mailmap", "maba30")
}
#[must_use]
pub fn tree_entry_is_gitattributes_blob(mode: u32, name: &[u8]) -> bool {
if mode == 0o120000 {
return false;
}
let Ok(name_str) = std::str::from_utf8(name) else {
return false;
};
is_hfs_dot_gitattributes(name_str) || is_ntfs_dot_gitattributes(name_str)
}
fn is_hfs_or_ntfs_dot_gitmodules(name: &str) -> bool {
is_hfs_dot_gitmodules(name) || is_ntfs_dot_gitmodules(name)
}
fn is_hfs_or_ntfs_dot_gitattributes(name: &str) -> bool {
is_hfs_dot_gitattributes(name) || is_ntfs_dot_gitattributes(name)
}
pub fn fsck_dot_special_tree_pass(
tree_oid: &ObjectId,
data: &[u8],
gitmodules_out: &mut HashSet<ObjectId>,
gitattributes_out: &mut HashSet<ObjectId>,
) -> Result<Vec<DotFsckIssue>> {
let entries = parse_tree(data)?;
let mut issues = Vec::new();
for TreeEntry { mode, name, oid } in entries {
let Ok(name_str) = std::str::from_utf8(&name) else {
continue;
};
let is_symlink = mode == 0o120000;
if is_hfs_or_ntfs_dot_gitmodules(name_str) {
if is_symlink {
issues.push(DotFsckIssue::TreeSymlink {
tree_oid: *tree_oid,
id: "gitmodulesSymlink",
detail: ".gitmodules is a symbolic link",
});
} else {
gitmodules_out.insert(oid);
}
}
if is_hfs_or_ntfs_dot_gitattributes(name_str) {
if is_symlink {
issues.push(DotFsckIssue::TreeSymlink {
tree_oid: *tree_oid,
id: "gitattributesSymlink",
detail: ".gitattributes is a symlink",
});
} else {
gitattributes_out.insert(oid);
}
}
if is_symlink {
if is_hfs_dot_gitignore(name_str) || is_ntfs_dot_gitignore(name_str) {
issues.push(DotFsckIssue::TreeSymlink {
tree_oid: *tree_oid,
id: "gitignoreSymlink",
detail: ".gitignore is a symlink",
});
}
if is_hfs_dot_mailmap(name_str) || is_ntfs_dot_mailmap(name_str) {
issues.push(DotFsckIssue::TreeSymlink {
tree_oid: *tree_oid,
id: "mailmapSymlink",
detail: ".mailmap is a symlink",
});
}
}
let mut slash_rest = name_str;
while let Some(idx) = slash_rest.find('\\') {
let after = &slash_rest[idx + 1..];
if is_ntfs_dot_gitmodules(after) {
if is_symlink {
issues.push(DotFsckIssue::TreeSymlink {
tree_oid: *tree_oid,
id: "gitmodulesSymlink",
detail: ".gitmodules is a symbolic link",
});
} else {
gitmodules_out.insert(oid);
}
}
slash_rest = after;
}
}
Ok(issues)
}
#[derive(Debug, Clone)]
pub enum DotFsckIssue {
TreeSymlink {
tree_oid: ObjectId,
id: &'static str,
detail: &'static str,
},
NonBlobDotFile {
oid: ObjectId,
kind: ObjectKind,
id: &'static str,
detail: &'static str,
},
BlobGitmodules {
blob_oid: ObjectId,
id: &'static str,
detail: String,
},
BlobGitattributes {
blob_oid: ObjectId,
id: &'static str,
detail: &'static str,
},
}
impl DotFsckIssue {
#[must_use]
pub fn format_line(&self) -> String {
match self {
DotFsckIssue::TreeSymlink {
tree_oid,
id,
detail,
} => {
let prefix = if *id == "gitmodulesSymlink" {
"error"
} else {
"warning"
};
format!("{prefix} in tree {}: {}: {}", tree_oid.to_hex(), id, detail)
}
DotFsckIssue::NonBlobDotFile {
oid,
kind,
id,
detail,
} => format!(
"error in {} {}: {}: {}",
kind.as_str(),
oid.to_hex(),
id,
detail
),
DotFsckIssue::BlobGitmodules {
blob_oid,
id,
detail,
} => {
let prefix = if *id == "gitmodulesParse" {
"warning"
} else {
"error"
};
format!("{prefix} in blob {}: {}: {}", blob_oid.to_hex(), id, detail)
}
DotFsckIssue::BlobGitattributes {
blob_oid,
id,
detail,
} => format!("error in blob {}: {}: {}", blob_oid.to_hex(), id, detail),
}
}
#[must_use]
pub fn is_error_severity(&self) -> bool {
!matches!(
self,
DotFsckIssue::BlobGitmodules {
id: "gitmodulesParse",
..
} | DotFsckIssue::TreeSymlink {
id: "gitattributesSymlink" | "gitignoreSymlink" | "mailmapSymlink",
..
}
)
}
}
fn gitmodules_blob_unparseable(data: &[u8]) -> bool {
for raw in data.split(|b| *b == b'\n') {
let line = trim_bytes(raw);
if line.is_empty() || line[0] == b'#' || line[0] == b';' {
continue;
}
if line.first() == Some(&b'[') && !line.contains(&b']') {
return true;
}
}
false
}
fn trim_bytes(mut s: &[u8]) -> &[u8] {
while let Some((&f, r)) = s.split_first() {
if f == b' ' || f == b'\t' {
s = r;
} else {
break;
}
}
while let Some((&l, r)) = s.split_last() {
if l == b' ' || l == b'\t' || l == b'\r' {
s = r;
} else {
break;
}
}
s
}
pub fn fsck_dot_special_object(
oid: &ObjectId,
kind: ObjectKind,
data: &[u8],
gitmodules_oids: &HashSet<ObjectId>,
gitattributes_oids: &HashSet<ObjectId>,
) -> Vec<DotFsckIssue> {
let mut out = Vec::new();
if gitmodules_oids.contains(oid) {
if kind != ObjectKind::Blob {
out.push(DotFsckIssue::NonBlobDotFile {
oid: *oid,
kind,
id: "gitmodulesBlob",
detail: "non-blob found at .gitmodules",
});
return out;
}
if let Some(msg) = validate_gitmodules_blob_line(data) {
let (id, detail) = split_fsck_colon(&msg);
out.push(DotFsckIssue::BlobGitmodules {
blob_oid: *oid,
id,
detail: detail.to_string(),
});
} else {
let text = std::str::from_utf8(data).unwrap_or("");
let strict_bad =
ConfigFile::parse(Path::new(".gitmodules"), text, ConfigScope::Local).is_err();
if strict_bad || gitmodules_blob_unparseable(data) {
out.push(DotFsckIssue::BlobGitmodules {
blob_oid: *oid,
id: "gitmodulesParse",
detail: "could not parse gitmodules blob".to_string(),
});
}
}
}
if gitattributes_oids.contains(oid) {
if kind != ObjectKind::Blob {
out.push(DotFsckIssue::NonBlobDotFile {
oid: *oid,
kind,
id: "gitattributesBlob",
detail: "non-blob found at .gitattributes",
});
return out;
}
if data.len() > ATTR_MAX_FILE_SIZE {
out.push(DotFsckIssue::BlobGitattributes {
blob_oid: *oid,
id: "gitattributesLarge",
detail: ".gitattributes too large to parse",
});
} else {
let mut ptr = 0usize;
while ptr < data.len() {
let rest = &data[ptr..];
let line_end = rest.iter().position(|&b| b == b'\n').unwrap_or(rest.len());
if line_end >= ATTR_MAX_LINE_LENGTH {
out.push(DotFsckIssue::BlobGitattributes {
blob_oid: *oid,
id: "gitattributesLineLength",
detail: ".gitattributes has too long lines to parse",
});
break;
}
ptr += line_end;
if ptr < data.len() && data[ptr] == b'\n' {
ptr += 1;
}
}
}
}
out
}
#[derive(Debug, Default)]
pub struct DotFsckTracker {
pub gitmodules_found: HashSet<ObjectId>,
pub gitmodules_done: HashSet<ObjectId>,
pub gitattributes_found: HashSet<ObjectId>,
pub gitattributes_done: HashSet<ObjectId>,
}
impl DotFsckTracker {
pub fn on_tree(&mut self, tree_oid: &ObjectId, data: &[u8]) -> Result<Vec<DotFsckIssue>> {
fsck_dot_special_tree_pass(
tree_oid,
data,
&mut self.gitmodules_found,
&mut self.gitattributes_found,
)
}
pub fn on_object(
&mut self,
oid: &ObjectId,
kind: ObjectKind,
data: &[u8],
) -> Vec<DotFsckIssue> {
let need_gm = self.gitmodules_found.contains(oid) && !self.gitmodules_done.contains(oid);
let need_ga =
self.gitattributes_found.contains(oid) && !self.gitattributes_done.contains(oid);
if !need_gm && !need_ga {
return Vec::new();
}
if need_gm {
self.gitmodules_done.insert(*oid);
}
if need_ga {
self.gitattributes_done.insert(*oid);
}
fsck_dot_special_object(
oid,
kind,
data,
&self.gitmodules_found,
&self.gitattributes_found,
)
}
pub fn finish_pending(&mut self, odb: &Odb) -> Result<Vec<DotFsckIssue>> {
self.finish_pending_resolve(|oid| odb.read(oid).ok().map(|o| (o.kind, o.data)))
}
pub fn finish_pending_resolve<F>(&mut self, mut resolve: F) -> Result<Vec<DotFsckIssue>>
where
F: FnMut(&ObjectId) -> Option<(ObjectKind, Vec<u8>)>,
{
let mut out = Vec::new();
let pending_gm: Vec<ObjectId> = self
.gitmodules_found
.difference(&self.gitmodules_done)
.copied()
.collect();
let pending_ga: Vec<ObjectId> = self
.gitattributes_found
.difference(&self.gitattributes_done)
.copied()
.collect();
for oid in pending_gm {
self.gitmodules_done.insert(oid);
let Some((kind, data)) = resolve(&oid) else {
continue;
};
out.extend(fsck_dot_special_object(
&oid,
kind,
&data,
&self.gitmodules_found,
&self.gitattributes_found,
));
}
for oid in pending_ga {
if self.gitattributes_done.contains(&oid) {
continue;
}
self.gitattributes_done.insert(oid);
let Some((kind, data)) = resolve(&oid) else {
continue;
};
out.extend(fsck_dot_special_object(
&oid,
kind,
&data,
&self.gitmodules_found,
&self.gitattributes_found,
));
}
Ok(out)
}
}
pub fn verify_packed_dot_special(by_oid: &HashMap<ObjectId, (ObjectKind, Vec<u8>)>) -> Result<()> {
let mut tracker = DotFsckTracker::default();
let mut keys: Vec<ObjectId> = by_oid.keys().copied().collect();
keys.sort();
for oid in keys {
let (kind, data) = &by_oid[&oid];
if *kind == ObjectKind::Tree {
for di in tracker.on_tree(&oid, data)? {
if di.is_error_severity() {
return Err(crate::error::Error::CorruptObject(di.format_line()));
}
}
}
for di in tracker.on_object(&oid, *kind, data) {
if di.is_error_severity() {
return Err(crate::error::Error::CorruptObject(di.format_line()));
}
}
}
for di in tracker.finish_pending_resolve(|id| by_oid.get(id).map(|(k, d)| (*k, d.clone())))? {
if di.is_error_severity() {
return Err(crate::error::Error::CorruptObject(di.format_line()));
}
}
Ok(())
}
fn split_fsck_colon(msg: &str) -> (&'static str, &str) {
let Some((a, b)) = msg.split_once(": ") else {
return ("gitmodules", msg);
};
match a {
"gitmodulesName" => ("gitmodulesName", b),
"gitmodulesUrl" => ("gitmodulesUrl", b),
"gitmodulesPath" => ("gitmodulesPath", b),
"gitmodulesUpdate" => ("gitmodulesUpdate", b),
_ => ("gitmodules", msg),
}
}
pub fn write_gitmodules_cli_option_warnings(
w: &mut dyn Write,
content: &str,
) -> std::io::Result<()> {
if let Ok(config) = ConfigFile::parse(Path::new(".gitmodules"), content, ConfigScope::Local) {
let mut any = false;
for entry in &config.entries {
let key = &entry.key;
let Some(rest) = key.strip_prefix("submodule.") else {
continue;
};
let Some(last_dot) = rest.rfind('.') else {
continue;
};
let var = &rest[last_dot + 1..];
if var != "path" && var != "url" {
continue;
}
let Some(value) = entry.value.as_deref() else {
continue;
};
if looks_like_command_line_option(value) {
writeln!(
w,
"warning: ignoring '{key}' which may be interpreted as a command-line option: {value}"
)?;
any = true;
}
}
if any {
return Ok(());
}
}
let mut subsection: Option<&str> = None;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
subsection = None;
if let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
let inner = inner.trim();
if let Some(rest) = inner.strip_prefix("submodule") {
let rest = rest.trim();
let name = rest
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(rest);
if !name.is_empty() {
subsection = Some(name);
}
}
}
continue;
}
let Some((raw_key, raw_val)) = trimmed.split_once('=') else {
continue;
};
let key = raw_key.trim();
if key != "path" && key != "url" {
continue;
}
let mut val = raw_val.trim();
if val.len() >= 2 && val.starts_with('"') && val.ends_with('"') {
val = &val[1..val.len() - 1];
}
if looks_like_command_line_option(val) {
let key_full = match subsection {
Some(name) => format!("submodule.{name}.{key}"),
None => key.to_string(),
};
writeln!(
w,
"warning: ignoring '{key_full}' which may be interpreted as a command-line option: {val}"
)?;
}
}
Ok(())
}
#[must_use]
pub fn check_submodule_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let b = name.as_bytes();
if b.len() >= 2
&& b[0] == b'.'
&& b[1] == b'.'
&& (b.len() == 2 || b[2] == b'/' || b[2] == b'\\')
{
return false;
}
let mut i = 0usize;
while i < b.len() {
let c = b[i];
i += 1;
if c == b'/' || c == b'\\' {
let j = i;
if b.len() >= j + 2
&& b[j] == b'.'
&& b[j + 1] == b'.'
&& (j + 2 >= b.len() || b[j + 2] == b'/' || b[j + 2] == b'\\')
{
return false;
}
}
}
true
}
fn is_xplatform_dir_sep(b: u8) -> bool {
b == b'/' || b == b'\\'
}
fn starts_with_dot_dot_slash(url: &str) -> bool {
let b = url.as_bytes();
b.len() >= 3 && b[0] == b'.' && b[1] == b'.' && is_xplatform_dir_sep(b[2])
}
fn starts_with_dot_slash(url: &str) -> bool {
let b = url.as_bytes();
b.len() >= 2 && b[0] == b'.' && is_xplatform_dir_sep(b[1])
}
fn submodule_url_is_relative(url: &str) -> bool {
starts_with_dot_slash(url) || starts_with_dot_dot_slash(url)
}
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn percent_decode_git_style(input: &str) -> Option<Vec<u8>> {
let b = input.as_bytes();
let mut out = Vec::with_capacity(b.len());
let mut i = 0usize;
while i < b.len() {
if b[i] == b'%' {
if i + 2 >= b.len() {
return None;
}
let hi = hex_val(b[i + 1])?;
let lo = hex_val(b[i + 2])?;
out.push((hi << 4) | lo);
i += 3;
} else {
out.push(b[i]);
i += 1;
}
}
Some(out)
}
fn count_leading_dotdots(url: &str) -> (usize, &str) {
let mut n = 0usize;
let mut s = url;
loop {
if starts_with_dot_dot_slash(s) {
n += 1;
s = &s[3..];
continue;
}
if starts_with_dot_slash(s) {
s = &s[2..];
continue;
}
break;
}
(n, s)
}
fn url_to_curl_transport_url(url: &str) -> Option<&str> {
url.strip_prefix("http::")
.or_else(|| url.strip_prefix("https::"))
.or_else(|| url.strip_prefix("ftp::"))
.or_else(|| url.strip_prefix("ftps::"))
.or_else(|| {
if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("ftp://")
|| url.starts_with("ftps://")
{
Some(url)
} else {
None
}
})
}
#[must_use]
pub fn check_submodule_url(url: &str) -> bool {
if looks_like_command_line_option(url) {
return false;
}
if submodule_url_is_relative(url) || url.starts_with("git://") {
let Some(decoded) = percent_decode_git_style(url) else {
return false;
};
if decoded.contains(&b'\n') {
return false;
}
let (n, rest) = count_leading_dotdots(url);
if n > 0 {
let rb = rest.as_bytes();
if !rb.is_empty() && (rb[0] == b':' || rb[0] == b'/') {
return false;
}
}
return true;
}
if let Some(curl_url) = url_to_curl_transport_url(url) {
if (curl_url.starts_with("http://") || curl_url.starts_with("https://"))
&& curl_url.contains(":///")
{
return false;
}
let Ok(parsed) = Url::parse(curl_url) else {
return false;
};
if !matches!(
parsed.scheme(),
"http" | "https" | "ftp" | "ftps" | "ws" | "wss"
) {
return false;
}
if parsed.host_str().is_none() {
return false;
}
match parsed.host() {
Some(Host::Domain(d)) if d.contains(':') => return false,
None => return false,
_ => {}
}
if parsed.path().starts_with(':') {
return false;
}
let normalized = parsed.as_str();
let Some(decoded) = percent_decode_git_style(normalized) else {
return false;
};
!decoded.contains(&b'\n')
} else {
true
}
}
pub const ATTR_MAX_LINE_LENGTH: usize = 2048;
pub const ATTR_MAX_FILE_SIZE: usize = 100 * 1024 * 1024;
fn submodule_update_is_command(value: &str) -> bool {
!value.is_empty() && value.starts_with('!')
}
fn raw_gitmodules_submodule_names(content: &str) -> Vec<String> {
let mut out = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.starts_with('[') {
continue;
}
let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) else {
continue;
};
let inner = inner.trim();
let Some(rest) = inner.strip_prefix("submodule") else {
continue;
};
let rest = rest.trim();
let name = rest
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(rest);
if !name.is_empty() {
out.push(name.to_string());
}
}
out
}
pub fn validate_gitmodules_blob_line(data: &[u8]) -> Option<String> {
let Ok(text) = std::str::from_utf8(data) else {
return None;
};
let mut worst: Option<String> = None;
if let Ok(config) = ConfigFile::parse(Path::new(".gitmodules"), text, ConfigScope::Local) {
for entry in &config.entries {
let key = &entry.key;
let Some(rest) = key.strip_prefix("submodule.") else {
continue;
};
let Some(last_dot) = rest.rfind('.') else {
continue;
};
let name = &rest[..last_dot];
let var = &rest[last_dot + 1..];
if !check_submodule_name(name) {
worst.get_or_insert_with(|| {
format!("gitmodulesName: disallowed submodule name: {name}")
});
}
let Some(value) = entry.value.as_deref() else {
continue;
};
match var {
"url" => {
if !check_submodule_url(value) {
worst.get_or_insert_with(|| {
format!("gitmodulesUrl: disallowed submodule url: {value}")
});
}
}
"path" => {
if looks_like_command_line_option(value) {
worst = Some(format!(
"gitmodulesPath: disallowed submodule path: {value}"
));
}
}
"update" => {
if submodule_update_is_command(value) {
worst.get_or_insert_with(|| {
format!(
"gitmodulesUpdate: disallowed submodule update setting: {value}"
)
});
}
}
_ => {}
}
}
}
for name in raw_gitmodules_submodule_names(text) {
if !check_submodule_name(&name) {
worst.get_or_insert_with(|| {
format!("gitmodulesName: disallowed submodule name: {name}")
});
}
}
worst
}
fn collect_gitmodules_blobs_from_tree(
odb: &Odb,
tree_oid: ObjectId,
seen_trees: &mut HashSet<ObjectId>,
) -> Result<HashSet<ObjectId>> {
let mut blobs = HashSet::new();
let mut stack = vec![tree_oid];
while let Some(tid) = stack.pop() {
if !seen_trees.insert(tid) {
continue;
}
let obj = odb.read(&tid)?;
if obj.kind != ObjectKind::Tree {
continue;
}
let entries = parse_tree(&obj.data)?;
for TreeEntry { mode, name, oid } in entries {
if tree_entry_is_gitmodules_blob(mode, &name) {
blobs.insert(oid);
} else if mode == 0o040000 {
stack.push(oid);
}
}
}
Ok(blobs)
}
pub fn verify_gitmodules_for_commit(odb: &Odb, commit_oid: ObjectId) -> Result<Option<String>> {
let obj = odb.read(&commit_oid)?;
if obj.kind != ObjectKind::Commit {
return Ok(None);
}
let commit = parse_commit(&obj.data)?;
let mut seen_trees = HashSet::new();
let blobs = collect_gitmodules_blobs_from_tree(odb, commit.tree, &mut seen_trees)?;
for oid in blobs {
let blob = odb.read(&oid)?;
if blob.kind != ObjectKind::Blob {
continue;
}
if let Some(msg) = validate_gitmodules_blob_line(&blob.data) {
return Ok(Some(format!("{}: {}", oid.to_hex(), msg)));
}
}
Ok(None)
}
pub fn oids_from_copied_object_paths(copied: &[PathBuf]) -> Result<HashSet<ObjectId>> {
let mut out = HashSet::new();
for p in copied {
let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
continue;
};
if name.ends_with(".idx") {
let idx = read_pack_index(p)?;
for e in &idx.entries {
if e.oid.len() == 20 {
if let Ok(oid) = ObjectId::from_bytes(&e.oid) {
out.insert(oid);
}
}
}
continue;
}
if let Some(oid) = object_id_from_loose_object_path(p) {
out.insert(oid);
}
}
Ok(out)
}
fn object_id_from_loose_object_path(path: &Path) -> Option<ObjectId> {
let file_name = path.file_name()?.to_str()?;
if file_name.len() != 38 {
return None;
}
let parent = path.parent()?.file_name()?.to_str()?;
if parent.len() != 2 {
return None;
}
let hex = format!("{parent}{file_name}");
ObjectId::from_hex(&hex).ok()
}