1use super::child_environment::{COUNT_VARIABLE, environment_value, parse_config_count};
18use super::private_storage::read_regular_file;
19use super::repo_read_hook::enabled;
20use super::repo_read_config_scan::{ScopeEnvironment, discovery_key, entries_avoid_keys, outer_scopes_avoid_keys};
21use super::repo_read_config_text::{CONFIG_TEXT_LIMIT, ConfigEntry, last_entry, parse_bool, parse_config_text};
22use std::ffi::OsString;
23use std::path::{Path, PathBuf};
24
25const PLAIN_ENVIRONMENT_PREFIXES: [&str; 19] = [
27 "GIT_AUTHOR_",
28 "GIT_COMMITTER_",
29 "GIT_TERMINAL_PROMPT",
30 "GIT_ASKPASS",
31 "GIT_PAGER",
32 "GIT_EDITOR",
33 "GIT_SEQUENCE_EDITOR",
34 "GIT_EXEC_PATH",
35 "GIT_SSH",
36 "GIT_ALLOW_PROTOCOL",
37 "GIT_LFS_",
38 "GIT_CONFIG_NOSYSTEM",
39 "GIT_CONFIG_GLOBAL",
40 "GIT_CONFIG_SYSTEM",
41 "GIT_CONFIG_COUNT",
42 "GIT_CONFIG_KEY_",
43 "GIT_CONFIG_VALUE_",
44 "GIT_MERGE_AUTOEDIT",
45 "GIT_OPTIONAL_LOCKS",
46];
47
48const SMALL_FILE_LIMIT: u64 = 4096;
50
51const ENV_CONFIG_ENTRY_LIMIT: u64 = 1024;
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ObjectFormat {
57 Sha1,
59 Sha256,
61}
62
63impl ObjectFormat {
64 pub fn name(self) -> &'static str {
66 return match self {
67 ObjectFormat::Sha1 => "sha1",
68 ObjectFormat::Sha256 => "sha256",
69 };
70 }
71
72 pub fn hex_length(self) -> usize {
74 return match self {
75 ObjectFormat::Sha1 => 40,
76 ObjectFormat::Sha256 => 64,
77 };
78 }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct RepoLayout {
92 pub git_dir: PathBuf,
94 pub common_dir: PathBuf,
96 pub worktree_root: PathBuf,
98 pub prefix: Vec<u8>,
100 pub object_format: ObjectFormat,
102}
103
104fn env_key_moves_discovery(key: &str) -> bool {
106 let lowered: String = key.to_ascii_lowercase();
107 return lowered == "core.bare" || lowered == "core.worktree" || lowered.starts_with("include");
108}
109
110fn env_config_is_plain(variables: &[(OsString, OsString)]) -> bool {
112 return env_config_avoids(variables, &env_key_moves_discovery);
113}
114
115pub fn env_config_avoids(variables: &[(OsString, OsString)], declines: &dyn Fn(&str) -> bool) -> bool {
126 let Some(count_text) = environment_value(variables, COUNT_VARIABLE) else {
127 return true;
128 };
129 let Some(count) = parse_config_count(count_text.as_os_str()) else {
130 return false;
131 };
132 if count > ENV_CONFIG_ENTRY_LIMIT {
133 return false;
134 }
135 for index in 0..count {
136 let Some(key) = environment_value(variables, format!("GIT_CONFIG_KEY_{index}").as_str()) else {
137 return false;
138 };
139 match key.to_str() {
140 Some(text) if !declines(text) => {}
141 _ => return false,
142 }
143 }
144 return true;
145}
146
147fn environment_is_plain(variables: &[(OsString, OsString)]) -> bool {
149 for (name, _) in variables {
150 let Some(text) = name.to_str() else {
151 continue;
152 };
153 if text.starts_with("GIT_") && !PLAIN_ENVIRONMENT_PREFIXES.iter().any(|prefix| { return text.starts_with(prefix); }) {
154 return false;
155 }
156 }
157 return env_config_is_plain(variables);
158}
159
160fn effective_environment(overlay: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
162 let mut variables: Vec<(OsString, OsString)> = std::env::vars_os()
163 .filter(|(name, _)| { return !overlay.iter().any(|(added, _)| { return added == name; }); })
164 .collect();
165 variables.extend(overlay.iter().cloned());
166 return variables;
167}
168
169fn start_directory(global_prefix: &[OsString]) -> Option<PathBuf> {
172 let mut directory: PathBuf = std::env::current_dir().ok()?;
173 let mut options = global_prefix.iter();
174 while let Some(option) = options.next() {
175 if option != "-C" {
176 return None;
177 }
178 let path: &OsString = options.next()?;
179 if !path.is_empty() {
180 directory = directory.join(path);
181 }
182 if !std::fs::metadata(&directory).is_ok_and(|found| { return found.is_dir(); }) {
184 return None;
185 }
186 }
187 return std::fs::canonicalize(directory).ok();
188}
189
190fn small_file(path: &Path) -> Option<String> {
192 let bytes: Vec<u8> = read_regular_file(path, SMALL_FILE_LIMIT).ok()?;
193 return String::from_utf8(bytes).ok();
194}
195
196fn has_git_directory_entries(directory: &Path) -> bool {
198 return directory.join("HEAD").exists() && directory.join("objects").is_dir() && directory.join("refs").is_dir();
199}
200
201fn head_is_valid(text: &str, format: ObjectFormat) -> bool {
203 let Some(line) = text.strip_suffix('\n') else {
204 return false;
205 };
206 if let Some(name) = line.strip_prefix("ref: ") {
207 return name.starts_with("refs/") && !name.contains('\n');
208 }
209 return line.len() == format.hex_length() && line.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
210}
211
212#[cfg(unix)]
214fn owned_by_current_user(path: &Path) -> bool {
215 use std::os::unix::fs::MetadataExt;
216 let Ok(metadata) = std::fs::symlink_metadata(path) else {
217 return false;
218 };
219 return metadata.uid() == unsafe { libc::geteuid() };
221}
222
223#[cfg(not(unix))]
225fn owned_by_current_user(_path: &Path) -> bool {
226 return false;
227}
228
229#[cfg(unix)]
231fn device_of(path: &Path) -> Option<u64> {
232 use std::os::unix::fs::MetadataExt;
233 return std::fs::metadata(path).ok().map(|metadata| { return metadata.dev(); });
234}
235
236#[cfg(not(unix))]
238fn device_of(_path: &Path) -> Option<u64> {
239 return None;
240}
241
242fn dot_git_target(worktree: &Path) -> Option<Option<(PathBuf, bool)>> {
245 let dot_git: PathBuf = worktree.join(".git");
246 let metadata = match std::fs::symlink_metadata(&dot_git) {
247 Ok(found) => found,
248 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(None),
249 Err(_) => return None,
250 };
251 if metadata.is_dir() {
252 return Some(Some((dot_git, false)));
253 }
254 if !metadata.is_file() {
255 return None;
256 }
257 let text: String = small_file(dot_git.as_path())?;
258 let target: &str = text.strip_prefix("gitdir: ")?.strip_suffix('\n')?;
259 if target.is_empty() || target.contains('\n') {
260 return None;
261 }
262 return Some(Some((worktree.join(target), true)));
263}
264
265fn object_format_of(entries: &[ConfigEntry]) -> Option<ObjectFormat> {
275 let version: Option<&[u8]> = last_entry(entries, "core.repositoryformatversion").and_then(|entry| { return entry.value.as_deref(); });
276 let version_one: bool = match version {
277 None | Some(b"0") => false,
278 Some(b"1") => true,
279 Some(_) => return None,
280 };
281 let mut format: ObjectFormat = ObjectFormat::Sha1;
282 for entry in entries.iter().filter(|entry| { return entry.key.starts_with("extensions."); }) {
283 if !version_one {
284 return None;
285 }
286 let value: &[u8] = entry.value.as_deref().unwrap_or_default();
287 match (entry.key.as_str(), value) {
288 ("extensions.noop", _) => {}
289 ("extensions.preciousobjects", _) if parse_bool(entry.value.as_deref()).is_some() => {}
290 ("extensions.objectformat", b"sha1") => format = ObjectFormat::Sha1,
291 ("extensions.objectformat", b"sha256") => format = ObjectFormat::Sha256,
292 ("extensions.refstorage", b"files") => {}
293 _ => return None,
294 }
295 }
296 return Some(format);
297}
298
299fn finish(start: &Path, worktree: &Path, entry: (PathBuf, bool), scope: &ScopeEnvironment) -> Option<RepoLayout> {
309 let (target, from_file) = entry;
310 let git_dir: PathBuf = std::fs::canonicalize(target).ok()?;
311 let dot_git: PathBuf = worktree.join(".git");
312 let owners_ok: bool = owned_by_current_user(worktree)
313 && owned_by_current_user(git_dir.as_path())
314 && (!from_file || owned_by_current_user(dot_git.as_path()));
315 if !owners_ok {
316 return None;
317 }
318 let common_dir: PathBuf = match small_file(git_dir.join("commondir").as_path()) {
319 Some(text) => std::fs::canonicalize(git_dir.join(text.strip_suffix('\n')?)).ok()?,
320 None if git_dir.join("commondir").exists() => return None,
321 None => git_dir.clone(),
322 };
323 if git_dir.join("reftable").exists() || common_dir.join("reftable").exists() || !has_git_directory_entries_split(&git_dir, &common_dir) {
324 return None;
325 }
326 let config_bytes: Vec<u8> = read_regular_file(common_dir.join("config").as_path(), CONFIG_TEXT_LIMIT as u64).ok()?;
327 let entries: Vec<ConfigEntry> = parse_config_text(config_bytes.as_slice())?;
328 let object_format: ObjectFormat = object_format_of(entries.as_slice())?;
329
330 let discoverable: Vec<ConfigEntry> = entries
332 .iter()
333 .filter(|candidate| { return !(candidate.key == "core.bare" && parse_bool(candidate.value.as_deref()) == Some(false)); })
334 .cloned()
335 .collect();
336 let local_ok: bool = entries_avoid_keys(discoverable.as_slice(), common_dir.join("config").as_path(), scope.home.as_deref(), 0, &discovery_key)?;
337 let worktree_config: bool = last_entry(entries.as_slice(), "extensions.worktreeconfig").is_some();
338 if !local_ok || worktree_config || !outer_scopes_avoid_keys(scope, &discovery_key)? {
339 return None;
340 }
341 let head: String = small_file(git_dir.join("HEAD").as_path())?;
342 if !head_is_valid(head.as_str(), object_format) {
343 return None;
344 }
345 let relative: &Path = start.strip_prefix(worktree).ok()?;
346 let mut prefix: Vec<u8> = Vec::new();
347 for component in relative.components() {
348 prefix.extend_from_slice(component.as_os_str().as_encoded_bytes());
349 prefix.push(b'/');
350 }
351 return Some(RepoLayout { git_dir, common_dir, worktree_root: worktree.to_path_buf(), prefix, object_format });
352}
353
354fn has_git_directory_entries_split(git_dir: &Path, common_dir: &Path) -> bool {
356 return git_dir.join("HEAD").exists() && common_dir.join("objects").is_dir() && common_dir.join("refs").is_dir();
357}
358
359pub fn read_layout(global_prefix: &[OsString], overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
371 if !enabled(overlay) || !environment_is_plain(effective_environment(overlay).as_slice()) {
372 return None;
373 }
374 let start: PathBuf = start_directory(global_prefix)?;
375 return walk_up(start.as_path(), &ScopeEnvironment::from_variables(effective_environment(overlay).as_slice()));
376}
377
378pub fn read_layout_with(start: &Path, variables: &[(OsString, OsString)], scope: &ScopeEnvironment) -> Option<RepoLayout> {
388 if !environment_is_plain(variables) {
389 return None;
390 }
391 return walk_up(std::fs::canonicalize(start).ok()?.as_path(), scope);
392}
393
394pub fn read_layout_in(directory: &Path, overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
404 if !enabled(overlay) {
405 return None;
406 }
407 let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
408 return read_layout_with(directory, variables.as_slice(), &ScopeEnvironment::from_variables(variables.as_slice()));
409}
410
411fn walk_up(start: &Path, scope: &ScopeEnvironment) -> Option<RepoLayout> {
413 let mut directory: PathBuf = start.to_path_buf();
414 loop {
415 if let Some(entry) = dot_git_target(directory.as_path())? {
416 return finish(start, directory.as_path(), entry, scope);
417 }
418 if has_git_directory_entries(directory.as_path()) {
419 return None;
420 }
421 let parent: PathBuf = directory.parent()?.to_path_buf();
422 if device_of(parent.as_path())? != device_of(directory.as_path())? {
423 return None;
424 }
425 directory = parent;
426 }
427}
428
429pub fn git_path(layout: &RepoLayout, name: &str) -> Option<PathBuf> {
441 const KNOWN: [&str; 9] = [
442 "index",
443 "objects",
444 "HEAD",
445 "MERGE_HEAD",
446 "CHERRY_PICK_HEAD",
447 "REVERT_HEAD",
448 "cli-git-transactions",
449 "packed-refs",
450 "config",
451 ];
452 if layout.git_dir != layout.common_dir || !KNOWN.contains(&name) {
453 return None;
454 }
455 let path: PathBuf = layout.git_dir.join(name);
456 if std::fs::symlink_metadata(path.as_path()).is_ok_and(|metadata| { return metadata.file_type().is_symlink(); }) {
458 return None;
459 }
460 return Some(path);
461}
462
463#[cfg(test)]
464#[path = "repo_read_layout_tests.rs"]
465mod tests;