use crate::core::{find_repo_root, Blob, Commit, Object, ObjectHash, Tag, Tree};
use crate::response::ImportGitResponse;
use crate::storage::ObjectStore;
use sha1::Digest as Sha1Digest;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
pub fn execute(source: String) -> Result<ImportGitResponse, crate::errors::LitError> {
let source_path = PathBuf::from(&source);
let git_dir = find_git_dir(&source_path)?;
let repo_root = match find_repo_root() {
Ok(r) => r,
Err(_) => {
crate::commands::init::execute(false, None)?;
find_repo_root()?
}
};
let store = ObjectStore::new(&repo_root);
let mut hash_map: HashMap<String, ObjectHash> = HashMap::new();
let mut objects_imported = 0u64;
let mut refs_imported = 0u64;
let mut discovered: HashMap<String, DiscoveredObject> = HashMap::new();
let mut deltas_unresolved = 0u64;
let objects_dir = git_dir.join("objects");
if objects_dir.exists() {
for entry in walkdir::WalkDir::new(&objects_dir)
.min_depth(2)
.max_depth(2)
{
let entry = entry.map_err(|e| format!("Failed to walk objects: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if let (Some(dir_name), Some(file_name)) =
(path.parent().and_then(|p| p.file_name()), path.file_name())
{
let dir_str = dir_name.to_string_lossy();
let file_str = file_name.to_string_lossy();
if dir_str == "pack" || dir_str == "info" {
continue;
}
let git_hash = format!("{}{}", dir_str, file_str);
match discover_loose_object(path) {
Ok((hash, object)) => {
discovered.insert(hash, object);
}
Err(e) => {
eprintln!("Warning: skipping object {}: {}", &git_hash[..8], e);
}
}
}
}
}
let mut packs: Vec<(PathBuf, HashMap<usize, PackEntry>)> = Vec::new();
let pack_dir = objects_dir.join("pack");
if pack_dir.exists() {
for entry in
fs::read_dir(&pack_dir).map_err(|e| format!("Failed to read pack dir: {}", e))?
{
let entry = entry.map_err(|e| format!("Pack dir entry error: {}", e))?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("pack") {
match read_pack_entries(&path) {
Ok(entries) => packs.push((path, entries)),
Err(e) => eprintln!("Warning: skipping pack {}: {}", path.display(), e),
}
}
}
}
if !packs.is_empty() {
resolve_all_packs(&packs, &mut discovered, &mut deltas_unresolved)?;
}
let roots: Vec<String> = discovered.keys().cloned().collect();
let mut scheduled: HashSet<String> = HashSet::new();
for git_hash in &roots {
objects_imported += import_subgraph(
git_hash,
&discovered,
&store,
&mut hash_map,
&mut scheduled,
deltas_unresolved,
)?;
}
let refs_heads = git_dir.join("refs").join("heads");
if refs_heads.exists() {
for entry in walkdir::WalkDir::new(&refs_heads).min_depth(1) {
let entry = entry.map_err(|e| format!("Failed to walk refs: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let branch_name = entry
.path()
.strip_prefix(&refs_heads)
.map_err(|e| format!("Path error: {}", e))?
.to_string_lossy()
.replace('\\', "/");
let git_hash = fs::read_to_string(entry.path())
.map_err(|e| format!("Failed to read ref: {}", e))?
.trim()
.to_string();
if let Some(lit_hash) = hash_map.get(&git_hash) {
crate::core::write_ref(
&repo_root,
&format!("heads/{}", branch_name),
lit_hash.as_str(),
)?;
refs_imported += 1;
}
}
}
let refs_tags = git_dir.join("refs").join("tags");
if refs_tags.exists() {
for entry in walkdir::WalkDir::new(&refs_tags).min_depth(1) {
let entry = entry.map_err(|e| format!("Failed to walk tags: {}", e))?;
if !entry.file_type().is_file() {
continue;
}
let tag_name = entry
.path()
.strip_prefix(&refs_tags)
.map_err(|e| format!("Path error: {}", e))?
.to_string_lossy()
.replace('\\', "/");
let git_hash = fs::read_to_string(entry.path())
.map_err(|e| format!("Failed to read ref: {}", e))?
.trim()
.to_string();
if let Some(lit_hash) = hash_map.get(&git_hash) {
crate::core::write_ref(
&repo_root,
&format!("tags/{}", tag_name),
lit_hash.as_str(),
)?;
refs_imported += 1;
}
}
}
let head_path = git_dir.join("HEAD");
if head_path.exists() {
let head_content =
fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
let head_content = head_content.trim();
if let Some(ref_target) = head_content.strip_prefix("ref: refs/heads/") {
crate::core::update_head(&repo_root, ref_target)?;
}
}
let gitignore = source_path.join(".gitignore");
let litignore = repo_root.join(".litignore");
if gitignore.exists() && !litignore.exists() {
let _ = fs::copy(&gitignore, &litignore);
}
Ok(ImportGitResponse {
source: source.clone(),
objects_imported,
refs_imported,
hash_mapping_count: hash_map.len(),
message: format!(
"Imported {} objects and {} refs from Git repository",
objects_imported, refs_imported
),
})
}
fn find_git_dir(path: &Path) -> Result<PathBuf, crate::errors::LitError> {
let dot_git = path.join(".git");
if dot_git.is_dir() {
return Ok(dot_git);
}
if path.join("objects").is_dir() && path.join("refs").is_dir() {
return Ok(path.to_path_buf());
}
Err(format!("Not a Git repository: {}", path.display()).into())
}
enum ObjectSource {
Loose(PathBuf),
Packed(Vec<u8>),
}
struct DiscoveredObject {
source: ObjectSource,
obj_type: String,
deps: Vec<String>,
}
impl DiscoveredObject {
fn content(&self) -> Result<Vec<u8>, crate::errors::LitError> {
match &self.source {
ObjectSource::Loose(path) => Ok(read_loose_object(path)?.2),
ObjectSource::Packed(content) => Ok(content.clone()),
}
}
}
enum Step {
Visit(String),
Emit(String),
}
fn read_loose_object(path: &Path) -> Result<(String, String, Vec<u8>), crate::errors::LitError> {
let compressed = fs::read(path).map_err(|e| format!("Read error: {}", e))?;
let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
let mut raw = Vec::new();
decoder
.read_to_end(&mut raw)
.map_err(|e| format!("Decompress error: {}", e))?;
let null_pos = raw
.iter()
.position(|&b| b == 0)
.ok_or("Invalid Git object: no null byte")?;
let header = std::str::from_utf8(&raw[..null_pos]).map_err(|_| "Invalid Git object header")?;
let (obj_type, _size_str) = header
.split_once(' ')
.ok_or("Invalid Git object header format")?;
let mut sha1 = sha1::Sha1::new();
sha1.update(&raw);
let git_hash = hex::encode(sha1.finalize());
Ok((git_hash, obj_type.to_string(), raw[null_pos + 1..].to_vec()))
}
fn discover_loose_object(
path: &Path,
) -> Result<(String, DiscoveredObject), crate::errors::LitError> {
let (git_hash, obj_type, content) = read_loose_object(path)?;
let deps = git_dependencies(&obj_type, &content)?;
Ok((
git_hash,
DiscoveredObject {
source: ObjectSource::Loose(path.to_path_buf()),
obj_type,
deps,
},
))
}
fn git_dependencies(
obj_type: &str,
content: &[u8],
) -> Result<Vec<String>, crate::errors::LitError> {
Ok(match obj_type {
"tree" => git_tree_entries(content)?
.into_iter()
.map(|(_, _, hash)| hash)
.collect(),
"commit" => git_commit_refs(content),
"tag" => git_tag_target(content).into_iter().collect(),
_ => Vec::new(),
})
}
fn git_tag_target(content: &[u8]) -> Option<String> {
let text = std::str::from_utf8(content).ok()?;
git_header(text)
.lines()
.find_map(|line| line.strip_prefix("object "))
.map(|hash| hash.trim().to_string())
}
fn git_header(text: &str) -> &str {
git_header_and_message(text).0
}
fn git_header_and_message(text: &str) -> (&str, &str) {
text.split_once("\n\n").unwrap_or((text, ""))
}
fn import_subgraph(
root: &str,
discovered: &HashMap<String, DiscoveredObject>,
store: &ObjectStore,
hash_map: &mut HashMap<String, ObjectHash>,
scheduled: &mut HashSet<String>,
deltas_unresolved: u64,
) -> Result<u64, crate::errors::LitError> {
let mut imported = 0u64;
let mut stack = vec![Step::Visit(root.to_string())];
while let Some(step) = stack.pop() {
match step {
Step::Visit(git_hash) => {
if hash_map.contains_key(&git_hash) || !scheduled.insert(git_hash.clone()) {
continue;
}
let object = discovered
.get(&git_hash)
.ok_or_else(|| missing_object_error(&git_hash, deltas_unresolved))?;
let deps = object.deps.clone();
stack.push(Step::Emit(git_hash));
for dep in deps {
stack.push(Step::Visit(dep));
}
}
Step::Emit(git_hash) => {
let object = &discovered[&git_hash];
let content = object.content()?;
let lit_obj = match object.obj_type.as_str() {
"blob" => Object::Blob(Blob::new(content)),
"tree" => Object::Tree(parse_git_tree(&content, hash_map)?),
"commit" => Object::Commit(parse_git_commit(&content, hash_map)?),
"tag" => Object::Tag(parse_git_tag(&content, hash_map)?),
other => return Err(format!("Unknown object type: {}", other).into()),
};
let lit_hash = store.write(&lit_obj)?;
hash_map.insert(git_hash, lit_hash);
imported += 1;
}
}
}
Ok(imported)
}
fn missing_object_error(git_hash: &str, deltas_unresolved: u64) -> crate::errors::LitError {
let mut msg = format!(
"Git object {} is referenced but was not found in the source repository",
&git_hash[..8.min(git_hash.len())]
);
if deltas_unresolved > 0 {
msg.push_str(&format!(
". {} pack {} could not be resolved because the base {} not present \
— the source looks like a thin pack; fetch it with \
`git -C <source> index-pack --fix-thin` or unpack it first",
deltas_unresolved,
if deltas_unresolved == 1 {
"delta"
} else {
"deltas"
},
if deltas_unresolved == 1 {
"was"
} else {
"were"
}
));
}
crate::errors::LitError::general(msg)
}
fn lookup_lit_hash(
hash_map: &HashMap<String, ObjectHash>,
git_hash: &str,
context: &str,
) -> Result<ObjectHash, crate::errors::LitError> {
hash_map.get(git_hash).cloned().ok_or_else(|| {
crate::errors::LitError::general(format!(
"Cannot import {}: referenced Git object {} has not been converted",
context,
&git_hash[..8.min(git_hash.len())]
))
})
}
fn git_tree_entries(
content: &[u8],
) -> Result<Vec<(String, String, String)>, crate::errors::LitError> {
let mut entries = Vec::new();
let mut pos = 0;
while pos < content.len() {
let space_pos = content[pos..]
.iter()
.position(|&b| b == b' ')
.ok_or("Invalid tree entry: no space")?
+ pos;
let null_pos = content[space_pos..]
.iter()
.position(|&b| b == 0)
.ok_or("Invalid tree entry: no null")?
+ space_pos;
let mode = std::str::from_utf8(&content[pos..space_pos])
.map_err(|_| "Invalid mode in tree")?
.to_string();
let name = std::str::from_utf8(&content[space_pos + 1..null_pos])
.map_err(|_| "Invalid name in tree")?
.to_string();
if null_pos + 21 > content.len() {
break;
}
let git_hash = hex::encode(&content[null_pos + 1..null_pos + 21]);
entries.push((mode, name, git_hash));
pos = null_pos + 21;
}
Ok(entries)
}
fn parse_git_tree(
content: &[u8],
hash_map: &HashMap<String, ObjectHash>,
) -> Result<Tree, crate::errors::LitError> {
let mut tree = Tree::new();
for (mode, name, git_hash) in git_tree_entries(content)? {
let lit_hash = lookup_lit_hash(hash_map, &git_hash, &format!("tree entry '{}'", name))?;
let obj_type = if mode.starts_with("40") {
"tree"
} else {
"blob"
}
.to_string();
tree.add_entry(mode, name, lit_hash, obj_type);
}
Ok(tree)
}
fn git_commit_refs(content: &[u8]) -> Vec<String> {
let text = match std::str::from_utf8(content) {
Ok(t) => t,
Err(_) => return Vec::new(),
};
let mut refs = Vec::new();
for line in text.lines() {
if line.is_empty() {
break;
}
if let Some(rest) = line.strip_prefix("tree ") {
refs.push(rest.trim().to_string());
} else if let Some(rest) = line.strip_prefix("parent ") {
refs.push(rest.trim().to_string());
}
}
refs
}
fn parse_git_commit(
content: &[u8],
hash_map: &HashMap<String, ObjectHash>,
) -> Result<Commit, crate::errors::LitError> {
let text = std::str::from_utf8(content).map_err(|_| "Invalid commit: not UTF-8")?;
let mut tree_hash = String::new();
let mut parents = Vec::new();
let mut author = String::new();
let mut committer = String::new();
let mut timestamp: i64 = 0;
let mut timezone = None;
let (header, message) = git_header_and_message(text);
for line in header.lines() {
if let Some(rest) = line.strip_prefix("tree ") {
tree_hash = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("parent ") {
parents.push(rest.trim().to_string());
} else if let Some(rest) = line.strip_prefix("author ") {
let (name, ts, tz) = parse_git_ident(rest);
author = name;
timestamp = ts;
timezone = tz;
} else if let Some(rest) = line.strip_prefix("committer ") {
let (name, _, _) = parse_git_ident(rest);
committer = name;
}
}
let lit_tree = lookup_lit_hash(hash_map, &tree_hash, "commit tree")?;
let lit_parents: Vec<ObjectHash> = parents
.iter()
.map(|p| lookup_lit_hash(hash_map, p, "commit parent"))
.collect::<Result<_, _>>()?;
Ok(Commit {
tree: lit_tree,
parents: lit_parents,
author,
committer,
timestamp,
message: message.to_string(),
pq_signature: None,
metadata: None,
timezone,
})
}
fn parse_git_tag(
content: &[u8],
hash_map: &HashMap<String, ObjectHash>,
) -> Result<Tag, crate::errors::LitError> {
let text = std::str::from_utf8(content).map_err(|_| "Invalid tag: not UTF-8")?;
let (header, message) = git_header_and_message(text);
let mut target_hash = String::new();
let mut target_type = String::new();
let mut tag_name = String::new();
let mut tagger = String::new();
let mut timestamp: i64 = 0;
let mut timezone = None;
for line in header.lines() {
if let Some(rest) = line.strip_prefix("object ") {
target_hash = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("type ") {
target_type = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("tag ") {
tag_name = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("tagger ") {
let (name, ts, tz) = parse_git_ident(rest);
tagger = name;
timestamp = ts;
timezone = tz;
}
}
Ok(Tag {
target: lookup_lit_hash(hash_map, &target_hash, "tag target")?,
target_type,
tag_name,
tagger,
timestamp,
message: message.to_string(),
pq_signature: None,
metadata: None,
timezone,
})
}
fn parse_git_ident(ident: &str) -> (String, i64, Option<String>) {
if let Some(bracket_pos) = ident.rfind('>') {
let name_email = &ident[..=bracket_pos];
let rest = ident[bracket_pos + 1..].trim();
let mut fields = rest.split_whitespace();
let timestamp = fields
.next()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let timezone = fields.next().map(|tz| tz.to_string());
(name_email.trim().to_string(), timestamp, timezone)
} else {
(ident.to_string(), 0, None)
}
}
enum PackEntry {
Whole { obj_type: String, content: Vec<u8> },
OfsDelta { base_offset: usize, delta: Vec<u8> },
RefDelta { base: String, delta: Vec<u8> },
}
const MAX_DELTA_ROUNDS: usize = 1024;
fn read_pack_entries(
pack_path: &Path,
) -> Result<HashMap<usize, PackEntry>, crate::errors::LitError> {
let data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
if data.len() < 12 {
return Err("Pack file too small".into());
}
if &data[0..4] != b"PACK" {
return Err("Invalid pack file magic".into());
}
let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if version != 2 && version != 3 {
return Err(format!("Unsupported pack version: {}", version).into());
}
let num_objects = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let mut entries: HashMap<usize, PackEntry> = HashMap::new();
let mut pos = 12;
for _ in 0..num_objects {
if pos + 20 > data.len() {
break;
}
let start = pos;
match read_pack_entry(&data, &mut pos, start) {
Ok(entry) => {
entries.insert(start, entry);
}
Err(e) => {
eprintln!("Warning: skipping pack entry: {}", e);
break;
}
}
}
Ok(entries)
}
fn read_pack_entry(
data: &[u8],
pos: &mut usize,
entry_start: usize,
) -> Result<PackEntry, crate::errors::LitError> {
let mut byte = *data.get(*pos).ok_or("Unexpected end of pack")?;
let obj_type = (byte >> 4) & 0x07;
let mut _size: u64 = (byte & 0x0f) as u64;
let mut shift = 4;
*pos += 1;
while byte & 0x80 != 0 {
byte = *data.get(*pos).ok_or("Truncated pack header")?;
_size |= ((byte & 0x7f) as u64) << shift;
shift += 7;
*pos += 1;
}
match obj_type {
1..=4 => {
let obj_type = match obj_type {
1 => "commit",
2 => "tree",
3 => "blob",
4 => "tag",
_ => unreachable!(),
};
Ok(PackEntry::Whole {
obj_type: obj_type.to_string(),
content: inflate_at(data, pos)?,
})
}
6 => {
let mut byte = *data.get(*pos).ok_or("Truncated offset delta")?;
let mut back: u64 = (byte & 0x7f) as u64;
*pos += 1;
while byte & 0x80 != 0 {
byte = *data.get(*pos).ok_or("Truncated offset delta")?;
back = ((back + 1) << 7) | (byte & 0x7f) as u64;
*pos += 1;
}
let base_offset = entry_start
.checked_sub(back as usize)
.ok_or("Offset delta points before the start of the pack")?;
Ok(PackEntry::OfsDelta {
base_offset,
delta: inflate_at(data, pos)?,
})
}
7 => {
if *pos + 20 > data.len() {
return Err("Truncated ref delta".into());
}
let base = hex::encode(&data[*pos..*pos + 20]);
*pos += 20;
Ok(PackEntry::RefDelta {
base,
delta: inflate_at(data, pos)?,
})
}
_ => Err(format!("Unknown pack object type: {}", obj_type).into()),
}
}
fn inflate_at(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, crate::errors::LitError> {
let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
let mut out = Vec::new();
decoder
.read_to_end(&mut out)
.map_err(|e| format!("Decompress error: {}", e))?;
*pos += decoder.total_in() as usize;
Ok(out)
}
fn git_object_hash(obj_type: &str, content: &[u8]) -> String {
let mut sha1 = sha1::Sha1::new();
sha1.update(format!("{} {}\0", obj_type, content.len()).as_bytes());
sha1.update(content);
hex::encode(sha1.finalize())
}
fn resolve_all_packs(
packs: &[(PathBuf, HashMap<usize, PackEntry>)],
discovered: &mut HashMap<String, DiscoveredObject>,
deltas_unresolved: &mut u64,
) -> Result<u64, crate::errors::LitError> {
let mut keys: Vec<(usize, usize)> = Vec::new();
for (pack, entries) in packs.iter().enumerate() {
let mut offsets: Vec<usize> = entries.1.keys().copied().collect();
offsets.sort_unstable();
keys.extend(offsets.into_iter().map(|offset| (pack, offset)));
}
let mut resolved: HashMap<(usize, usize), (String, String, Vec<u8>)> = HashMap::new();
let mut by_hash: HashMap<String, (usize, usize)> = HashMap::new();
for _ in 0..MAX_DELTA_ROUNDS {
let mut progressed = false;
for &key in &keys {
if resolved.contains_key(&key) {
continue;
}
if let Some(object) = try_resolve_entry(key, packs, &resolved, &by_hash, discovered)? {
by_hash.insert(object.0.clone(), key);
resolved.insert(key, object);
progressed = true;
}
}
if !progressed {
break;
}
}
let mut recorded = 0u64;
for key in &keys {
match resolved.remove(key) {
Some((git_hash, obj_type, content)) => {
let deps = git_dependencies(&obj_type, &content)?;
discovered.insert(
git_hash,
DiscoveredObject {
source: ObjectSource::Packed(content),
obj_type,
deps,
},
);
recorded += 1;
}
None => *deltas_unresolved += 1,
}
}
Ok(recorded)
}
fn try_resolve_entry(
key: (usize, usize),
packs: &[(PathBuf, HashMap<usize, PackEntry>)],
resolved: &HashMap<(usize, usize), (String, String, Vec<u8>)>,
by_hash: &HashMap<String, (usize, usize)>,
discovered: &HashMap<String, DiscoveredObject>,
) -> Result<Option<(String, String, Vec<u8>)>, crate::errors::LitError> {
let (pack, offset) = key;
let entry = match packs
.get(pack)
.and_then(|(_, entries)| entries.get(&offset))
{
Some(entry) => entry,
None => return Ok(None),
};
let (obj_type, content) = match entry {
PackEntry::Whole { obj_type, content } => (obj_type.clone(), content.clone()),
PackEntry::OfsDelta { base_offset, delta } => match resolved.get(&(pack, *base_offset)) {
Some((_, base_type, base)) => (base_type.clone(), apply_delta(base, delta)?),
None => return Ok(None),
},
PackEntry::RefDelta { base, delta } => {
let resolved_base = by_hash
.get(base)
.and_then(|key| resolved.get(key))
.map(|(_, base_type, base)| (base_type.clone(), base.clone()));
match resolved_base {
Some((base_type, base_content)) => (base_type, apply_delta(&base_content, delta)?),
None => match discovered.get(base) {
Some(object) => (
object.obj_type.clone(),
apply_delta(&object.content()?, delta)?,
),
None => return Ok(None),
},
}
}
};
let git_hash = git_object_hash(&obj_type, &content);
Ok(Some((git_hash, obj_type, content)))
}
fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<u64, crate::errors::LitError> {
let mut value: u64 = 0;
let mut shift = 0;
loop {
let byte = *data.get(*pos).ok_or("Truncated delta size")?;
*pos += 1;
value |= ((byte & 0x7f) as u64) << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
shift += 7;
if shift > 63 {
return Err("Delta size overflows 64 bits".into());
}
}
}
fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>, crate::errors::LitError> {
let mut pos = 0;
let base_size = read_delta_varint(delta, &mut pos)?;
if base_size != base.len() as u64 {
return Err(format!(
"Delta expects a {}-byte base, but the base object is {} bytes",
base_size,
base.len()
)
.into());
}
let target_size = read_delta_varint(delta, &mut pos)?;
let mut out: Vec<u8> = Vec::with_capacity(target_size as usize);
while pos < delta.len() {
let instruction = delta[pos];
pos += 1;
if instruction & 0x80 != 0 {
let mut copy_offset: usize = 0;
for shift in 0..4 {
if instruction & (1 << shift) != 0 {
let byte = *delta.get(pos).ok_or("Truncated delta copy offset")?;
pos += 1;
copy_offset |= (byte as usize) << (8 * shift);
}
}
let mut copy_size: usize = 0;
for shift in 0..3 {
if instruction & (0x10 << shift) != 0 {
let byte = *delta.get(pos).ok_or("Truncated delta copy size")?;
pos += 1;
copy_size |= (byte as usize) << (8 * shift);
}
}
if copy_size == 0 {
copy_size = 0x10000; }
let end = copy_offset
.checked_add(copy_size)
.ok_or("Delta copy range overflows")?;
if end > base.len() {
return Err(format!(
"Delta copies bytes {}..{} from a {}-byte base",
copy_offset,
end,
base.len()
)
.into());
}
out.extend_from_slice(&base[copy_offset..end]);
} else if instruction != 0 {
let len = (instruction & 0x7f) as usize;
let end = pos.checked_add(len).ok_or("Delta insert range overflows")?;
if end > delta.len() {
return Err("Delta insert runs past the end of the delta".into());
}
out.extend_from_slice(&delta[pos..end]);
pos = end;
} else {
return Err("Delta contains a reserved 0x00 instruction".into());
}
}
if out.len() as u64 != target_size {
return Err(format!(
"Delta produced {} bytes, but its header declares {}",
out.len(),
target_size
)
.into());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn varint(mut value: u64) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
return out;
}
}
}
fn copy(offset: u32, size: u32) -> Vec<u8> {
let mut instruction = 0x80u8;
let mut operands = Vec::new();
for i in 0..4 {
let byte = ((offset >> (8 * i)) & 0xff) as u8;
if byte != 0 {
instruction |= 1 << i;
operands.push(byte);
}
}
for i in 0..3 {
let byte = ((size >> (8 * i)) & 0xff) as u8;
if byte != 0 {
instruction |= 0x10 << i;
operands.push(byte);
}
}
let mut out = vec![instruction];
out.extend(operands);
out
}
fn insert(data: &[u8]) -> Vec<u8> {
let mut out = vec![data.len() as u8];
out.extend_from_slice(data);
out
}
fn delta(base_len: u64, target_len: u64, body: &[Vec<u8>]) -> Vec<u8> {
let mut out = varint(base_len);
out.extend(varint(target_len));
for chunk in body {
out.extend_from_slice(chunk);
}
out
}
#[test]
fn apply_delta_inserts_literal_bytes() {
let d = delta(0, 5, &[insert(b"hello")]);
assert_eq!(apply_delta(b"", &d).unwrap(), b"hello");
}
#[test]
fn apply_delta_copies_from_base() {
let base = b"hello world";
let d = delta(
base.len() as u64,
11,
&[copy(6, 5), insert(b" "), copy(0, 5)],
);
assert_eq!(apply_delta(base, &d).unwrap(), b"world hello");
}
#[test]
fn apply_delta_treats_zero_size_as_64k() {
let base = vec![b'x'; 0x10000];
let d = delta(base.len() as u64, 0x10000, &[copy(0, 0x10000)]);
assert_eq!(apply_delta(&base, &d).unwrap(), base);
}
#[test]
fn apply_delta_rejects_a_base_of_the_wrong_size() {
let d = delta(99, 5, &[insert(b"hello")]);
let err = apply_delta(b"short", &d).unwrap_err();
let detail = err.internal_message();
assert!(
detail.contains("99"),
"error should name the expected size: {}",
detail
);
}
#[test]
fn apply_delta_rejects_a_copy_past_the_end_of_the_base() {
let base = b"tiny";
let d = delta(base.len() as u64, 100, &[copy(0, 100)]);
assert!(apply_delta(base, &d).is_err());
}
#[test]
fn apply_delta_rejects_the_reserved_instruction() {
let d = delta(0, 1, &[vec![0x00]]);
assert!(apply_delta(b"", &d).is_err());
}
#[test]
fn apply_delta_rejects_output_of_the_wrong_length() {
let d = delta(0, 10, &[insert(b"hello")]);
assert!(apply_delta(b"", &d).is_err());
}
#[test]
fn test_ref_delta_resolves_against_a_sibling_pack_in_either_order() {
let base_content = b"the base object contents".to_vec();
let base_hash = git_object_hash("blob", &base_content);
let suffix = b" plus more";
let body = delta(
base_content.len() as u64,
(base_content.len() + suffix.len()) as u64,
&[copy(0, base_content.len() as u32), insert(suffix)],
);
let build = |base_first: bool| -> Vec<(PathBuf, HashMap<usize, PackEntry>)> {
let base_pack: HashMap<usize, PackEntry> = [(
0usize,
PackEntry::Whole {
obj_type: "blob".to_string(),
content: base_content.clone(),
},
)]
.into_iter()
.collect();
let delta_pack: HashMap<usize, PackEntry> = [(
0usize,
PackEntry::RefDelta {
base: base_hash.clone(),
delta: body.clone(),
},
)]
.into_iter()
.collect();
if base_first {
vec![
(PathBuf::from("base.pack"), base_pack),
(PathBuf::from("delta.pack"), delta_pack),
]
} else {
vec![
(PathBuf::from("delta.pack"), delta_pack),
(PathBuf::from("base.pack"), base_pack),
]
}
};
let mut expected = base_content.clone();
expected.extend_from_slice(suffix);
let expected_hash = git_object_hash("blob", &expected);
for (label, packs) in [
("base pack first", build(true)),
("delta pack first", build(false)),
] {
let mut discovered = HashMap::new();
let mut unresolved = 0u64;
let recorded = resolve_all_packs(&packs, &mut discovered, &mut unresolved).unwrap();
assert_eq!(
unresolved, 0,
"{}: nothing should be left unresolved",
label
);
assert_eq!(recorded, 2, "{}: both objects should be recorded", label);
assert_eq!(
discovered.get(&expected_hash).map(|o| o.content().unwrap()),
Some(expected.clone()),
"{}: the delta should rebuild against the sibling pack's base",
label
);
}
}
}