1use crate::core::{find_repo_root, Blob, Commit, Object, ObjectHash, Tag, Tree};
2use crate::response::ImportGitResponse;
3use crate::storage::ObjectStore;
4use sha1::Digest as Sha1Digest;
5use std::collections::{HashMap, HashSet};
6use std::fs;
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10pub fn execute(source: String) -> Result<ImportGitResponse, crate::errors::LitError> {
14 let source_path = PathBuf::from(&source);
15 let git_dir = find_git_dir(&source_path)?;
16
17 let repo_root = match find_repo_root() {
19 Ok(r) => r,
20 Err(_) => {
21 crate::commands::init::execute(false, None)?;
22 find_repo_root()?
23 }
24 };
25
26 let store = ObjectStore::new(&repo_root);
27 let mut hash_map: HashMap<String, ObjectHash> = HashMap::new();
28 let mut objects_imported = 0u64;
29 let mut refs_imported = 0u64;
30
31 let mut discovered: HashMap<String, DiscoveredObject> = HashMap::new();
38 let mut deltas_unresolved = 0u64;
39
40 let objects_dir = git_dir.join("objects");
42 if objects_dir.exists() {
43 for entry in walkdir::WalkDir::new(&objects_dir)
44 .min_depth(2)
45 .max_depth(2)
46 {
47 let entry = entry.map_err(|e| format!("Failed to walk objects: {}", e))?;
48 if !entry.file_type().is_file() {
49 continue;
50 }
51 let path = entry.path();
52 if let (Some(dir_name), Some(file_name)) =
54 (path.parent().and_then(|p| p.file_name()), path.file_name())
55 {
56 let dir_str = dir_name.to_string_lossy();
57 let file_str = file_name.to_string_lossy();
58 if dir_str == "pack" || dir_str == "info" {
60 continue;
61 }
62 let git_hash = format!("{}{}", dir_str, file_str);
63
64 match discover_loose_object(path) {
65 Ok((hash, object)) => {
66 discovered.insert(hash, object);
67 }
68 Err(e) => {
69 eprintln!("Warning: skipping object {}: {}", &git_hash[..8], e);
70 }
71 }
72 }
73 }
74 }
75
76 let mut packs: Vec<(PathBuf, HashMap<usize, PackEntry>)> = Vec::new();
78 let pack_dir = objects_dir.join("pack");
79 if pack_dir.exists() {
80 for entry in
81 fs::read_dir(&pack_dir).map_err(|e| format!("Failed to read pack dir: {}", e))?
82 {
83 let entry = entry.map_err(|e| format!("Pack dir entry error: {}", e))?;
84 let path = entry.path();
85 if path.extension().and_then(|e| e.to_str()) == Some("pack") {
86 match read_pack_entries(&path) {
87 Ok(entries) => packs.push((path, entries)),
88 Err(e) => eprintln!("Warning: skipping pack {}: {}", path.display(), e),
89 }
90 }
91 }
92 }
93
94 if !packs.is_empty() {
98 resolve_all_packs(&packs, &mut discovered, &mut deltas_unresolved)?;
99 }
100
101 let roots: Vec<String> = discovered.keys().cloned().collect();
103 let mut scheduled: HashSet<String> = HashSet::new();
104 for git_hash in &roots {
105 objects_imported += import_subgraph(
106 git_hash,
107 &discovered,
108 &store,
109 &mut hash_map,
110 &mut scheduled,
111 deltas_unresolved,
112 )?;
113 }
114
115 let refs_heads = git_dir.join("refs").join("heads");
118 if refs_heads.exists() {
119 for entry in walkdir::WalkDir::new(&refs_heads).min_depth(1) {
120 let entry = entry.map_err(|e| format!("Failed to walk refs: {}", e))?;
121 if !entry.file_type().is_file() {
122 continue;
123 }
124 let branch_name = entry
125 .path()
126 .strip_prefix(&refs_heads)
127 .map_err(|e| format!("Path error: {}", e))?
128 .to_string_lossy()
129 .replace('\\', "/");
130 let git_hash = fs::read_to_string(entry.path())
131 .map_err(|e| format!("Failed to read ref: {}", e))?
132 .trim()
133 .to_string();
134 if let Some(lit_hash) = hash_map.get(&git_hash) {
135 crate::core::write_ref(
136 &repo_root,
137 &format!("heads/{}", branch_name),
138 lit_hash.as_str(),
139 )?;
140 refs_imported += 1;
141 }
142 }
143 }
144
145 let refs_tags = git_dir.join("refs").join("tags");
147 if refs_tags.exists() {
148 for entry in walkdir::WalkDir::new(&refs_tags).min_depth(1) {
149 let entry = entry.map_err(|e| format!("Failed to walk tags: {}", e))?;
150 if !entry.file_type().is_file() {
151 continue;
152 }
153 let tag_name = entry
154 .path()
155 .strip_prefix(&refs_tags)
156 .map_err(|e| format!("Path error: {}", e))?
157 .to_string_lossy()
158 .replace('\\', "/");
159 let git_hash = fs::read_to_string(entry.path())
160 .map_err(|e| format!("Failed to read ref: {}", e))?
161 .trim()
162 .to_string();
163 if let Some(lit_hash) = hash_map.get(&git_hash) {
164 crate::core::write_ref(
165 &repo_root,
166 &format!("tags/{}", tag_name),
167 lit_hash.as_str(),
168 )?;
169 refs_imported += 1;
170 }
171 }
172 }
173
174 let head_path = git_dir.join("HEAD");
176 if head_path.exists() {
177 let head_content =
178 fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
179 let head_content = head_content.trim();
180 if let Some(ref_target) = head_content.strip_prefix("ref: refs/heads/") {
181 crate::core::update_head(&repo_root, ref_target)?;
182 }
183 }
184
185 let gitignore = source_path.join(".gitignore");
187 let litignore = repo_root.join(".litignore");
188 if gitignore.exists() && !litignore.exists() {
189 let _ = fs::copy(&gitignore, &litignore);
190 }
191
192 Ok(ImportGitResponse {
193 source: source.clone(),
194 objects_imported,
195 refs_imported,
196 hash_mapping_count: hash_map.len(),
197 message: format!(
198 "Imported {} objects and {} refs from Git repository",
199 objects_imported, refs_imported
200 ),
201 })
202}
203
204fn find_git_dir(path: &Path) -> Result<PathBuf, crate::errors::LitError> {
206 let dot_git = path.join(".git");
208 if dot_git.is_dir() {
209 return Ok(dot_git);
210 }
211 if path.join("objects").is_dir() && path.join("refs").is_dir() {
213 return Ok(path.to_path_buf());
214 }
215 Err(format!("Not a Git repository: {}", path.display()).into())
216}
217
218enum ObjectSource {
220 Loose(PathBuf),
222 Packed(Vec<u8>),
225}
226
227struct DiscoveredObject {
229 source: ObjectSource,
230 obj_type: String,
231 deps: Vec<String>,
233}
234
235impl DiscoveredObject {
236 fn content(&self) -> Result<Vec<u8>, crate::errors::LitError> {
238 match &self.source {
239 ObjectSource::Loose(path) => Ok(read_loose_object(path)?.2),
240 ObjectSource::Packed(content) => Ok(content.clone()),
241 }
242 }
243}
244
245enum Step {
247 Visit(String),
249 Emit(String),
251}
252
253fn read_loose_object(path: &Path) -> Result<(String, String, Vec<u8>), crate::errors::LitError> {
255 let compressed = fs::read(path).map_err(|e| format!("Read error: {}", e))?;
256
257 let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
259 let mut raw = Vec::new();
260 decoder
261 .read_to_end(&mut raw)
262 .map_err(|e| format!("Decompress error: {}", e))?;
263
264 let null_pos = raw
266 .iter()
267 .position(|&b| b == 0)
268 .ok_or("Invalid Git object: no null byte")?;
269 let header = std::str::from_utf8(&raw[..null_pos]).map_err(|_| "Invalid Git object header")?;
270 let (obj_type, _size_str) = header
271 .split_once(' ')
272 .ok_or("Invalid Git object header format")?;
273
274 let mut sha1 = sha1::Sha1::new();
276 sha1.update(&raw);
277 let git_hash = hex::encode(sha1.finalize());
278
279 Ok((git_hash, obj_type.to_string(), raw[null_pos + 1..].to_vec()))
280}
281
282fn discover_loose_object(
284 path: &Path,
285) -> Result<(String, DiscoveredObject), crate::errors::LitError> {
286 let (git_hash, obj_type, content) = read_loose_object(path)?;
287 let deps = git_dependencies(&obj_type, &content)?;
288 Ok((
289 git_hash,
290 DiscoveredObject {
291 source: ObjectSource::Loose(path.to_path_buf()),
292 obj_type,
293 deps,
294 },
295 ))
296}
297
298fn git_dependencies(
300 obj_type: &str,
301 content: &[u8],
302) -> Result<Vec<String>, crate::errors::LitError> {
303 Ok(match obj_type {
304 "tree" => git_tree_entries(content)?
305 .into_iter()
306 .map(|(_, _, hash)| hash)
307 .collect(),
308 "commit" => git_commit_refs(content),
309 "tag" => git_tag_target(content).into_iter().collect(),
310 _ => Vec::new(),
311 })
312}
313
314fn git_tag_target(content: &[u8]) -> Option<String> {
316 let text = std::str::from_utf8(content).ok()?;
317 git_header(text)
318 .lines()
319 .find_map(|line| line.strip_prefix("object "))
320 .map(|hash| hash.trim().to_string())
321}
322
323fn git_header(text: &str) -> &str {
325 git_header_and_message(text).0
326}
327
328fn git_header_and_message(text: &str) -> (&str, &str) {
333 text.split_once("\n\n").unwrap_or((text, ""))
334}
335
336fn import_subgraph(
342 root: &str,
343 discovered: &HashMap<String, DiscoveredObject>,
344 store: &ObjectStore,
345 hash_map: &mut HashMap<String, ObjectHash>,
346 scheduled: &mut HashSet<String>,
347 deltas_unresolved: u64,
348) -> Result<u64, crate::errors::LitError> {
349 let mut imported = 0u64;
350 let mut stack = vec![Step::Visit(root.to_string())];
351
352 while let Some(step) = stack.pop() {
353 match step {
354 Step::Visit(git_hash) => {
355 if hash_map.contains_key(&git_hash) || !scheduled.insert(git_hash.clone()) {
356 continue;
357 }
358 let object = discovered
359 .get(&git_hash)
360 .ok_or_else(|| missing_object_error(&git_hash, deltas_unresolved))?;
361 let deps = object.deps.clone();
362 stack.push(Step::Emit(git_hash));
363 for dep in deps {
364 stack.push(Step::Visit(dep));
365 }
366 }
367 Step::Emit(git_hash) => {
368 let object = &discovered[&git_hash];
369 let content = object.content()?;
370
371 let lit_obj = match object.obj_type.as_str() {
372 "blob" => Object::Blob(Blob::new(content)),
373 "tree" => Object::Tree(parse_git_tree(&content, hash_map)?),
374 "commit" => Object::Commit(parse_git_commit(&content, hash_map)?),
375 "tag" => Object::Tag(parse_git_tag(&content, hash_map)?),
376 other => return Err(format!("Unknown object type: {}", other).into()),
377 };
378
379 let lit_hash = store.write(&lit_obj)?;
380 hash_map.insert(git_hash, lit_hash);
381 imported += 1;
382 }
383 }
384 }
385
386 Ok(imported)
387}
388
389fn missing_object_error(git_hash: &str, deltas_unresolved: u64) -> crate::errors::LitError {
395 let mut msg = format!(
396 "Git object {} is referenced but was not found in the source repository",
397 &git_hash[..8.min(git_hash.len())]
398 );
399 if deltas_unresolved > 0 {
400 msg.push_str(&format!(
401 ". {} pack {} could not be resolved because the base {} not present \
402 — the source looks like a thin pack; fetch it with \
403 `git -C <source> index-pack --fix-thin` or unpack it first",
404 deltas_unresolved,
405 if deltas_unresolved == 1 {
406 "delta"
407 } else {
408 "deltas"
409 },
410 if deltas_unresolved == 1 {
411 "was"
412 } else {
413 "were"
414 }
415 ));
416 }
417 crate::errors::LitError::general(msg)
418}
419
420fn lookup_lit_hash(
422 hash_map: &HashMap<String, ObjectHash>,
423 git_hash: &str,
424 context: &str,
425) -> Result<ObjectHash, crate::errors::LitError> {
426 hash_map.get(git_hash).cloned().ok_or_else(|| {
427 crate::errors::LitError::general(format!(
428 "Cannot import {}: referenced Git object {} has not been converted",
429 context,
430 &git_hash[..8.min(git_hash.len())]
431 ))
432 })
433}
434
435fn git_tree_entries(
437 content: &[u8],
438) -> Result<Vec<(String, String, String)>, crate::errors::LitError> {
439 let mut entries = Vec::new();
440 let mut pos = 0;
441
442 while pos < content.len() {
443 let space_pos = content[pos..]
445 .iter()
446 .position(|&b| b == b' ')
447 .ok_or("Invalid tree entry: no space")?
448 + pos;
449 let null_pos = content[space_pos..]
450 .iter()
451 .position(|&b| b == 0)
452 .ok_or("Invalid tree entry: no null")?
453 + space_pos;
454
455 let mode = std::str::from_utf8(&content[pos..space_pos])
456 .map_err(|_| "Invalid mode in tree")?
457 .to_string();
458 let name = std::str::from_utf8(&content[space_pos + 1..null_pos])
459 .map_err(|_| "Invalid name in tree")?
460 .to_string();
461
462 if null_pos + 21 > content.len() {
463 break;
464 }
465 let git_hash = hex::encode(&content[null_pos + 1..null_pos + 21]);
466
467 entries.push((mode, name, git_hash));
468 pos = null_pos + 21;
469 }
470
471 Ok(entries)
472}
473
474fn parse_git_tree(
476 content: &[u8],
477 hash_map: &HashMap<String, ObjectHash>,
478) -> Result<Tree, crate::errors::LitError> {
479 let mut tree = Tree::new();
480
481 for (mode, name, git_hash) in git_tree_entries(content)? {
482 let lit_hash = lookup_lit_hash(hash_map, &git_hash, &format!("tree entry '{}'", name))?;
483
484 let obj_type = if mode.starts_with("40") {
485 "tree"
486 } else {
487 "blob"
488 }
489 .to_string();
490
491 tree.add_entry(mode, name, lit_hash, obj_type);
492 }
493
494 Ok(tree)
495}
496
497fn git_commit_refs(content: &[u8]) -> Vec<String> {
499 let text = match std::str::from_utf8(content) {
500 Ok(t) => t,
501 Err(_) => return Vec::new(),
502 };
503
504 let mut refs = Vec::new();
505 for line in text.lines() {
506 if line.is_empty() {
509 break;
510 }
511 if let Some(rest) = line.strip_prefix("tree ") {
512 refs.push(rest.trim().to_string());
513 } else if let Some(rest) = line.strip_prefix("parent ") {
514 refs.push(rest.trim().to_string());
515 }
516 }
517 refs
518}
519
520fn parse_git_commit(
522 content: &[u8],
523 hash_map: &HashMap<String, ObjectHash>,
524) -> Result<Commit, crate::errors::LitError> {
525 let text = std::str::from_utf8(content).map_err(|_| "Invalid commit: not UTF-8")?;
526
527 let mut tree_hash = String::new();
528 let mut parents = Vec::new();
529 let mut author = String::new();
530 let mut committer = String::new();
531 let mut timestamp: i64 = 0;
532 let mut timezone = None;
533
534 let (header, message) = git_header_and_message(text);
535
536 for line in header.lines() {
537 if let Some(rest) = line.strip_prefix("tree ") {
538 tree_hash = rest.trim().to_string();
539 } else if let Some(rest) = line.strip_prefix("parent ") {
540 parents.push(rest.trim().to_string());
541 } else if let Some(rest) = line.strip_prefix("author ") {
542 let (name, ts, tz) = parse_git_ident(rest);
543 author = name;
544 timestamp = ts;
545 timezone = tz;
546 } else if let Some(rest) = line.strip_prefix("committer ") {
547 let (name, _, _) = parse_git_ident(rest);
548 committer = name;
549 }
550 }
551
552 let lit_tree = lookup_lit_hash(hash_map, &tree_hash, "commit tree")?;
554
555 let lit_parents: Vec<ObjectHash> = parents
556 .iter()
557 .map(|p| lookup_lit_hash(hash_map, p, "commit parent"))
558 .collect::<Result<_, _>>()?;
559
560 Ok(Commit {
561 tree: lit_tree,
562 parents: lit_parents,
563 author,
564 committer,
565 timestamp,
566 message: message.to_string(),
567 pq_signature: None,
568 metadata: None,
569 timezone,
570 })
571}
572
573fn parse_git_tag(
575 content: &[u8],
576 hash_map: &HashMap<String, ObjectHash>,
577) -> Result<Tag, crate::errors::LitError> {
578 let text = std::str::from_utf8(content).map_err(|_| "Invalid tag: not UTF-8")?;
579 let (header, message) = git_header_and_message(text);
580
581 let mut target_hash = String::new();
582 let mut target_type = String::new();
583 let mut tag_name = String::new();
584 let mut tagger = String::new();
585 let mut timestamp: i64 = 0;
586 let mut timezone = None;
587
588 for line in header.lines() {
589 if let Some(rest) = line.strip_prefix("object ") {
590 target_hash = rest.trim().to_string();
591 } else if let Some(rest) = line.strip_prefix("type ") {
592 target_type = rest.trim().to_string();
593 } else if let Some(rest) = line.strip_prefix("tag ") {
594 tag_name = rest.trim().to_string();
595 } else if let Some(rest) = line.strip_prefix("tagger ") {
596 let (name, ts, tz) = parse_git_ident(rest);
597 tagger = name;
598 timestamp = ts;
599 timezone = tz;
600 }
601 }
602
603 Ok(Tag {
604 target: lookup_lit_hash(hash_map, &target_hash, "tag target")?,
605 target_type,
606 tag_name,
607 tagger,
608 timestamp,
609 message: message.to_string(),
610 pq_signature: None,
611 metadata: None,
612 timezone,
613 })
614}
615
616fn parse_git_ident(ident: &str) -> (String, i64, Option<String>) {
618 if let Some(bracket_pos) = ident.rfind('>') {
620 let name_email = &ident[..=bracket_pos];
621 let rest = ident[bracket_pos + 1..].trim();
622 let mut fields = rest.split_whitespace();
623 let timestamp = fields
624 .next()
625 .and_then(|s| s.parse::<i64>().ok())
626 .unwrap_or(0);
627 let timezone = fields.next().map(|tz| tz.to_string());
628 (name_email.trim().to_string(), timestamp, timezone)
629 } else {
630 (ident.to_string(), 0, None)
631 }
632}
633
634enum PackEntry {
636 Whole { obj_type: String, content: Vec<u8> },
638 OfsDelta { base_offset: usize, delta: Vec<u8> },
640 RefDelta { base: String, delta: Vec<u8> },
643}
644
645const MAX_DELTA_ROUNDS: usize = 1024;
651
652fn read_pack_entries(
654 pack_path: &Path,
655) -> Result<HashMap<usize, PackEntry>, crate::errors::LitError> {
656 let data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
657
658 if data.len() < 12 {
660 return Err("Pack file too small".into());
661 }
662 if &data[0..4] != b"PACK" {
663 return Err("Invalid pack file magic".into());
664 }
665 let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
666 if version != 2 && version != 3 {
667 return Err(format!("Unsupported pack version: {}", version).into());
668 }
669 let num_objects = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
670
671 let mut entries: HashMap<usize, PackEntry> = HashMap::new();
674 let mut pos = 12;
675 for _ in 0..num_objects {
676 if pos + 20 > data.len() {
678 break;
679 }
680 let start = pos;
681 match read_pack_entry(&data, &mut pos, start) {
682 Ok(entry) => {
683 entries.insert(start, entry);
684 }
685 Err(e) => {
686 eprintln!("Warning: skipping pack entry: {}", e);
687 break;
688 }
689 }
690 }
691
692 Ok(entries)
693}
694
695fn read_pack_entry(
700 data: &[u8],
701 pos: &mut usize,
702 entry_start: usize,
703) -> Result<PackEntry, crate::errors::LitError> {
704 let mut byte = *data.get(*pos).ok_or("Unexpected end of pack")?;
706 let obj_type = (byte >> 4) & 0x07;
707 let mut _size: u64 = (byte & 0x0f) as u64;
708 let mut shift = 4;
709 *pos += 1;
710
711 while byte & 0x80 != 0 {
712 byte = *data.get(*pos).ok_or("Truncated pack header")?;
713 _size |= ((byte & 0x7f) as u64) << shift;
714 shift += 7;
715 *pos += 1;
716 }
717
718 match obj_type {
719 1..=4 => {
720 let obj_type = match obj_type {
722 1 => "commit",
723 2 => "tree",
724 3 => "blob",
725 4 => "tag",
726 _ => unreachable!(),
727 };
728 Ok(PackEntry::Whole {
729 obj_type: obj_type.to_string(),
730 content: inflate_at(data, pos)?,
731 })
732 }
733 6 => {
734 let mut byte = *data.get(*pos).ok_or("Truncated offset delta")?;
737 let mut back: u64 = (byte & 0x7f) as u64;
738 *pos += 1;
739 while byte & 0x80 != 0 {
740 byte = *data.get(*pos).ok_or("Truncated offset delta")?;
741 back = ((back + 1) << 7) | (byte & 0x7f) as u64;
742 *pos += 1;
743 }
744 let base_offset = entry_start
745 .checked_sub(back as usize)
746 .ok_or("Offset delta points before the start of the pack")?;
747 Ok(PackEntry::OfsDelta {
748 base_offset,
749 delta: inflate_at(data, pos)?,
750 })
751 }
752 7 => {
753 if *pos + 20 > data.len() {
755 return Err("Truncated ref delta".into());
756 }
757 let base = hex::encode(&data[*pos..*pos + 20]);
758 *pos += 20;
759 Ok(PackEntry::RefDelta {
760 base,
761 delta: inflate_at(data, pos)?,
762 })
763 }
764 _ => Err(format!("Unknown pack object type: {}", obj_type).into()),
765 }
766}
767
768fn inflate_at(data: &[u8], pos: &mut usize) -> Result<Vec<u8>, crate::errors::LitError> {
770 let mut decoder = flate2::read::ZlibDecoder::new(&data[*pos..]);
771 let mut out = Vec::new();
772 decoder
773 .read_to_end(&mut out)
774 .map_err(|e| format!("Decompress error: {}", e))?;
775 *pos += decoder.total_in() as usize;
776 Ok(out)
777}
778
779fn git_object_hash(obj_type: &str, content: &[u8]) -> String {
781 let mut sha1 = sha1::Sha1::new();
782 sha1.update(format!("{} {}\0", obj_type, content.len()).as_bytes());
783 sha1.update(content);
784 hex::encode(sha1.finalize())
785}
786
787fn resolve_all_packs(
805 packs: &[(PathBuf, HashMap<usize, PackEntry>)],
806 discovered: &mut HashMap<String, DiscoveredObject>,
807 deltas_unresolved: &mut u64,
808) -> Result<u64, crate::errors::LitError> {
809 let mut keys: Vec<(usize, usize)> = Vec::new();
812 for (pack, entries) in packs.iter().enumerate() {
813 let mut offsets: Vec<usize> = entries.1.keys().copied().collect();
814 offsets.sort_unstable();
815 keys.extend(offsets.into_iter().map(|offset| (pack, offset)));
816 }
817
818 let mut resolved: HashMap<(usize, usize), (String, String, Vec<u8>)> = HashMap::new();
819 let mut by_hash: HashMap<String, (usize, usize)> = HashMap::new();
821
822 for _ in 0..MAX_DELTA_ROUNDS {
823 let mut progressed = false;
824 for &key in &keys {
825 if resolved.contains_key(&key) {
826 continue;
827 }
828 if let Some(object) = try_resolve_entry(key, packs, &resolved, &by_hash, discovered)? {
829 by_hash.insert(object.0.clone(), key);
830 resolved.insert(key, object);
831 progressed = true;
832 }
833 }
834 if !progressed {
835 break;
836 }
837 }
838
839 let mut recorded = 0u64;
840 for key in &keys {
841 match resolved.remove(key) {
842 Some((git_hash, obj_type, content)) => {
843 let deps = git_dependencies(&obj_type, &content)?;
844 discovered.insert(
845 git_hash,
846 DiscoveredObject {
847 source: ObjectSource::Packed(content),
848 obj_type,
849 deps,
850 },
851 );
852 recorded += 1;
853 }
854 None => *deltas_unresolved += 1,
855 }
856 }
857
858 Ok(recorded)
859}
860
861fn try_resolve_entry(
863 key: (usize, usize),
864 packs: &[(PathBuf, HashMap<usize, PackEntry>)],
865 resolved: &HashMap<(usize, usize), (String, String, Vec<u8>)>,
866 by_hash: &HashMap<String, (usize, usize)>,
867 discovered: &HashMap<String, DiscoveredObject>,
868) -> Result<Option<(String, String, Vec<u8>)>, crate::errors::LitError> {
869 let (pack, offset) = key;
870 let entry = match packs
871 .get(pack)
872 .and_then(|(_, entries)| entries.get(&offset))
873 {
874 Some(entry) => entry,
875 None => return Ok(None),
876 };
877
878 let (obj_type, content) = match entry {
879 PackEntry::Whole { obj_type, content } => (obj_type.clone(), content.clone()),
880 PackEntry::OfsDelta { base_offset, delta } => match resolved.get(&(pack, *base_offset)) {
882 Some((_, base_type, base)) => (base_type.clone(), apply_delta(base, delta)?),
883 None => return Ok(None),
884 },
885 PackEntry::RefDelta { base, delta } => {
886 let resolved_base = by_hash
889 .get(base)
890 .and_then(|key| resolved.get(key))
891 .map(|(_, base_type, base)| (base_type.clone(), base.clone()));
892
893 match resolved_base {
894 Some((base_type, base_content)) => (base_type, apply_delta(&base_content, delta)?),
895 None => match discovered.get(base) {
896 Some(object) => (
897 object.obj_type.clone(),
898 apply_delta(&object.content()?, delta)?,
899 ),
900 None => return Ok(None),
901 },
902 }
903 }
904 };
905
906 let git_hash = git_object_hash(&obj_type, &content);
907 Ok(Some((git_hash, obj_type, content)))
908}
909
910fn read_delta_varint(data: &[u8], pos: &mut usize) -> Result<u64, crate::errors::LitError> {
912 let mut value: u64 = 0;
913 let mut shift = 0;
914 loop {
915 let byte = *data.get(*pos).ok_or("Truncated delta size")?;
916 *pos += 1;
917 value |= ((byte & 0x7f) as u64) << shift;
918 if byte & 0x80 == 0 {
919 return Ok(value);
920 }
921 shift += 7;
922 if shift > 63 {
923 return Err("Delta size overflows 64 bits".into());
924 }
925 }
926}
927
928fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>, crate::errors::LitError> {
934 let mut pos = 0;
935
936 let base_size = read_delta_varint(delta, &mut pos)?;
937 if base_size != base.len() as u64 {
938 return Err(format!(
939 "Delta expects a {}-byte base, but the base object is {} bytes",
940 base_size,
941 base.len()
942 )
943 .into());
944 }
945 let target_size = read_delta_varint(delta, &mut pos)?;
946
947 let mut out: Vec<u8> = Vec::with_capacity(target_size as usize);
948 while pos < delta.len() {
949 let instruction = delta[pos];
950 pos += 1;
951
952 if instruction & 0x80 != 0 {
953 let mut copy_offset: usize = 0;
956 for shift in 0..4 {
957 if instruction & (1 << shift) != 0 {
958 let byte = *delta.get(pos).ok_or("Truncated delta copy offset")?;
959 pos += 1;
960 copy_offset |= (byte as usize) << (8 * shift);
961 }
962 }
963 let mut copy_size: usize = 0;
964 for shift in 0..3 {
965 if instruction & (0x10 << shift) != 0 {
966 let byte = *delta.get(pos).ok_or("Truncated delta copy size")?;
967 pos += 1;
968 copy_size |= (byte as usize) << (8 * shift);
969 }
970 }
971 if copy_size == 0 {
972 copy_size = 0x10000; }
974
975 let end = copy_offset
976 .checked_add(copy_size)
977 .ok_or("Delta copy range overflows")?;
978 if end > base.len() {
979 return Err(format!(
980 "Delta copies bytes {}..{} from a {}-byte base",
981 copy_offset,
982 end,
983 base.len()
984 )
985 .into());
986 }
987 out.extend_from_slice(&base[copy_offset..end]);
988 } else if instruction != 0 {
989 let len = (instruction & 0x7f) as usize;
991 let end = pos.checked_add(len).ok_or("Delta insert range overflows")?;
992 if end > delta.len() {
993 return Err("Delta insert runs past the end of the delta".into());
994 }
995 out.extend_from_slice(&delta[pos..end]);
996 pos = end;
997 } else {
998 return Err("Delta contains a reserved 0x00 instruction".into());
999 }
1000 }
1001
1002 if out.len() as u64 != target_size {
1003 return Err(format!(
1004 "Delta produced {} bytes, but its header declares {}",
1005 out.len(),
1006 target_size
1007 )
1008 .into());
1009 }
1010
1011 Ok(out)
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 fn varint(mut value: u64) -> Vec<u8> {
1020 let mut out = Vec::new();
1021 loop {
1022 let mut byte = (value & 0x7f) as u8;
1023 value >>= 7;
1024 if value != 0 {
1025 byte |= 0x80;
1026 }
1027 out.push(byte);
1028 if value == 0 {
1029 return out;
1030 }
1031 }
1032 }
1033
1034 fn copy(offset: u32, size: u32) -> Vec<u8> {
1039 let mut instruction = 0x80u8;
1040 let mut operands = Vec::new();
1041 for i in 0..4 {
1042 let byte = ((offset >> (8 * i)) & 0xff) as u8;
1043 if byte != 0 {
1044 instruction |= 1 << i;
1045 operands.push(byte);
1046 }
1047 }
1048 for i in 0..3 {
1049 let byte = ((size >> (8 * i)) & 0xff) as u8;
1050 if byte != 0 {
1051 instruction |= 0x10 << i;
1052 operands.push(byte);
1053 }
1054 }
1055 let mut out = vec![instruction];
1056 out.extend(operands);
1057 out
1058 }
1059
1060 fn insert(data: &[u8]) -> Vec<u8> {
1062 let mut out = vec![data.len() as u8];
1063 out.extend_from_slice(data);
1064 out
1065 }
1066
1067 fn delta(base_len: u64, target_len: u64, body: &[Vec<u8>]) -> Vec<u8> {
1069 let mut out = varint(base_len);
1070 out.extend(varint(target_len));
1071 for chunk in body {
1072 out.extend_from_slice(chunk);
1073 }
1074 out
1075 }
1076
1077 #[test]
1078 fn apply_delta_inserts_literal_bytes() {
1079 let d = delta(0, 5, &[insert(b"hello")]);
1080 assert_eq!(apply_delta(b"", &d).unwrap(), b"hello");
1081 }
1082
1083 #[test]
1084 fn apply_delta_copies_from_base() {
1085 let base = b"hello world";
1086 let d = delta(
1087 base.len() as u64,
1088 11,
1089 &[copy(6, 5), insert(b" "), copy(0, 5)],
1090 );
1091 assert_eq!(apply_delta(base, &d).unwrap(), b"world hello");
1092 }
1093
1094 #[test]
1095 fn apply_delta_treats_zero_size_as_64k() {
1096 let base = vec![b'x'; 0x10000];
1098 let d = delta(base.len() as u64, 0x10000, &[copy(0, 0x10000)]);
1099 assert_eq!(apply_delta(&base, &d).unwrap(), base);
1100 }
1101
1102 #[test]
1103 fn apply_delta_rejects_a_base_of_the_wrong_size() {
1104 let d = delta(99, 5, &[insert(b"hello")]);
1105 let err = apply_delta(b"short", &d).unwrap_err();
1106 let detail = err.internal_message();
1108 assert!(
1109 detail.contains("99"),
1110 "error should name the expected size: {}",
1111 detail
1112 );
1113 }
1114
1115 #[test]
1116 fn apply_delta_rejects_a_copy_past_the_end_of_the_base() {
1117 let base = b"tiny";
1118 let d = delta(base.len() as u64, 100, &[copy(0, 100)]);
1119 assert!(apply_delta(base, &d).is_err());
1120 }
1121
1122 #[test]
1123 fn apply_delta_rejects_the_reserved_instruction() {
1124 let d = delta(0, 1, &[vec![0x00]]);
1125 assert!(apply_delta(b"", &d).is_err());
1126 }
1127
1128 #[test]
1129 fn apply_delta_rejects_output_of_the_wrong_length() {
1130 let d = delta(0, 10, &[insert(b"hello")]);
1132 assert!(apply_delta(b"", &d).is_err());
1133 }
1134
1135 #[test]
1143 fn test_ref_delta_resolves_against_a_sibling_pack_in_either_order() {
1144 let base_content = b"the base object contents".to_vec();
1145 let base_hash = git_object_hash("blob", &base_content);
1146
1147 let suffix = b" plus more";
1149 let body = delta(
1150 base_content.len() as u64,
1151 (base_content.len() + suffix.len()) as u64,
1152 &[copy(0, base_content.len() as u32), insert(suffix)],
1153 );
1154
1155 let build = |base_first: bool| -> Vec<(PathBuf, HashMap<usize, PackEntry>)> {
1157 let base_pack: HashMap<usize, PackEntry> = [(
1158 0usize,
1159 PackEntry::Whole {
1160 obj_type: "blob".to_string(),
1161 content: base_content.clone(),
1162 },
1163 )]
1164 .into_iter()
1165 .collect();
1166 let delta_pack: HashMap<usize, PackEntry> = [(
1167 0usize,
1168 PackEntry::RefDelta {
1169 base: base_hash.clone(),
1170 delta: body.clone(),
1171 },
1172 )]
1173 .into_iter()
1174 .collect();
1175
1176 if base_first {
1177 vec![
1178 (PathBuf::from("base.pack"), base_pack),
1179 (PathBuf::from("delta.pack"), delta_pack),
1180 ]
1181 } else {
1182 vec![
1183 (PathBuf::from("delta.pack"), delta_pack),
1184 (PathBuf::from("base.pack"), base_pack),
1185 ]
1186 }
1187 };
1188
1189 let mut expected = base_content.clone();
1190 expected.extend_from_slice(suffix);
1191 let expected_hash = git_object_hash("blob", &expected);
1192
1193 for (label, packs) in [
1194 ("base pack first", build(true)),
1195 ("delta pack first", build(false)),
1196 ] {
1197 let mut discovered = HashMap::new();
1198 let mut unresolved = 0u64;
1199 let recorded = resolve_all_packs(&packs, &mut discovered, &mut unresolved).unwrap();
1200
1201 assert_eq!(
1202 unresolved, 0,
1203 "{}: nothing should be left unresolved",
1204 label
1205 );
1206 assert_eq!(recorded, 2, "{}: both objects should be recorded", label);
1207 assert_eq!(
1208 discovered.get(&expected_hash).map(|o| o.content().unwrap()),
1209 Some(expected.clone()),
1210 "{}: the delta should rebuild against the sibling pack's base",
1211 label
1212 );
1213 }
1214 }
1215}