#![cfg(unix)]
use crate::private_storage::read_regular_file;
use std::path::{Path, PathBuf};
const LOOSE_LIMIT: u64 = 4096;
const PACKED_LIMIT: u64 = 256 << 20;
const SYMBOLIC_DEPTH_LIMIT: usize = 5;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RefValue {
Direct(String),
Symbolic(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Lookup {
Found(RefValue),
Missing,
Unsure,
}
pub struct Refs {
directory: PathBuf,
hex_length: usize,
}
pub fn is_plain_ref_name(name: &str) -> bool {
let Some(rest) = name.strip_prefix("refs/") else {
return false;
};
if rest.is_empty() || rest.ends_with('.') || rest.contains("..") {
return false;
}
return rest.split('/').all(|component| {
return !component.is_empty()
&& !component.starts_with('.')
&& !component.ends_with(".lock")
&& component.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'); });
});
}
impl Refs {
pub fn new(directory: &Path, hex_length: usize) -> Refs {
return Refs { directory: directory.to_path_buf(), hex_length };
}
fn is_name(&self, text: &str) -> bool {
return text.len() == self.hex_length
&& text.bytes().all(|byte| { return matches!(byte, b'0'..=b'9' | b'a'..=b'f'); })
&& !text.bytes().all(|byte| { return byte == b'0'; });
}
fn parse_loose(&self, text: &str) -> Option<RefValue> {
let line: &str = text.strip_suffix('\n')?;
if let Some(target) = line.strip_prefix("ref: ") {
return is_plain_ref_name(target).then(|| { return RefValue::Symbolic(target.to_string()); });
}
return self.is_name(line).then(|| { return RefValue::Direct(line.to_string()); });
}
fn packed(&self) -> Option<Vec<(String, String)>> {
return self.packed_named(false);
}
fn packed_named(&self, skip_unplain: bool) -> Option<Vec<(String, String)>> {
let bytes: Vec<u8> = match read_regular_file(self.directory.join("packed-refs").as_path(), PACKED_LIMIT) {
Ok(bytes) => bytes,
Err(crate::private_storage::ReadRefusal::Missing) => return Some(Vec::new()),
Err(_) => return None,
};
let text: &str = std::str::from_utf8(bytes.as_slice()).ok()?;
let mut packed: Vec<(String, String)> = Vec::new();
let mut after_ref: bool = false;
for (position, line) in text.lines().enumerate() {
if line.starts_with('#') {
if position != 0 {
return None;
}
continue;
}
if let Some(peeled) = line.strip_prefix('^') {
if !after_ref || !self.is_name(peeled) {
return None;
}
after_ref = false;
continue;
}
let (object, name) = line.split_once(' ')?;
if !self.is_name(object) || name.is_empty() || name.bytes().any(|byte| { return byte < 0x20 || byte == 0x7f; }) {
return None;
}
after_ref = true;
if !is_plain_ref_name(name) {
if skip_unplain {
continue;
}
return None;
}
packed.push((name.to_string(), object.to_string()));
}
return Some(packed);
}
pub fn lookup_head(&self, git_dir: &Path) -> Option<RefValue> {
let bytes: Vec<u8> = read_regular_file(git_dir.join("HEAD").as_path(), LOOSE_LIMIT).ok()?;
return self.parse_loose(std::str::from_utf8(bytes.as_slice()).ok()?);
}
pub fn lookup(&self, name: &str) -> Lookup {
if !is_plain_ref_name(name) {
return Lookup::Unsure;
}
let path: PathBuf = self.directory.join(name);
match std::fs::symlink_metadata(&path) {
Ok(metadata) if metadata.is_file() => {
let Ok(bytes) = read_regular_file(path.as_path(), LOOSE_LIMIT) else {
return Lookup::Unsure;
};
let Some(value) = std::str::from_utf8(bytes.as_slice()).ok().and_then(|text| { return self.parse_loose(text); }) else {
return Lookup::Unsure;
};
return Lookup::Found(value);
}
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => return Lookup::Unsure,
Err(error) if matches!(error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory) => {}
Err(_) => return Lookup::Unsure,
}
let Some(packed) = self.packed_named(true) else {
return Lookup::Unsure;
};
return match packed.into_iter().find(|(candidate, _)| { return candidate == name; }) {
Some((_, object)) => Lookup::Found(RefValue::Direct(object)),
None => Lookup::Missing,
};
}
pub fn resolve(&self, name: &str) -> Option<String> {
let mut current: String = name.to_string();
for _ in 0..SYMBOLIC_DEPTH_LIMIT {
match self.lookup(current.as_str()) {
Lookup::Found(RefValue::Direct(object)) => return Some(object),
Lookup::Found(RefValue::Symbolic(target)) => current = target,
Lookup::Missing | Lookup::Unsure => return None,
}
}
return None;
}
pub fn list(&self) -> Option<Vec<(String, RefValue)>> {
let mut found: std::collections::BTreeMap<String, RefValue> = std::collections::BTreeMap::new();
for (name, object) in self.packed()? {
found.insert(name, RefValue::Direct(object));
}
let mut pending: Vec<String> = vec![String::from("refs")];
while let Some(directory) = pending.pop() {
let listing = match std::fs::read_dir(self.directory.join(&directory)) {
Ok(listing) => listing,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
for candidate in listing {
let entry = candidate.ok()?;
let name: String = format!("{directory}/{}", entry.file_name().to_str()?);
let kind = entry.file_type().ok()?;
if kind.is_dir() {
pending.push(name);
} else if kind.is_file() && !name.ends_with(".lock") {
match self.lookup(name.as_str()) {
Lookup::Found(value) => {
found.insert(name, value);
}
Lookup::Missing | Lookup::Unsure => return None,
}
} else {
return None;
}
}
}
return Some(found.into_iter().collect());
}
}
#[cfg(test)]
#[path = "repo_read_refs_tests.rs"]
mod tests;