use std::fs;
use std::io::Write;
use std::path::Path;
use crate::error::{Error, Result};
use crate::hash::verify_trailer;
use crate::objects::HashAlgo;
use crate::pack::PackIndex;
use tempfile::NamedTempFile;
pub const RIDX_SIGNATURE: u32 = 0x5249_4458;
pub const RIDX_VERSION: u32 = 1;
pub const RIDX_HASH_ID_SHA1: u32 = 1;
pub const RIDX_HASH_ID_SHA256: u32 = 2;
const HEADER_LEN: usize = 12;
const SHA1_TRAILER: usize = 20;
const SHA256_TRAILER: usize = 32;
const fn ridx_hash_id(hash_len: usize) -> u32 {
if hash_len == SHA256_TRAILER {
RIDX_HASH_ID_SHA256
} else {
RIDX_HASH_ID_SHA1
}
}
pub fn append_hashfile_checksum(out: &mut Vec<u8>, hash_len: usize) {
let algo = HashAlgo::from_len(hash_len).unwrap_or(HashAlgo::Sha1);
out.extend_from_slice(&algo.checksum_digest(&*out));
}
#[must_use]
pub fn hashfile_checksum_valid(data: &[u8], hash_len: usize) -> bool {
if data.len() < hash_len {
return false;
}
HashAlgo::from_len(hash_len).is_some_and(|algo| verify_trailer(algo, data).is_ok())
}
pub fn build_pack_rev_bytes(index: &PackIndex) -> Result<Vec<u8>> {
let offsets: Vec<u64> = index
.iter()
.map(|e| e.offset())
.collect::<Result<Vec<_>>>()?;
Ok(build_pack_rev_bytes_from_index_order_offsets(&offsets))
}
#[must_use]
pub fn build_pack_rev_bytes_from_index_order_offsets(offsets: &[u64]) -> Vec<u8> {
build_pack_rev_bytes_from_index_order_offsets_and_checksum(offsets, &[0u8; SHA1_TRAILER])
}
#[must_use]
pub fn build_pack_rev_bytes_from_index_order_offsets_and_checksum(
offsets: &[u64],
pack_checksum: &[u8],
) -> Vec<u8> {
let hash_len = if pack_checksum.len() == SHA256_TRAILER {
SHA256_TRAILER
} else {
SHA1_TRAILER
};
let n = offsets.len();
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_by_key(|&i| offsets[i as usize]);
let body_len = HEADER_LEN + n * 4 + hash_len;
let total_len = body_len + hash_len;
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(&RIDX_SIGNATURE.to_be_bytes());
out.extend_from_slice(&RIDX_VERSION.to_be_bytes());
out.extend_from_slice(&ridx_hash_id(hash_len).to_be_bytes());
for idx_pos in order {
out.extend_from_slice(&idx_pos.to_be_bytes());
}
if pack_checksum.len() >= hash_len {
out.extend_from_slice(&pack_checksum[..hash_len]);
} else {
out.extend_from_slice(&vec![0u8; hash_len]);
}
debug_assert_eq!(out.len(), body_len);
append_hashfile_checksum(&mut out, hash_len);
debug_assert_eq!(out.len(), total_len);
out
}
pub fn write_pack_rev_sidecar_for_index_path(idx_path: &Path) -> Result<()> {
let index = crate::pack::read_pack_index(idx_path)?;
write_pack_rev_sidecar(&index)
}
pub fn write_pack_rev_sidecar(index: &PackIndex) -> Result<()> {
let rev_path = rev_path_for_index(&index.idx_path);
if rev_path.is_file() && verify_pack_rev_file(&rev_path, index).is_ok() {
return Ok(());
}
let hash_len = index.hash_bytes();
let pack_data = fs::read(&index.pack_path).map_err(Error::Io)?;
if pack_data.len() < hash_len {
return Err(Error::CorruptObject("pack file too short for rev".into()));
}
let pack_checksum = pack_data[pack_data.len() - hash_len..].to_vec();
let offsets: Vec<u64> = (0..index.len())
.map(|i| index.offset_at(i))
.collect::<Result<Vec<_>>>()?;
let rev_bytes =
build_pack_rev_bytes_from_index_order_offsets_and_checksum(&offsets, &pack_checksum);
let parent = rev_path.parent().ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"rev path has no parent",
))
})?;
let tmp = NamedTempFile::new_in(parent).map_err(Error::Io)?;
tmp.as_file().write_all(&rev_bytes).map_err(Error::Io)?;
tmp.persist(&rev_path).map_err(|e| Error::Io(e.error))?;
Ok(())
}
#[must_use]
pub fn rev_path_for_index(idx_path: &Path) -> std::path::PathBuf {
let mut p = idx_path.to_path_buf();
p.set_extension("rev");
p
}
#[must_use]
pub fn hashfile_checksum_valid_sha1(data: &[u8]) -> bool {
if data.len() < SHA1_TRAILER {
return false;
}
verify_trailer(HashAlgo::Sha1, data).is_ok()
}
fn read_u32_be(buf: &[u8], pos: &mut usize) -> Option<u32> {
if *pos + 4 > buf.len() {
return None;
}
let v = u32::from_be_bytes(buf[*pos..*pos + 4].try_into().ok()?);
*pos += 4;
Some(v)
}
pub fn pack_rev_fsck_messages(
data: &[u8],
index: &PackIndex,
rev_path_display: &str,
) -> Vec<String> {
let n = index.len();
let hash_len = index.hash_bytes();
let expected_len = HEADER_LEN + n * 4 + hash_len + hash_len;
if data.len() < HEADER_LEN + hash_len {
return vec![format!(
"reverse-index file {rev_path_display} is too small"
)];
}
if data.len() != expected_len {
return vec![format!("reverse-index file {rev_path_display} is corrupt")];
}
let mut pos = 0usize;
let Some(sig) = read_u32_be(data, &mut pos) else {
return vec!["truncated rev-index header".to_owned()];
};
if sig != RIDX_SIGNATURE {
return vec![format!(
"reverse-index file {rev_path_display} has unknown signature"
)];
}
let Some(ver) = read_u32_be(data, &mut pos) else {
return vec!["truncated rev-index header".to_owned()];
};
if ver != RIDX_VERSION {
return vec![format!(
"reverse-index file {rev_path_display} has unsupported version {ver}"
)];
}
let Some(hash_id) = read_u32_be(data, &mut pos) else {
return vec!["truncated rev-index header".to_owned()];
};
if hash_id != RIDX_HASH_ID_SHA1 && hash_id != 2 {
return vec![format!(
"reverse-index file {rev_path_display} has unsupported hash id {hash_id}"
)];
}
let mut msgs = Vec::new();
if !hashfile_checksum_valid(data, hash_len) {
msgs.push("invalid checksum".to_owned());
}
let Ok(offsets) = (0..n)
.map(|i| index.offset_at(i))
.collect::<Result<Vec<_>>>()
else {
msgs.push("corrupt pack index offsets".to_owned());
return msgs;
};
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_by(|&a, &b| offsets[a as usize].cmp(&offsets[b as usize]));
for (i, &expected) in order.iter().enumerate() {
let Some(got) = read_u32_be(data, &mut pos) else {
msgs.push("truncated rev-index data".to_owned());
break;
};
if got != expected {
msgs.push(format!(
"invalid rev-index position at {i}: {got} != {expected}"
));
}
}
msgs
}
pub fn verify_pack_rev_file_contents(
data: &[u8],
index: &PackIndex,
path_for_errors: &str,
) -> std::result::Result<(), String> {
let hash_len = index.hash_bytes();
if !hashfile_checksum_valid(data, hash_len) {
return Err(format!("sha1 file '{path_for_errors}': validation error"));
}
let n = index.len();
let expected_len = HEADER_LEN + n * 4 + hash_len + hash_len;
if data.len() != expected_len {
return Err(format!("reverse-index file {path_for_errors} is corrupt"));
}
let mut pos = 0usize;
let sig = read_u32_be(data, &mut pos).ok_or_else(|| "truncated rev-index header".to_owned())?;
if sig != RIDX_SIGNATURE {
return Err(format!(
"reverse-index file {path_for_errors} has unknown signature"
));
}
let ver = read_u32_be(data, &mut pos).ok_or_else(|| "truncated rev-index header".to_owned())?;
if ver != RIDX_VERSION {
return Err(format!(
"reverse-index file {path_for_errors} has unsupported version {ver}"
));
}
let hash_id =
read_u32_be(data, &mut pos).ok_or_else(|| "truncated rev-index header".to_owned())?;
if hash_id != RIDX_HASH_ID_SHA1 && hash_id != 2 {
return Err(format!(
"reverse-index file {path_for_errors} has unsupported hash id {hash_id}"
));
}
let offsets: Vec<u64> = (0..n)
.map(|i| index.offset_at(i))
.collect::<Result<Vec<_>>>()
.map_err(|e| format!("corrupt pack index: {e}"))?;
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_by(|&a, &b| offsets[a as usize].cmp(&offsets[b as usize]));
for (i, &expected) in order.iter().enumerate() {
let got =
read_u32_be(data, &mut pos).ok_or_else(|| "truncated rev-index data".to_owned())?;
if got != expected {
return Err(format!(
"invalid rev-index position at {i}: {got} != {expected}"
));
}
}
Ok(())
}
pub fn verify_pack_rev_file(rev_path: &Path, index: &PackIndex) -> std::result::Result<(), String> {
let data = match std::fs::read(rev_path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(format!("failed to read {}: {e}", rev_path.display())),
};
let p = rev_path.display().to_string();
verify_pack_rev_file_contents(&data, index, &p)
}
#[must_use]
pub fn try_rev_positions_in_pack_order(data: &[u8], num_objects: usize) -> Option<Vec<u32>> {
let hash_len = [SHA1_TRAILER, SHA256_TRAILER]
.into_iter()
.find(|&hl| data.len() == HEADER_LEN + num_objects * 4 + hl + hl)?;
if !hashfile_checksum_valid(data, hash_len) {
return None;
}
let mut pos = 0usize;
let sig = read_u32_be(data, &mut pos)?;
if sig != RIDX_SIGNATURE {
return None;
}
let ver = read_u32_be(data, &mut pos)?;
if ver != RIDX_VERSION {
return None;
}
let hash_id = read_u32_be(data, &mut pos)?;
if hash_id != RIDX_HASH_ID_SHA1 && hash_id != RIDX_HASH_ID_SHA256 {
return None;
}
let mut pack_order_idx = vec![0u32; num_objects];
for slot in &mut pack_order_idx {
*slot = read_u32_be(data, &mut pos)?;
}
if pos != HEADER_LEN + num_objects * 4 {
return None;
}
pos += hash_len;
if pos != data.len() - hash_len {
return None;
}
if pack_order_idx.len() != num_objects {
return None;
}
let mut seen = vec![false; num_objects];
for &p in &pack_order_idx {
let pi = p as usize;
if pi >= num_objects || seen[pi] {
return None;
}
seen[pi] = true;
}
Some(pack_order_idx)
}
#[cfg(test)]
mod coverage_tests {
use super::*;
use crate::objects::ObjectId;
use crate::pack::{read_pack_index, write_v2_pack_index_with_trailer};
#[test]
fn append_checksum_sha256_and_short_pack_trailer_padding() {
let mut out = b"RIDX".to_vec();
append_hashfile_checksum(&mut out, SHA256_TRAILER);
assert!(hashfile_checksum_valid(&out, SHA256_TRAILER));
let rev = build_pack_rev_bytes_from_index_order_offsets_and_checksum(&[0, 64], &[1, 2, 3]);
assert!(rev.len() > HEADER_LEN + SHA256_TRAILER);
}
#[test]
fn verify_pack_rev_file_io_and_try_rev_none_paths() {
let tmp = tempfile::tempdir().expect("tempdir");
let idx_path = tmp.path().join("x.idx");
let oid = ObjectId::from_hex("0102030405060708090a0b0c0d0e0f1011121314").expect("oid");
write_v2_pack_index_with_trailer(&idx_path, &[(oid, 12, 0)], &[0u8; 20], 20, None)
.expect("write idx");
let index = read_pack_index(&idx_path).expect("load idx");
let rev_path = rev_path_for_index(&idx_path);
std::fs::create_dir_all(&rev_path).expect("dir");
assert!(verify_pack_rev_file(&rev_path, &index).is_err());
let rev = build_pack_rev_bytes(&index).expect("rev bytes");
assert!(try_rev_positions_in_pack_order(&rev, index.len()).is_some());
assert!(try_rev_positions_in_pack_order(&rev[..rev.len() - 1], index.len()).is_none());
let mut bad = rev.clone();
bad[4..8].copy_from_slice(&8u32.to_be_bytes());
assert!(try_rev_positions_in_pack_order(&bad, index.len()).is_none());
let mut bad_id = rev.clone();
bad_id[8..12].copy_from_slice(&8u32.to_be_bytes());
assert!(try_rev_positions_in_pack_order(&bad_id, index.len()).is_none());
let msgs = pack_rev_fsck_messages(&[], &index, "empty");
assert!(!msgs.is_empty());
}
#[test]
fn hashfile_checksum_valid_sha1_rejects_short_input() {
assert!(!hashfile_checksum_valid_sha1(b"short"));
}
}