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 super::repo_read_config_typed::{entries_are_accepted, environment_is_accepted, outer_scopes_are_accepted};
23use std::ffi::OsString;
24use std::path::{Path, PathBuf};
25
26const PLAIN_ENVIRONMENT_PREFIXES: [&str; 19] = [
28 "GIT_AUTHOR_",
29 "GIT_COMMITTER_",
30 "GIT_TERMINAL_PROMPT",
31 "GIT_ASKPASS",
32 "GIT_PAGER",
33 "GIT_EDITOR",
34 "GIT_SEQUENCE_EDITOR",
35 "GIT_EXEC_PATH",
36 "GIT_SSH",
37 "GIT_ALLOW_PROTOCOL",
38 "GIT_LFS_",
39 "GIT_CONFIG_NOSYSTEM",
40 "GIT_CONFIG_GLOBAL",
41 "GIT_CONFIG_SYSTEM",
42 "GIT_CONFIG_COUNT",
43 "GIT_CONFIG_KEY_",
44 "GIT_CONFIG_VALUE_",
45 "GIT_MERGE_AUTOEDIT",
46 "GIT_OPTIONAL_LOCKS",
47];
48
49const SMALL_FILE_LIMIT: u64 = 4096;
51
52const ENV_CONFIG_ENTRY_LIMIT: u64 = 1024;
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum ObjectFormat {
58 Sha1,
60 Sha256,
62}
63
64impl ObjectFormat {
65 pub fn name(self) -> &'static str {
67 return match self {
68 ObjectFormat::Sha1 => "sha1",
69 ObjectFormat::Sha256 => "sha256",
70 };
71 }
72
73 pub fn hex_length(self) -> usize {
75 return match self {
76 ObjectFormat::Sha1 => 40,
77 ObjectFormat::Sha256 => 64,
78 };
79 }
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct RepoLayout {
93 pub git_dir: PathBuf,
95 pub common_dir: PathBuf,
97 pub worktree_root: PathBuf,
99 pub prefix: Vec<u8>,
101 pub object_format: ObjectFormat,
103}
104
105fn env_key_moves_discovery(key: &str) -> bool {
107 let lowered: String = key.to_ascii_lowercase();
108 return lowered == "core.bare" || lowered == "core.worktree" || lowered.starts_with("include");
109}
110
111fn env_config_is_plain(variables: &[(OsString, OsString)]) -> bool {
113 return env_config_avoids(variables, &env_key_moves_discovery);
114}
115
116pub fn env_config_avoids(variables: &[(OsString, OsString)], declines: &dyn Fn(&str) -> bool) -> bool {
127 let Some(count_text) = environment_value(variables, COUNT_VARIABLE) else {
128 return true;
129 };
130 let Some(count) = parse_config_count(count_text.as_os_str()) else {
131 return false;
132 };
133 if count > ENV_CONFIG_ENTRY_LIMIT {
134 return false;
135 }
136 for index in 0..count {
137 let Some(key) = environment_value(variables, format!("GIT_CONFIG_KEY_{index}").as_str()) else {
138 return false;
139 };
140 match key.to_str() {
141 Some(text) if !declines(text) => {}
142 _ => return false,
143 }
144 }
145 return true;
146}
147
148fn environment_is_plain(variables: &[(OsString, OsString)]) -> bool {
150 for (name, _) in variables {
151 let Some(text) = name.to_str() else {
152 continue;
153 };
154 if text.starts_with("GIT_") && !PLAIN_ENVIRONMENT_PREFIXES.iter().any(|prefix| { return text.starts_with(prefix); }) {
155 return false;
156 }
157 }
158 return env_config_is_plain(variables);
159}
160
161fn effective_environment(overlay: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
163 let mut variables: Vec<(OsString, OsString)> = std::env::vars_os()
164 .filter(|(name, _)| { return !overlay.iter().any(|(added, _)| { return added == name; }); })
165 .collect();
166 variables.extend(overlay.iter().cloned());
167 return variables;
168}
169
170fn start_directory(global_prefix: &[OsString]) -> Option<PathBuf> {
173 let mut directory: PathBuf = std::env::current_dir().ok()?;
174 let mut options = global_prefix.iter();
175 while let Some(option) = options.next() {
176 if option != "-C" {
177 return None;
178 }
179 let path: &OsString = options.next()?;
180 if !path.is_empty() {
181 directory = directory.join(path);
182 }
183 if !std::fs::metadata(&directory).is_ok_and(|found| { return found.is_dir(); }) {
185 return None;
186 }
187 }
188 return std::fs::canonicalize(directory).ok();
189}
190
191fn small_file(path: &Path) -> Option<String> {
193 let bytes: Vec<u8> = read_regular_file(path, SMALL_FILE_LIMIT).ok()?;
194 return String::from_utf8(bytes).ok();
195}
196
197fn has_git_directory_entries(directory: &Path) -> bool {
199 return directory.join("HEAD").exists() && directory.join("objects").is_dir() && directory.join("refs").is_dir();
200}
201
202fn head_is_valid(text: &str, format: ObjectFormat) -> bool {
204 let Some(line) = text.strip_suffix('\n') else {
205 return false;
206 };
207 if let Some(name) = line.strip_prefix("ref: ") {
208 return name.starts_with("refs/") && !name.contains('\n');
209 }
210 return line.len() == format.hex_length() && line.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
211}
212
213#[cfg(unix)]
215fn owned_by_current_user(path: &Path) -> bool {
216 use std::os::unix::fs::MetadataExt;
217 let Ok(metadata) = std::fs::symlink_metadata(path) else {
218 return false;
219 };
220 return metadata.uid() == unsafe { libc::geteuid() };
222}
223
224#[cfg(not(unix))]
226fn owned_by_current_user(_path: &Path) -> bool {
227 return false;
228}
229
230#[cfg(unix)]
232fn device_of(path: &Path) -> Option<u64> {
233 use std::os::unix::fs::MetadataExt;
234 return std::fs::metadata(path).ok().map(|metadata| { return metadata.dev(); });
235}
236
237#[cfg(not(unix))]
239fn device_of(_path: &Path) -> Option<u64> {
240 return None;
241}
242
243fn dot_git_target(worktree: &Path) -> Option<Option<(PathBuf, bool)>> {
246 let dot_git: PathBuf = worktree.join(".git");
247 let metadata = match std::fs::symlink_metadata(&dot_git) {
248 Ok(found) => found,
249 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(None),
250 Err(_) => return None,
251 };
252 if metadata.is_dir() {
253 return Some(Some((dot_git, false)));
254 }
255 if !metadata.is_file() {
256 return None;
257 }
258 let text: String = small_file(dot_git.as_path())?;
259 let target: &str = text.strip_prefix("gitdir: ")?.strip_suffix('\n')?;
260 if target.is_empty() || target.contains('\n') {
261 return None;
262 }
263 return Some(Some((worktree.join(target), true)));
264}
265
266fn object_format_of(entries: &[ConfigEntry]) -> Option<ObjectFormat> {
276 let version: Option<&[u8]> = match last_entry(entries, "core.repositoryformatversion") {
278 Some(entry) => Some(entry.value.as_deref()?),
279 None => None,
280 };
281 let version_one: bool = match version {
282 None | Some(b"0") => false,
283 Some(b"1") => true,
284 Some(_) => return None,
285 };
286 let mut format: ObjectFormat = ObjectFormat::Sha1;
287 for entry in entries.iter().filter(|entry| { return entry.key.starts_with("extensions."); }) {
288 if !version_one {
289 return None;
290 }
291 let value: &[u8] = entry.value.as_deref().unwrap_or_default();
292 match (entry.key.as_str(), value) {
293 ("extensions.noop", _) => {}
294 ("extensions.preciousobjects", _) if parse_bool(entry.value.as_deref()).is_some() => {}
295 ("extensions.objectformat", b"sha1") => format = ObjectFormat::Sha1,
296 ("extensions.objectformat", b"sha256") => format = ObjectFormat::Sha256,
297 ("extensions.refstorage", b"files") => {}
298 _ => return None,
299 }
300 }
301 return Some(format);
302}
303
304fn finish(start: &Path, worktree: &Path, entry: (PathBuf, bool), scope: &ScopeEnvironment) -> Option<RepoLayout> {
314 let (target, from_file) = entry;
315 let git_dir: PathBuf = std::fs::canonicalize(target).ok()?;
316 let dot_git: PathBuf = worktree.join(".git");
317 let owners_ok: bool = owned_by_current_user(worktree)
318 && owned_by_current_user(git_dir.as_path())
319 && (!from_file || owned_by_current_user(dot_git.as_path()));
320 if !owners_ok {
321 return None;
322 }
323 let common_dir: PathBuf = match small_file(git_dir.join("commondir").as_path()) {
324 Some(text) => std::fs::canonicalize(git_dir.join(text.strip_suffix('\n')?)).ok()?,
325 None if git_dir.join("commondir").exists() => return None,
326 None => git_dir.clone(),
327 };
328 if git_dir.join("reftable").exists() || common_dir.join("reftable").exists() || !has_git_directory_entries_split(&git_dir, &common_dir) {
329 return None;
330 }
331 let config_bytes: Vec<u8> = read_regular_file(common_dir.join("config").as_path(), CONFIG_TEXT_LIMIT as u64).ok()?;
332 let entries: Vec<ConfigEntry> = parse_config_text(config_bytes.as_slice())?;
333 let object_format: ObjectFormat = object_format_of(entries.as_slice())?;
334
335 let discoverable: Vec<ConfigEntry> = entries
337 .iter()
338 .filter(|candidate| { return !(candidate.key == "core.bare" && parse_bool(candidate.value.as_deref()) == Some(false)); })
339 .cloned()
340 .collect();
341 let local_ok: bool = entries_avoid_keys(discoverable.as_slice(), common_dir.join("config").as_path(), scope.home.as_deref(), 0, &discovery_key)?;
342 let worktree_config: bool = last_entry(entries.as_slice(), "extensions.worktreeconfig").is_some();
343 if !local_ok || worktree_config || !outer_scopes_avoid_keys(scope, &discovery_key)? {
344 return None;
345 }
346 let typed_ok: bool = entries_are_accepted(entries.as_slice(), common_dir.join("config").as_path(), scope.home.as_deref(), 0)? && outer_scopes_are_accepted(scope)?;
349 if !typed_ok {
350 return None;
351 }
352 let head: String = small_file(git_dir.join("HEAD").as_path())?;
353 if !head_is_valid(head.as_str(), object_format) {
354 return None;
355 }
356 let relative: &Path = start.strip_prefix(worktree).ok()?;
357 let mut prefix: Vec<u8> = Vec::new();
358 for component in relative.components() {
359 prefix.extend_from_slice(component.as_os_str().as_encoded_bytes());
360 prefix.push(b'/');
361 }
362 return Some(RepoLayout { git_dir, common_dir, worktree_root: worktree.to_path_buf(), prefix, object_format });
363}
364
365fn has_git_directory_entries_split(git_dir: &Path, common_dir: &Path) -> bool {
367 return git_dir.join("HEAD").exists() && common_dir.join("objects").is_dir() && common_dir.join("refs").is_dir();
368}
369
370pub fn read_layout(global_prefix: &[OsString], overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
382 if !enabled(overlay) {
383 return None;
384 }
385 let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
386 if !environment_is_plain(variables.as_slice()) || !environment_is_accepted(variables.as_slice()) {
387 return None;
388 }
389 let start: PathBuf = start_directory(global_prefix)?;
390 return walk_up(start.as_path(), &ScopeEnvironment::from_variables(variables.as_slice()));
391}
392
393pub fn read_layout_with(start: &Path, variables: &[(OsString, OsString)], scope: &ScopeEnvironment) -> Option<RepoLayout> {
403 if !environment_is_plain(variables) || !environment_is_accepted(variables) {
404 return None;
405 }
406 return walk_up(std::fs::canonicalize(start).ok()?.as_path(), scope);
407}
408
409pub fn read_layout_in(directory: &Path, overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
419 if !enabled(overlay) {
420 return None;
421 }
422 let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
423 return read_layout_with(directory, variables.as_slice(), &ScopeEnvironment::from_variables(variables.as_slice()));
424}
425
426fn walk_up(start: &Path, scope: &ScopeEnvironment) -> Option<RepoLayout> {
428 let mut directory: PathBuf = start.to_path_buf();
429 loop {
430 if let Some(entry) = dot_git_target(directory.as_path())? {
431 return finish(start, directory.as_path(), entry, scope);
432 }
433 if has_git_directory_entries(directory.as_path()) {
434 return None;
435 }
436 let parent: PathBuf = directory.parent()?.to_path_buf();
437 if device_of(parent.as_path())? != device_of(directory.as_path())? {
438 return None;
439 }
440 directory = parent;
441 }
442}
443
444pub fn git_path(layout: &RepoLayout, name: &str) -> Option<PathBuf> {
456 const KNOWN: [&str; 9] = [
457 "index",
458 "objects",
459 "HEAD",
460 "MERGE_HEAD",
461 "CHERRY_PICK_HEAD",
462 "REVERT_HEAD",
463 "cli-git-transactions",
464 "packed-refs",
465 "config",
466 ];
467 if layout.git_dir != layout.common_dir || !KNOWN.contains(&name) {
468 return None;
469 }
470 let path: PathBuf = layout.git_dir.join(name);
471 if std::fs::symlink_metadata(path.as_path()).is_ok_and(|metadata| { return metadata.file_type().is_symlink(); }) {
473 return None;
474 }
475 return Some(path);
476}
477
478#[cfg(test)]
479#[path = "repo_read_layout_tests.rs"]
480mod tests;