#![cfg(unix)]
use super::repo_read_object::{ObjectKind, name_length};
use super::repo_read_layout::ObjectFormat;
use super::repo_read_odb::ObjectDatabase;
use std::cell::RefCell;
use std::rc::Rc;
const TREE_DEPTH_LIMIT: usize = 256;
const FLAT_ENTRY_LIMIT: usize = 4_000_000;
pub const MODE_TREE: u32 = 0o040_000;
pub const MODE_GITLINK: u32 = 0o160_000;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeEntry {
pub mode: u32,
pub name: Vec<u8>,
pub id: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FlatEntry {
pub path: Vec<u8>,
pub mode: u32,
pub id: Vec<u8>,
}
pub fn parse_tree(bytes: &[u8], hash_length: usize) -> Option<Vec<TreeEntry>> {
let mut entries: Vec<TreeEntry> = Vec::new();
let mut cursor: usize = 0;
while cursor < bytes.len() {
let space: usize = cursor + bytes[cursor..].iter().position(|byte| { return *byte == b' '; })?;
let mode_text: &str = std::str::from_utf8(&bytes[cursor..space]).ok()?;
if mode_text.is_empty() || mode_text.starts_with('0') {
return None;
}
let mode: u32 = u32::from_str_radix(mode_text, 8).ok()?;
if !matches!(mode, 0o100_644 | 0o100_755 | 0o120_000 | 0o160_000 | MODE_TREE) {
return None;
}
let nul: usize = space + 1 + bytes[space + 1..].iter().position(|byte| { return *byte == 0; })?;
let name: Vec<u8> = bytes[space + 1..nul].to_vec();
if name.is_empty() || name.contains(&b'/') {
return None;
}
let id: Vec<u8> = bytes.get(nul + 1..nul + 1 + hash_length)?.to_vec();
entries.push(TreeEntry { mode, name, id });
cursor = nul + 1 + hash_length;
}
return Some(entries);
}
pub fn is_tree_mode(mode: u32) -> bool {
return mode & 0o170_000 == MODE_TREE;
}
pub fn flatten_tree(database: &ObjectDatabase, tree: &[u8], format: ObjectFormat) -> Option<Vec<FlatEntry>> {
let mut flat: Vec<FlatEntry> = Vec::new();
walk(database, tree, format, &mut Vec::new(), 0, &mut flat)?;
return Some(flat);
}
fn walk(
database: &ObjectDatabase,
tree: &[u8],
format: ObjectFormat,
prefix: &mut Vec<u8>,
depth: usize,
flat: &mut Vec<FlatEntry>,
) -> Option<()> {
if depth > TREE_DEPTH_LIMIT {
return None;
}
let object = database.read(tree)?;
if object.kind != ObjectKind::Tree {
return None;
}
for entry in parse_tree(object.bytes.as_slice(), name_length(format))? {
let length: usize = prefix.len();
prefix.extend_from_slice(entry.name.as_slice());
if is_tree_mode(entry.mode) {
prefix.push(b'/');
walk(database, entry.id.as_slice(), format, prefix, depth + 1, flat)?;
} else {
flat.push(FlatEntry { path: prefix.clone(), mode: entry.mode, id: entry.id });
if flat.len() > FLAT_ENTRY_LIMIT {
return None;
}
}
prefix.truncate(length);
}
return Some(());
}
pub fn flatten_tree_shared(database: &ObjectDatabase, tree: &[u8], format: ObjectFormat) -> Option<Rc<Vec<FlatEntry>>> {
let key: Vec<u8> = tree.to_vec();
let kept: Option<Rc<Vec<FlatEntry>>> = SHARED_LISTS.with(|lists| {
return lists.borrow().iter().find(|(name, _)| { return *name == key; }).map(|(_, list)| { return Rc::clone(list); });
});
if let Some(found) = kept {
return Some(found);
}
let listed: Rc<Vec<FlatEntry>> = Rc::new(flatten_tree(database, tree, format)?);
SHARED_LISTS.with(|lists| {
let mut held = lists.borrow_mut();
if held.len() >= SHARED_LIST_LIMIT {
held.remove(0);
}
held.push((key, Rc::clone(&listed)));
});
return Some(listed);
}
const SHARED_LIST_LIMIT: usize = 3;
thread_local! {
static SHARED_LISTS: RefCell<Vec<(Vec<u8>, Rc<Vec<FlatEntry>>)>> = const { RefCell::new(Vec::new()) };
}
pub fn flatten_pair(database: &ObjectDatabase, old: &[u8], new: &[u8], format: ObjectFormat) -> Option<(Vec<FlatEntry>, Vec<FlatEntry>)> {
let mut before: Vec<FlatEntry> = Vec::new();
let mut after: Vec<FlatEntry> = Vec::new();
walk_pair(database, (old, new), format, &mut Vec::new(), 0, (&mut before, &mut after))?;
return Some((before, after));
}
fn tree_order_key(entry: &TreeEntry) -> Vec<u8> {
let mut key: Vec<u8> = entry.name.clone();
if is_tree_mode(entry.mode) {
key.push(b'/');
}
return key;
}
fn push_side(database: &ObjectDatabase, entry: &TreeEntry, format: ObjectFormat, prefix: &mut Vec<u8>, depth: usize, flat: &mut Vec<FlatEntry>) -> Option<()> {
let length: usize = prefix.len();
prefix.extend_from_slice(entry.name.as_slice());
if is_tree_mode(entry.mode) {
prefix.push(b'/');
walk(database, entry.id.as_slice(), format, prefix, depth + 1, flat)?;
} else {
flat.push(FlatEntry { path: prefix.clone(), mode: entry.mode, id: entry.id.clone() });
}
prefix.truncate(length);
return Some(());
}
fn walk_pair(
database: &ObjectDatabase,
trees: (&[u8], &[u8]),
format: ObjectFormat,
prefix: &mut Vec<u8>,
depth: usize,
flats: (&mut Vec<FlatEntry>, &mut Vec<FlatEntry>),
) -> Option<()> {
if depth > TREE_DEPTH_LIMIT {
return None;
}
let (before, after) = flats;
let entries_of = |name: &[u8]| -> Option<Vec<TreeEntry>> {
let object = database.read(name)?;
if object.kind != ObjectKind::Tree {
return None;
}
return parse_tree(object.bytes.as_slice(), name_length(format));
};
let old_entries: Vec<TreeEntry> = entries_of(trees.0)?;
let new_entries: Vec<TreeEntry> = entries_of(trees.1)?;
let (mut left, mut right): (usize, usize) = (0, 0);
while left < old_entries.len() || right < new_entries.len() {
let order = match (old_entries.get(left), new_entries.get(right)) {
(Some(gone), Some(added)) => tree_order_key(gone).cmp(&tree_order_key(added)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, _) => std::cmp::Ordering::Greater,
};
match order {
std::cmp::Ordering::Less => {
push_side(database, &old_entries[left], format, prefix, depth, before)?;
left += 1;
}
std::cmp::Ordering::Greater => {
push_side(database, &new_entries[right], format, prefix, depth, after)?;
right += 1;
}
std::cmp::Ordering::Equal => {
let (was, now): (&TreeEntry, &TreeEntry) = (&old_entries[left], &new_entries[right]);
if was.id != now.id || was.mode != now.mode {
if is_tree_mode(was.mode) {
let length: usize = prefix.len();
prefix.extend_from_slice(was.name.as_slice());
prefix.push(b'/');
walk_pair(database, (was.id.as_slice(), now.id.as_slice()), format, prefix, depth + 1, (&mut *before, &mut *after))?;
prefix.truncate(length);
} else {
push_side(database, was, format, prefix, depth, before)?;
push_side(database, now, format, prefix, depth, after)?;
}
}
left += 1;
right += 1;
}
}
if before.len() + after.len() > FLAT_ENTRY_LIMIT {
return None;
}
}
return Some(());
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommitFields {
pub tree: Vec<u8>,
pub parents: Vec<Vec<u8>>,
pub message: Vec<u8>,
}
pub fn parse_commit(bytes: &[u8], format: ObjectFormat) -> Option<CommitFields> {
let split: usize = bytes.windows(2).position(|pair| { return pair == b"\n\n"; })?;
let header: &[u8] = &bytes[..split];
let mut tree: Option<Vec<u8>> = None;
let mut parents: Vec<Vec<u8>> = Vec::new();
for (index, line) in header.split(|byte| { return *byte == b'\n'; }).enumerate() {
if let Some(text) = line.strip_prefix(b"tree ") {
if index != 0 {
return None;
}
tree = Some(super::repo_read_object::from_hex(text, format)?);
} else if let Some(text) = line.strip_prefix(b"parent ") {
parents.push(super::repo_read_object::from_hex(text, format)?);
} else if line.starts_with(b"encoding ") {
return None;
}
}
return Some(CommitFields { tree: tree?, parents, message: bytes[split + 2..].to_vec() });
}
pub fn subject_of(message: &[u8]) -> Option<Vec<u8>> {
if message.contains(&b'\r') || message.contains(&0) {
return None;
}
let mut subject: Vec<u8> = Vec::new();
let mut started: bool = false;
for line in message.split(|byte| { return *byte == b'\n'; }) {
let trimmed: &[u8] = line.trim_ascii_end();
let blank: bool = trimmed.is_empty();
if blank && !started {
continue;
}
if blank {
break;
}
if started {
subject.push(b' ');
}
started = true;
subject.extend_from_slice(line.trim_ascii_end());
}
return Some(subject);
}
#[cfg(test)]
#[path = "repo_read_tree_tests.rs"]
mod tests;