use std::fs;
use std::path::Path;
use crate::{Error, Result};
const HEADER_LEN: usize = 12;
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
Cleared(usize),
Skipped(String),
}
pub fn forget_stat(index_path: &Path, hash: gix_hash::Kind, paths: &[Vec<u8>]) -> Result<Outcome> {
if paths.is_empty() {
return Ok(Outcome::Cleared(0));
}
let Some(lock) = Lock::acquire(index_path)? else {
return Ok(Outcome::Skipped(format!(
"{}.lock is held by another git process, so the stat cache was left \
alone. The files are decrypted correctly; if `git status` shows them \
as modified, `git add --renormalize .` settles it.",
index_path.display()
)));
};
let data = match fs::read(index_path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(Outcome::Skipped(format!(
"{} does not exist, so there is no stat cache to refresh",
index_path.display()
)));
}
Err(err) => return Err(Error::Io(err)),
};
let hash_len = hash.len_in_bytes();
let Index {
mut data,
body_len,
version,
count,
skip_hash,
} = match inspect(data, hash) {
Ok(index) => index,
Err(why) => return Ok(skipped(index_path, &why)),
};
let Some(scan) = scan(&data[..body_len], version, count, hash_len, paths) else {
return Ok(skipped(index_path, "its entries did not parse"));
};
if scan.split_index {
return Ok(skipped(
index_path,
"this repository uses a split index, whose entries live in a shared \
file this build does not patch",
));
}
if scan.size_fields.is_empty() {
return Ok(Outcome::Cleared(0));
}
for offset in &scan.size_fields {
data[*offset..*offset + 4].fill(0);
}
if !skip_hash {
let Some(digest) = checksum(&data[..body_len], hash) else {
return Ok(skipped(index_path, "its checksum could not be computed"));
};
data[body_len..].copy_from_slice(&digest);
}
lock.commit(&data)?;
Ok(Outcome::Cleared(scan.size_fields.len()))
}
pub fn restage(
index_path: &Path,
hash: gix_hash::Kind,
updates: &[(Vec<u8>, Vec<u8>)],
) -> Result<Restaged> {
let hash_len = hash.len_in_bytes();
if updates.is_empty() {
return Ok(Restaged::Done(Vec::new()));
}
for (path, id) in updates {
if id.len() != hash_len {
return Err(Error::Config(format!(
"{}: the new object id is {} bytes, but this repository's index \
stores {hash_len}; the index was left alone",
String::from_utf8_lossy(path),
id.len()
)));
}
}
let Some(lock) = Lock::acquire(index_path)? else {
return Ok(Restaged::Skipped(format!(
"{}.lock is held by another git process, so nothing was re-staged. \
Try again, or run `git add` on the reported paths yourself.",
index_path.display()
)));
};
let data = match fs::read(index_path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(Restaged::Skipped(format!(
"{} does not exist, so there is nothing staged to re-stage",
index_path.display()
)));
}
Err(err) => return Err(Error::Io(err)),
};
let Index {
mut data,
body_len,
version,
count,
skip_hash,
} = match inspect(data, hash) {
Ok(index) => index,
Err(why) => return Ok(Restaged::Skipped(why_skipped(index_path, &why))),
};
let mut edits: Vec<(usize, &[u8], Vec<u8>)> = Vec::new();
let walked = walk(&data[..body_len], version, count, hash_len, &mut |entry| {
if entry.stage != 0 {
return;
}
if let Some((path, id)) = updates.iter().find(|(path, _)| path == entry.name) {
edits.push((entry.start, id.as_slice(), path.clone()));
}
});
let layout = match walked {
None => {
return Ok(Restaged::Skipped(why_skipped(
index_path,
"its entries did not parse",
)));
}
Some(walked) if walked.split_index => {
return Ok(Restaged::Skipped(why_skipped(
index_path,
"this repository uses a split index, whose entries live in a shared \
file this build does not patch",
)));
}
Some(walked) => walked,
};
if edits.is_empty() {
return Ok(Restaged::Done(Vec::new()));
}
let mut patched = Vec::with_capacity(edits.len());
for (start, id, path) in edits {
data[start + ID_FIELD..start + ID_FIELD + hash_len].copy_from_slice(id);
data[start + SIZE_FIELD..start + SIZE_FIELD + 4].fill(0);
patched.push(path);
}
let mut rebuilt = data[..layout.extensions_at].to_vec();
for extension in &layout.extensions {
if matches!(&extension.signature, b"TREE" | b"EOIE") {
continue;
}
rebuilt.extend_from_slice(&data[extension.start..extension.end]);
}
if skip_hash {
rebuilt.extend_from_slice(&vec![0u8; hash_len]);
} else {
let Some(digest) = checksum(&rebuilt, hash) else {
return Ok(Restaged::Skipped(why_skipped(
index_path,
"its checksum could not be computed",
)));
};
rebuilt.extend_from_slice(&digest);
}
debug_assert!(body_len >= layout.extensions_at);
lock.commit(&rebuilt)?;
Ok(Restaged::Done(patched))
}
#[derive(Debug, PartialEq, Eq)]
pub enum Restaged {
Done(Vec<Vec<u8>>),
Skipped(String),
}
#[derive(Debug, PartialEq, Eq)]
pub enum Staged {
Read(Vec<Option<Vec<u8>>>),
Unavailable(String),
}
pub fn staged_ids(index_path: &Path, hash: gix_hash::Kind, paths: &[Vec<u8>]) -> Result<Staged> {
let data = match fs::read(index_path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(Staged::Read(vec![None; paths.len()]));
}
Err(err) => return Err(Error::Io(err)),
};
let index = match inspect(data, hash) {
Ok(index) => index,
Err(why) => return Ok(Staged::Unavailable(why)),
};
let mut found: Vec<Option<Vec<u8>>> = vec![None; paths.len()];
let body = &index.data[..index.body_len];
let walked = walk(
body,
index.version,
index.count,
hash.len_in_bytes(),
&mut |entry| {
if entry.stage != 0 {
return;
}
for (at, path) in paths.iter().enumerate() {
if path.as_slice() == entry.name {
found[at] = Some(entry.id.to_vec());
}
}
},
);
match walked {
None => Ok(Staged::Unavailable("its entries did not parse".into())),
Some(walked) if walked.split_index => Ok(Staged::Unavailable(
"this repository uses a split index, whose entries live in a shared \
file this build does not read"
.into(),
)),
Some(_) => Ok(Staged::Read(found)),
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Listed {
Read(Vec<Tracked>),
Unavailable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tracked {
pub path: Vec<u8>,
pub id: Vec<u8>,
pub mode: u32,
pub intent_to_add: bool,
}
impl Tracked {
#[must_use]
pub fn is_regular_file(&self) -> bool {
self.mode & 0o170_000 == 0o100_000
}
#[must_use]
pub fn holds_content(&self) -> bool {
self.is_regular_file() && !self.intent_to_add
}
}
pub fn list(index_path: &Path, hash: gix_hash::Kind) -> Result<Listed> {
let data = match fs::read(index_path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(Listed::Read(Vec::new()));
}
Err(err) => return Err(Error::Io(err)),
};
let index = match inspect(data, hash) {
Ok(index) => index,
Err(why) => return Ok(Listed::Unavailable(why)),
};
let mut entries = Vec::with_capacity(index.count);
let walked = walk(
&index.data[..index.body_len],
index.version,
index.count,
hash.len_in_bytes(),
&mut |entry| {
if entry.stage == 0 {
entries.push(Tracked {
path: entry.name.to_vec(),
id: entry.id.to_vec(),
mode: entry.mode,
intent_to_add: entry.intent_to_add,
});
}
},
);
match walked {
None => Ok(Listed::Unavailable("its entries did not parse".into())),
Some(walked) if walked.split_index => Ok(Listed::Unavailable(
"this repository uses a split index, whose entries live in a shared \
file this build does not read"
.into(),
)),
Some(_) => Ok(Listed::Read(entries)),
}
}
#[must_use]
pub fn blob_id(hash: gix_hash::Kind, content: &[u8]) -> Option<Vec<u8>> {
let mut hasher = gix_hash::hasher(hash);
hasher.update(format!("blob {}\0", content.len()).as_bytes());
hasher.update(content);
hasher
.try_finalize()
.ok()
.map(|digest| digest.as_slice().to_vec())
}
struct Index {
data: Vec<u8>,
body_len: usize,
version: u32,
count: usize,
skip_hash: bool,
}
fn inspect(data: Vec<u8>, hash: gix_hash::Kind) -> std::result::Result<Index, String> {
let hash_len = hash.len_in_bytes();
if data.len() < HEADER_LEN + hash_len || !data.starts_with(b"DIRC") {
return Err("it is not an index this build can read".into());
}
let body_len = data.len() - hash_len;
let recorded = &data[body_len..];
let skip_hash = recorded.iter().all(|byte| *byte == 0);
if !skip_hash {
let Some(digest) = checksum(&data[..body_len], hash) else {
return Err("its checksum could not be computed".into());
};
if digest != recorded {
return Err("its checksum does not match its contents".into());
}
}
let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let count = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
if !(2..=4).contains(&version) {
return Err(format!(
"it is version {version}, which this build does not know"
));
}
Ok(Index {
data,
body_len,
version,
count,
skip_hash,
})
}
fn skipped(index_path: &Path, why: &str) -> Outcome {
Outcome::Skipped(format!(
"{} was left alone because {why}. The files are decrypted correctly; if \
`git status` shows them as modified, `git add --renormalize .` settles it.",
index_path.display()
))
}
fn why_skipped(index_path: &Path, why: &str) -> String {
format!(
"{} was left alone because {why}, so nothing was re-staged. \
`git add` on the reported paths does the same job.",
index_path.display()
)
}
fn checksum(body: &[u8], hash: gix_hash::Kind) -> Option<Vec<u8>> {
let mut hasher = gix_hash::hasher(hash);
hasher.update(body);
hasher
.try_finalize()
.ok()
.map(|digest| digest.as_slice().to_vec())
}
#[derive(Debug)]
struct Scan {
size_fields: Vec<usize>,
split_index: bool,
}
const SIZE_FIELD: usize = 36;
const ID_FIELD: usize = 40;
struct Entry<'a> {
start: usize,
name: &'a [u8],
id: &'a [u8],
stage: u8,
mode: u32,
intent_to_add: bool,
}
const MODE_FIELD: usize = 24;
fn scan(
body: &[u8],
version: u32,
count: usize,
hash_len: usize,
paths: &[Vec<u8>],
) -> Option<Scan> {
let mut fields = Vec::new();
let walked = walk(body, version, count, hash_len, &mut |entry| {
if paths.iter().any(|path| path.as_slice() == entry.name) {
fields.push(entry.start + SIZE_FIELD);
}
})?;
Some(Scan {
size_fields: fields,
split_index: walked.split_index,
})
}
fn walk(
body: &[u8],
version: u32,
count: usize,
hash_len: usize,
visit: &mut dyn FnMut(&Entry<'_>),
) -> Option<Walked> {
let fixed = ID_FIELD + hash_len + 2;
let mut cursor = HEADER_LEN;
let mut previous: Vec<u8> = Vec::new();
for _ in 0..count {
let start = cursor;
let flags_at = start.checked_add(ID_FIELD + hash_len)?;
if body.len() < flags_at + 2 {
return None;
}
let flags = u16::from_be_bytes([body[flags_at], body[flags_at + 1]]);
let extended = flags & 0x4000 != 0;
let intent_to_add = version >= 3
&& extended
&& body.len() >= flags_at + 4
&& u16::from_be_bytes([body[flags_at + 2], body[flags_at + 3]]) & 0x2000 != 0;
let stage = ((flags >> 12) & 0x3) as u8;
let declared = usize::from(flags & 0x0fff);
let mut at = start + fixed;
if version >= 3 && extended {
at += 2;
}
if at > body.len() {
return None;
}
let name = if version < 4 {
let end = if declared < 0x0fff {
let end = at.checked_add(declared)?;
if body.len() <= end || body[end] != 0 {
return None;
}
end
} else {
at + body[at..].iter().position(|byte| *byte == 0)?
};
cursor = start + (((end - start) + 8) & !7);
body[at..end].to_vec()
} else {
let (strip, used) = varint(body.get(at..)?)?;
let suffix_at = at + used;
let end = suffix_at + body.get(suffix_at..)?.iter().position(|byte| *byte == 0)?;
if strip > previous.len() {
return None;
}
cursor = end + 1;
let mut name = previous[..previous.len() - strip].to_vec();
name.extend_from_slice(&body[suffix_at..end]);
name
};
if cursor > body.len() {
return None;
}
visit(&Entry {
start,
name: &name,
id: &body[start + ID_FIELD..flags_at],
stage,
mode: u32::from_be_bytes([
body[start + MODE_FIELD],
body[start + MODE_FIELD + 1],
body[start + MODE_FIELD + 2],
body[start + MODE_FIELD + 3],
]),
intent_to_add,
});
previous = name;
}
let mut walked = Walked {
split_index: false,
extensions_at: cursor,
extensions: Vec::new(),
};
while cursor < body.len() {
let header_end = cursor.checked_add(8)?;
if header_end > body.len() {
return None;
}
let mut signature = [0u8; 4];
signature.copy_from_slice(&body[cursor..cursor + 4]);
if &signature == b"link" {
walked.split_index = true;
}
let length = u32::from_be_bytes([
body[cursor + 4],
body[cursor + 5],
body[cursor + 6],
body[cursor + 7],
]) as usize;
let start = cursor;
cursor = header_end.checked_add(length)?;
if cursor > body.len() {
return None;
}
walked.extensions.push(Extension {
signature,
start,
end: cursor,
});
}
Some(walked)
}
struct Walked {
split_index: bool,
extensions_at: usize,
extensions: Vec<Extension>,
}
struct Extension {
signature: [u8; 4],
start: usize,
end: usize,
}
fn varint(bytes: &[u8]) -> Option<(usize, usize)> {
let mut index = 1;
let mut byte = *bytes.first()?;
let mut value = usize::from(byte & 0x7f);
while byte & 0x80 != 0 {
if index >= 10 {
return None;
}
value = value.checked_add(1)?;
byte = *bytes.get(index)?;
index += 1;
value = value
.checked_mul(128)?
.checked_add(usize::from(byte & 0x7f))?;
}
Some((value, index))
}
struct Lock {
path: std::path::PathBuf,
target: std::path::PathBuf,
file: Option<fs::File>,
}
impl Lock {
fn acquire(index_path: &Path) -> Result<Option<Self>> {
let path = index_path.with_extension("lock");
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(file) => Ok(Some(Self {
path,
target: index_path.to_path_buf(),
file: Some(file),
})),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(None),
Err(err) => Err(Error::Io(err)),
}
}
fn commit(mut self, data: &[u8]) -> Result<()> {
use std::io::Write as _;
let mut file = self.file.take().ok_or_else(|| {
Error::Io(std::io::Error::other("the index lock was already released"))
})?;
let result = (|| -> std::io::Result<()> {
if let Ok(existing) = fs::metadata(&self.target) {
file.set_permissions(existing.permissions())?;
}
file.write_all(data)?;
file.sync_all()?;
Ok(())
})();
drop(file);
if let Err(err) = result.and_then(|()| fs::rename(&self.path, &self.target)) {
let _ = fs::remove_file(&self.path);
return Err(Error::Io(err));
}
if let Some(parent) = self.target.parent()
&& let Ok(directory) = fs::File::open(parent)
{
let _ = directory.sync_all();
}
Ok(())
}
}
impl Drop for Lock {
fn drop(&mut self) {
if self.file.take().is_some() {
let _ = fs::remove_file(&self.path);
}
}
}
#[must_use]
pub fn object_hash(object_format: Option<&str>) -> gix_hash::Kind {
match object_format {
Some(format) if format.eq_ignore_ascii_case("sha256") => gix_hash::Kind::Sha256,
_ => gix_hash::Kind::Sha1,
}
}