use crate::core::{Object, ObjectHash};
use crate::crypto::encryption::EncryptionManager;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crc32fast::Hasher as Crc32Hasher;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::collections::HashMap;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::{Path, PathBuf};
pub const PACK_MAGIC: &[u8; 4] = b"LITP";
pub const PACK_VERSION: u32 = 1;
pub const INDEX_MAGIC: &[u8; 4] = b"LITI";
pub const INDEX_VERSION: u32 = 1;
#[derive(Debug, Clone)]
pub struct PackIndexEntry {
pub hash: ObjectHash,
pub offset: u64,
pub crc32: u32,
}
pub fn packs_dir(repo_root: &Path) -> PathBuf {
repo_root.join(".lit").join("packs")
}
pub fn write_pack(
objects: &[(ObjectHash, Object)],
pack_path: &Path,
encryption: &EncryptionManager,
) -> Result<Vec<PackIndexEntry>, crate::errors::LitError> {
let mut buf: Vec<u8> = Vec::new();
let mut index_entries = Vec::new();
buf.extend_from_slice(PACK_MAGIC);
buf.write_u32::<BigEndian>(PACK_VERSION)
.map_err(|e| format!("Write error: {}", e))?;
buf.write_u32::<BigEndian>(objects.len() as u32)
.map_err(|e| format!("Write error: {}", e))?;
for (hash, obj) in objects {
let offset = buf.len() as u64;
let type_byte = match obj {
Object::Blob(_) => 1u8,
Object::Tree(_) => 2u8,
Object::Commit(_) => 3u8,
Object::Tag(_) => 4u8,
};
let raw = obj.to_bytes();
let uncompressed_size = raw.len() as u64;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
encoder
.write_all(&raw)
.map_err(|e| format!("Compress error: {}", e))?;
let compressed = encoder
.finish()
.map_err(|e| format!("Compress finish error: {}", e))?;
let stored = encryption.encrypt(&compressed)?;
let mut crc32 = Crc32Hasher::new();
crc32.update(&stored);
let crc_val = crc32.finalize();
buf.push(type_byte);
buf.write_u64::<BigEndian>(uncompressed_size)
.map_err(|e| format!("Write error: {}", e))?;
buf.write_u64::<BigEndian>(stored.len() as u64)
.map_err(|e| format!("Write error: {}", e))?;
buf.extend_from_slice(&stored);
index_entries.push(PackIndexEntry {
hash: hash.clone(),
offset,
crc32: crc_val,
});
}
fs::write(pack_path, &buf).map_err(|e| format!("Failed to write pack: {}", e))?;
Ok(index_entries)
}
pub fn write_pack_index(
entries: &[PackIndexEntry],
index_path: &Path,
) -> Result<(), crate::errors::LitError> {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(INDEX_MAGIC);
buf.write_u32::<BigEndian>(INDEX_VERSION)
.map_err(|e| format!("Write error: {}", e))?;
buf.write_u32::<BigEndian>(entries.len() as u32)
.map_err(|e| format!("Write error: {}", e))?;
let mut sorted = entries.to_vec();
sorted.sort_by(|a, b| a.hash.as_str().cmp(b.hash.as_str()));
for entry in &sorted {
let hash_bytes = entry.hash.as_str().as_bytes();
buf.write_u32::<BigEndian>(hash_bytes.len() as u32)
.map_err(|e| format!("Write error: {}", e))?;
buf.extend_from_slice(hash_bytes);
buf.write_u64::<BigEndian>(entry.offset)
.map_err(|e| format!("Write error: {}", e))?;
buf.write_u32::<BigEndian>(entry.crc32)
.map_err(|e| format!("Write error: {}", e))?;
}
fs::write(index_path, &buf).map_err(|e| format!("Failed to write index: {}", e))?;
Ok(())
}
pub fn read_pack_object(
pack_path: &Path,
offset: u64,
encryption: &EncryptionManager,
) -> Result<Object, crate::errors::LitError> {
let pack_data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
let mut cursor = Cursor::new(&pack_data);
cursor.set_position(offset);
let _type_byte = cursor.read_u8().map_err(|e| format!("Read error: {}", e))?;
let _uncompressed_size = cursor
.read_u64::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
let compressed_size = cursor
.read_u64::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
let pos = cursor.position() as usize;
let end = pos
.checked_add(compressed_size as usize)
.ok_or("Pack entry length overflows")?;
if end > pack_data.len() {
return Err("Pack data truncated".into());
}
let stored = &pack_data[pos..end];
let compressed = encryption.decrypt(stored)?;
let mut decoder = ZlibDecoder::new(&compressed[..]);
let mut raw = Vec::new();
decoder
.read_to_end(&mut raw)
.map_err(|e| format!("Decompress error: {}", e))?;
Object::from_bytes(&raw).map_err(Into::into)
}
pub fn load_pack_index(index_path: &Path) -> Result<HashMap<String, (PathBuf, u64)>, String> {
let data = fs::read(index_path).map_err(|e| format!("Failed to read index: {}", e))?;
let mut cursor = Cursor::new(&data);
let mut magic = [0u8; 4];
cursor
.read_exact(&mut magic)
.map_err(|e| format!("Read error: {}", e))?;
if &magic != INDEX_MAGIC {
return Err("Invalid pack index magic".into());
}
let _version = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
let count = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
let pack_path = index_path.with_extension("pack");
let mut map = HashMap::new();
for _ in 0..count {
let hash_len = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))? as usize;
let pos = cursor.position() as usize;
if pos + hash_len > data.len() {
return Err("Index data truncated".into());
}
let hash_str = String::from_utf8(data[pos..pos + hash_len].to_vec())
.map_err(|e| format!("Invalid hash UTF-8: {}", e))?;
cursor.set_position((pos + hash_len) as u64);
let offset = cursor
.read_u64::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
let _crc32 = cursor
.read_u32::<BigEndian>()
.map_err(|e| format!("Read error: {}", e))?;
map.insert(hash_str, (pack_path.clone(), offset));
}
Ok(map)
}
pub fn load_all(packs_dir: &Path) -> HashMap<String, (PathBuf, u64)> {
let mut map = HashMap::new();
let entries = match fs::read_dir(packs_dir) {
Ok(entries) => entries,
Err(_) => return map,
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("idx") {
continue;
}
match load_pack_index(&path) {
Ok(entries) => map.extend(entries),
Err(e) => eprintln!("Warning: ignoring unreadable pack index {:?}: {}", path, e),
}
}
map
}