use std::{
fs,
sync::{Arc, MutexGuard},
time::SystemTime,
};
use crate::{GitError, ObjectId, Repository, Result, error::invalid};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexEntry {
pub ctime_seconds: u32,
pub mtime_seconds: u32,
pub mode: u32,
pub size: u32,
pub id: ObjectId,
pub stage: u8,
pub assume_valid: bool,
pub skip_worktree: bool,
pub intent_to_add: bool,
pub path: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Index {
version: u32,
entries: Vec<IndexEntry>,
}
pub(crate) struct CachedIndex {
len: u64,
modified: Option<SystemTime>,
index: Arc<Index>,
}
impl Index {
#[must_use]
pub const fn version(&self) -> u32 {
self.version
}
#[must_use]
pub fn entries(&self) -> &[IndexEntry] {
&self.entries
}
fn parse(data: &[u8], repository: &Repository) -> Result<Self> {
if data.get(..4) != Some(b"DIRC") {
return Err(invalid("invalid index signature"));
}
let version = read_u32(data, 4)?;
if !(2..=4).contains(&version) {
return Err(GitError::Unsupported(format!("index version {version}")));
}
let count = usize::try_from(read_u32(data, 8)?)
.map_err(|_| invalid("index entry count overflow"))?;
if count > repository.limits().max_index_entries {
return Err(GitError::LimitExceeded {
resource: "index entries",
limit: repository.limits().max_index_entries,
});
}
let trailer = repository.hash_kind().bytes();
if data.len() < 12 + trailer {
return Err(invalid("truncated index"));
}
let content_end = data.len() - trailer;
let mut cursor = 12;
let mut entries = Vec::with_capacity(count);
let mut previous_path = Vec::new();
for _ in 0..count {
let entry = parse_entry(
data,
&mut cursor,
content_end,
version,
repository.hash_kind(),
&previous_path,
)?;
previous_path.clone_from(&entry.path);
entries.push(entry);
}
if !entries.windows(2).all(|pair| {
(pair[0].path.as_slice(), pair[0].stage) < (pair[1].path.as_slice(), pair[1].stage)
}) {
return Err(invalid("index entries are not sorted"));
}
parse_extensions(data, cursor, content_end)?;
Ok(Self { version, entries })
}
}
impl Repository {
pub fn index(&self) -> Result<Index> {
Ok((*self.index_shared()?).clone())
}
pub fn index_shared(&self) -> Result<Arc<Index>> {
let path = self.git_dir().join("index");
let metadata = fs::metadata(&path)?;
let modified = metadata.modified().ok();
let mut cache = self
.index_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(index) = cached(&cache, metadata.len(), modified) {
return Ok(index);
}
let index = Arc::new(Index::parse(&fs::read(path)?, self)?);
*cache = Some(CachedIndex {
len: metadata.len(),
modified,
index: Arc::clone(&index),
});
Ok(index)
}
}
fn cached(
cache: &MutexGuard<'_, Option<CachedIndex>>,
len: u64,
modified: Option<SystemTime>,
) -> Option<Arc<Index>> {
cache
.as_ref()
.filter(|cached| cached.len == len && cached.modified == modified)
.map(|cached| Arc::clone(&cached.index))
}
fn parse_entry(
data: &[u8],
cursor: &mut usize,
end: usize,
version: u32,
hash: crate::HashKind,
previous_path: &[u8],
) -> Result<IndexEntry> {
let start = *cursor;
let fixed = 40_usize
.checked_add(hash.bytes())
.and_then(|value| value.checked_add(2))
.ok_or_else(|| invalid("index entry length overflow"))?;
if start.saturating_add(fixed) > end {
return Err(invalid("truncated index entry"));
}
let ctime_seconds = read_u32(data, start)?;
let mtime_seconds = read_u32(data, start + 8)?;
let mode = read_u32(data, start + 24)?;
let size = read_u32(data, start + 36)?;
let oid_start = start + 40;
let id = ObjectId::from_bytes(
data.get(oid_start..oid_start + hash.bytes())
.ok_or_else(|| invalid("truncated index object id"))?,
)?;
*cursor = oid_start + hash.bytes();
let flags = read_u16(data, *cursor)?;
*cursor += 2;
let extended = flags & 0x4000 != 0;
if version == 2 && extended {
return Err(invalid("index v2 entry has extended flags"));
}
let extended_flags = if extended {
let value = read_u16(data, *cursor)?;
*cursor += 2;
if value & 0x1fff != 0 {
return Err(invalid("index entry has reserved extended flags"));
}
value
} else {
0
};
let path = if version == 4 {
parse_v4_path(data, cursor, end, previous_path)?
} else {
let path = take_path(data, cursor, end)?;
let entry_len = cursor
.checked_sub(start)
.ok_or_else(|| invalid("index entry cursor underflow"))?;
*cursor = start
.checked_add(entry_len.div_ceil(8) * 8)
.ok_or_else(|| invalid("index entry padding overflow"))?;
path
};
validate_path(&path)?;
Ok(IndexEntry {
ctime_seconds,
mtime_seconds,
mode,
size,
id,
stage: u8::try_from((flags >> 12) & 3).expect("two bits fit u8"),
assume_valid: flags & 0x8000 != 0,
skip_worktree: extended_flags & 0x4000 != 0,
intent_to_add: extended_flags & 0x2000 != 0,
path,
})
}
fn parse_v4_path(data: &[u8], cursor: &mut usize, end: usize, previous: &[u8]) -> Result<Vec<u8>> {
let remove = variable_width(data, cursor, end)?;
if remove > previous.len() {
return Err(invalid("index v4 path prefix is out of bounds"));
}
let suffix = take_path(data, cursor, end)?;
let mut path = previous[..previous.len() - remove].to_vec();
path.extend(suffix);
Ok(path)
}
fn variable_width(data: &[u8], cursor: &mut usize, end: usize) -> Result<usize> {
let mut byte = take(data, cursor, end)?;
let mut value = usize::from(byte & 0x7f);
while byte & 0x80 != 0 {
byte = take(data, cursor, end)?;
value = value
.checked_add(1)
.and_then(|value| value.checked_shl(7))
.and_then(|value| value.checked_add(usize::from(byte & 0x7f)))
.ok_or_else(|| invalid("index v4 path prefix overflow"))?;
}
Ok(value)
}
fn take_path(data: &[u8], cursor: &mut usize, end: usize) -> Result<Vec<u8>> {
let nul = data
.get(*cursor..end)
.and_then(|bytes| bytes.iter().position(|byte| *byte == 0))
.map(|offset| *cursor + offset)
.ok_or_else(|| invalid("index path has no terminator"))?;
let path = data[*cursor..nul].to_vec();
*cursor = nul + 1;
Ok(path)
}
fn validate_path(path: &[u8]) -> Result<()> {
if path.is_empty() || path.first() == Some(&b'/') || path.last() == Some(&b'/') {
return Err(invalid("invalid index path"));
}
if path
.split(|byte| *byte == b'/')
.any(|part| matches!(part, b"." | b".." | b".git"))
{
return Err(invalid("unsafe index path component"));
}
Ok(())
}
fn parse_extensions(data: &[u8], mut cursor: usize, end: usize) -> Result<()> {
while cursor < end {
if cursor.saturating_add(8) > end {
return Err(invalid("truncated index extension"));
}
let signature = &data[cursor..cursor + 4];
let length = usize::try_from(read_u32(data, cursor + 4)?)
.map_err(|_| invalid("index extension length overflow"))?;
if signature[0].is_ascii_lowercase() {
return Err(GitError::Unsupported(format!(
"mandatory index extension {}",
String::from_utf8_lossy(signature)
)));
}
cursor = cursor
.checked_add(8 + length)
.ok_or_else(|| invalid("index extension overflow"))?;
if cursor > end {
return Err(invalid("index extension is out of bounds"));
}
}
Ok(())
}
fn take(data: &[u8], cursor: &mut usize, end: usize) -> Result<u8> {
if *cursor >= end {
return Err(invalid("truncated index data"));
}
let value = data[*cursor];
*cursor += 1;
Ok(value)
}
fn read_u16(input: &[u8], offset: usize) -> Result<u16> {
let bytes = input
.get(offset..offset + 2)
.ok_or_else(|| invalid("truncated index integer"))?;
Ok(u16::from_be_bytes(bytes.try_into().expect("two bytes")))
}
fn read_u32(input: &[u8], offset: usize) -> Result<u32> {
let bytes = input
.get(offset..offset + 4)
.ok_or_else(|| invalid("truncated index integer"))?;
Ok(u32::from_be_bytes(bytes.try_into().expect("four bytes")))
}