1use crate::error::{BridgeError, Refusal};
15use crate::gitobj::Sha1Id;
16use crate::gitparse::{self, ModeMapping};
17use crate::gitsrc::{CatFileBatch, GitObjKind};
18use mkit_core::object::{
19 Blob, ChunkedBlob, Commit, EntryMode, Identity, Object, ObjectType, Tag, Tree, TreeEntry,
20};
21use mkit_core::{ChunkIterator, FastCdc, Hash};
22use std::collections::HashMap;
23
24pub const CHUNK_THRESHOLD: u64 = mkit_core::worktree::CHUNK_THRESHOLD;
26
27pub const MAX_TAG_CHAIN: usize = 16;
29
30pub const MAX_TREE_DEPTH: usize = 128;
33
34pub const IMPORT_SPEC_VERSION: u32 = 1;
36
37pub trait GitSource {
40 fn read_git(&mut self, id: &Sha1Id) -> Result<(GitObjKind, Vec<u8>), BridgeError>;
41}
42
43impl GitSource for CatFileBatch {
44 fn read_git(&mut self, id: &Sha1Id) -> Result<(GitObjKind, Vec<u8>), BridgeError> {
45 self.read(id)
46 }
47}
48
49#[derive(Debug, Default)]
51pub struct MemGitSource(pub HashMap<Sha1Id, (GitObjKind, Vec<u8>)>);
52
53impl MemGitSource {
54 pub fn put(&mut self, kind: GitObjKind, body: Vec<u8>) -> Sha1Id {
56 let id = crate::gitobj::GitObject {
57 gtype: kind.into(),
58 body: body.clone(),
59 }
60 .id();
61 self.0.insert(id, (kind, body));
62 id
63 }
64}
65
66impl GitSource for MemGitSource {
67 fn read_git(&mut self, id: &Sha1Id) -> Result<(GitObjKind, Vec<u8>), BridgeError> {
68 self.0
69 .get(id)
70 .cloned()
71 .ok_or_else(|| BridgeError::Source("object missing from memory source".into()))
72 }
73}
74
75pub trait ObjectSink {
79 fn write_object(&mut self, bytes: &[u8]) -> Result<Hash, BridgeError>;
80
81 fn kind_of(&self, _h: &Hash) -> Option<ObjectType> {
84 None
85 }
86}
87
88impl ObjectSink for mkit_core::ObjectStore {
89 fn write_object(&mut self, bytes: &[u8]) -> Result<Hash, BridgeError> {
90 self.write(bytes)
91 .map_err(|e| BridgeError::Source(format!("store write: {e}")))
92 }
93
94 fn kind_of(&self, h: &Hash) -> Option<ObjectType> {
95 self.read_object(h).ok().map(|o| o.object_type())
96 }
97}
98
99#[derive(Debug, Default)]
101pub struct MemSink(pub HashMap<Hash, Vec<u8>>);
102
103impl ObjectSink for MemSink {
104 fn write_object(&mut self, bytes: &[u8]) -> Result<Hash, BridgeError> {
105 let h = mkit_core::hash::hash(bytes);
106 self.0.insert(h, bytes.to_vec());
107 Ok(h)
108 }
109
110 fn kind_of(&self, h: &Hash) -> Option<ObjectType> {
111 self.0
112 .get(h)
113 .and_then(|b| mkit_core::deserialize(b).ok())
114 .map(|o| o.object_type())
115 }
116}
117
118pub type RetainRawFn<'f> = dyn FnMut(&Sha1Id, &[u8]) -> Result<(), BridgeError> + 'f;
122
123pub struct ImportSigner<'a> {
127 pub public: [u8; 32],
128 pub sign_commit: &'a mut dyn FnMut(&Commit) -> Result<[u8; 64], BridgeError>,
129 pub sign_tag: &'a mut dyn FnMut(&Tag) -> Result<[u8; 64], BridgeError>,
130}
131
132impl std::fmt::Debug for ImportSigner<'_> {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct("ImportSigner")
135 .field("public", &crate::gitobj::bytes_hex(&self.public))
136 .finish_non_exhaustive()
137 }
138}
139
140#[derive(Debug, Clone, Copy, Default)]
142pub struct ImportOptions {
143 pub fork_mode: bool,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct ImportedRef {
151 pub head: Hash,
153 pub new_pairs: Vec<(Sha1Id, Hash)>,
156 pub normalized_modes: bool,
158}
159
160pub struct Importer<'a, S: GitSource, K: ObjectSink> {
167 pub source: &'a mut S,
168 pub sink: &'a mut K,
169 pub signer: ImportSigner<'a>,
170 pub map: &'a mut HashMap<Sha1Id, Hash>,
171 pub retain_raw: &'a mut RetainRawFn<'a>,
174 pub options: ImportOptions,
175 pub depth_memo: DepthMemo,
178}
179
180#[derive(Debug, Default)]
185pub struct DepthMemo {
186 heights: HashMap<Sha1Id, usize>,
187 chains: HashMap<Sha1Id, usize>,
188}
189
190impl<S: GitSource, K: ObjectSink> std::fmt::Debug for Importer<'_, S, K> {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 f.debug_struct("Importer").finish_non_exhaustive()
193 }
194}
195
196impl<S: GitSource, K: ObjectSink> Importer<'_, S, K> {
197 pub fn import_ref(&mut self, tip: &Sha1Id) -> Result<ImportedRef, BridgeError> {
202 let mut new_pairs = Vec::new();
203 let mut normalized = false;
204 let head = self.object(tip, 0, 0, &mut new_pairs, &mut normalized)?;
205 Ok(ImportedRef {
206 head,
207 new_pairs,
208 normalized_modes: normalized,
209 })
210 }
211
212 pub fn import_commits(
225 &mut self,
226 order: &[Sha1Id],
227 tip: &Sha1Id,
228 new_pairs: &mut Vec<(Sha1Id, Hash)>,
229 normalized: &mut bool,
230 ) -> Result<Hash, BridgeError> {
231 for id in order {
232 self.object(id, 0, 0, new_pairs, normalized)?;
233 }
234 self.object(tip, 0, 0, new_pairs, normalized)
235 }
236
237 fn object(
239 &mut self,
240 id: &Sha1Id,
241 tag_depth: usize,
242 tree_depth: usize,
243 new_pairs: &mut Vec<(Sha1Id, Hash)>,
244 normalized: &mut bool,
245 ) -> Result<Hash, BridgeError> {
246 if let Some(h) = self.map.get(id).copied() {
247 self.check_hit_budget(id, &h, tag_depth, tree_depth)?;
253 return Ok(h);
254 }
255 let (kind, body) = self.source.read_git(id)?;
256 let h = match kind {
257 GitObjKind::Blob => self.blob(id, &body, new_pairs)?,
258 GitObjKind::Tree => {
259 if tree_depth >= MAX_TREE_DEPTH {
260 return Err(Refusal::TreeTooDeep { object: hash20(id) }.into());
261 }
262 self.tree(id, &body, tree_depth, new_pairs, normalized)?
263 }
264 GitObjKind::Commit => self.commit(id, &body, new_pairs, normalized)?,
265 GitObjKind::Tag => {
266 if tag_depth >= MAX_TAG_CHAIN {
267 return Err(Refusal::TagChain { object: hash20(id) }.into());
268 }
269 self.tag(id, &body, tag_depth, new_pairs, normalized)?
270 }
271 };
272 self.map.insert(*id, h);
273 new_pairs.push((*id, h));
274 Ok(h)
275 }
276
277 fn check_hit_budget(
281 &mut self,
282 id: &Sha1Id,
283 twin: &Hash,
284 tag_depth: usize,
285 tree_depth: usize,
286 ) -> Result<(), BridgeError> {
287 if tree_depth == 0 && tag_depth == 0 {
288 return Ok(());
289 }
290 match self.sink.kind_of(twin) {
291 Some(ObjectType::Tree) if tree_depth > 0 => {
292 let height = self.tree_height(id, MAX_TREE_DEPTH - tree_depth + 1)?;
293 if tree_depth + height > MAX_TREE_DEPTH {
294 return Err(Refusal::TreeTooDeep { object: hash20(id) }.into());
295 }
296 }
297 Some(ObjectType::Tag) if tag_depth > 0 => {
298 let len = self.tag_chain_len(id, MAX_TAG_CHAIN - tag_depth + 1)?;
299 if tag_depth + len > MAX_TAG_CHAIN {
300 return Err(Refusal::TagChain { object: hash20(id) }.into());
301 }
302 }
303 _ => {}
304 }
305 Ok(())
306 }
307
308 fn tree_height(&mut self, id: &Sha1Id, budget: usize) -> Result<usize, BridgeError> {
313 if let Some(h) = self.depth_memo.heights.get(id) {
314 return Ok(*h);
315 }
316 if budget == 0 {
317 return Ok(MAX_TREE_DEPTH + 1);
318 }
319 let (kind, body) = self.source.read_git(id)?;
320 if kind != GitObjKind::Tree {
321 return Ok(0);
322 }
323 let parsed =
324 gitparse::parse_tree(&body).map_err(|e| BridgeError::Source(format!("tree: {e}")))?;
325 let mut max_child = 0usize;
326 for e in parsed {
327 if gitparse::map_mode(&e.mode) == ModeMapping::Canonical(EntryMode::Tree)
328 || gitparse::map_mode(&e.mode) == ModeMapping::Normalized(EntryMode::Tree)
329 {
330 max_child = max_child.max(self.tree_height(&e.id, budget - 1)?);
331 if max_child > MAX_TREE_DEPTH {
332 break;
333 }
334 }
335 }
336 let h = 1 + max_child;
337 if h <= MAX_TREE_DEPTH {
338 self.depth_memo.heights.insert(*id, h);
342 }
343 Ok(h)
344 }
345
346 fn tag_chain_len(&mut self, id: &Sha1Id, budget: usize) -> Result<usize, BridgeError> {
349 if let Some(l) = self.depth_memo.chains.get(id) {
350 return Ok(*l);
351 }
352 if budget == 0 {
353 return Ok(MAX_TAG_CHAIN + 1);
354 }
355 let (kind, body) = self.source.read_git(id)?;
356 if kind != GitObjKind::Tag {
357 return Ok(0);
358 }
359 let parsed =
360 gitparse::parse_tag(&body).map_err(|e| BridgeError::Source(format!("tag: {e}")))?;
361 let len = 1 + self.tag_chain_len(&parsed.object, budget - 1)?;
362 if len <= MAX_TAG_CHAIN {
363 self.depth_memo.chains.insert(*id, len);
365 }
366 Ok(len)
367 }
368
369 fn blob(
371 &mut self,
372 id: &Sha1Id,
373 body: &[u8],
374 new_pairs: &mut Vec<(Sha1Id, Hash)>,
375 ) -> Result<Hash, BridgeError> {
376 let _ = new_pairs; if body.len() as u64 > mkit_core::worktree::MAX_FILE_BYTES {
378 return Err(Refusal::BlobTooLarge {
379 object: hash20(id),
380 size: body.len() as u64,
381 }
382 .into());
383 }
384 if body.len() as u64 <= CHUNK_THRESHOLD {
385 let bytes = ser(
386 id,
387 &Object::Blob(Blob {
388 data: body.to_vec(),
389 }),
390 )?;
391 return self.sink.write_object(&bytes);
392 }
393 let mut chunks = Vec::new();
394 for b in ChunkIterator::new(FastCdc::v1(), body) {
395 let chunk = ser(
396 id,
397 &Object::Blob(Blob {
398 data: body[b.offset..b.offset + b.length].to_vec(),
399 }),
400 )?;
401 chunks.push(self.sink.write_object(&chunk)?);
402 }
403 let manifest = ser(
404 id,
405 &Object::ChunkedBlob(ChunkedBlob {
406 total_size: body.len() as u64,
407 chunk_size: 0,
408 chunks,
409 }),
410 )?;
411 self.sink.write_object(&manifest)
412 }
413
414 fn tree(
416 &mut self,
417 id: &Sha1Id,
418 body: &[u8],
419 depth: usize,
420 new_pairs: &mut Vec<(Sha1Id, Hash)>,
421 normalized: &mut bool,
422 ) -> Result<Hash, BridgeError> {
423 let fork_mode = self.options.fork_mode;
424 let (tree, changed) = translate_tree_metadata(id, body, fork_mode, |child_id| {
425 let child = self.object(child_id, 0, depth + 1, new_pairs, normalized)?;
426 Ok((child, self.sink.kind_of(&child)))
427 })?;
428 *normalized |= changed;
429 let bytes = ser(id, &Object::Tree(tree))?;
430 self.sink.write_object(&bytes)
431 }
432
433 fn commit(
435 &mut self,
436 id: &Sha1Id,
437 body: &[u8],
438 new_pairs: &mut Vec<(Sha1Id, Hash)>,
439 normalized: &mut bool,
440 ) -> Result<Hash, BridgeError> {
441 let parsed = gitparse::parse_commit(body).map_err(|e| {
442 BridgeError::from(Refusal::Unparsable {
443 object: hash20(id),
444 detail: format!("commit: {e}"),
445 })
446 })?;
447 if parsed.committer.timestamp < 0 {
448 return Err(Refusal::NegativeTimestamp {
449 object: hash20(id),
450 timestamp: parsed.committer.timestamp,
451 }
452 .into());
453 }
454 if parsed.parents.len() > 1000 {
455 return Err(Refusal::TooManyParents { object: hash20(id) }.into());
456 }
457 if parsed.author.identity.is_empty() || parsed.author.identity.len() > 4096 {
458 return Err(Refusal::AuthorPayload { object: hash20(id) }.into());
459 }
460 let tree = self.object(&parsed.tree, 0, 0, new_pairs, normalized)?;
461 let mut parents = Vec::with_capacity(parsed.parents.len());
462 for p in &parsed.parents {
463 parents.push(self.object(p, 0, 0, new_pairs, normalized)?);
464 }
465 let raw = raw_git_bytes(GitObjKind::Commit, body);
468 (self.retain_raw)(id, &raw)?;
469
470 let mut commit = unsigned_commit(id, body, self.signer.public, tree, parents)?;
471 commit.signature = (self.signer.sign_commit)(&commit)?;
472 let bytes = ser(id, &Object::Commit(commit))?;
473 self.sink.write_object(&bytes)
474 }
475
476 fn tag(
478 &mut self,
479 id: &Sha1Id,
480 body: &[u8],
481 depth: usize,
482 new_pairs: &mut Vec<(Sha1Id, Hash)>,
483 normalized: &mut bool,
484 ) -> Result<Hash, BridgeError> {
485 let parsed = gitparse::parse_tag(body).map_err(|e| {
486 BridgeError::from(Refusal::Unparsable {
487 object: hash20(id),
488 detail: format!("tag: {e}"),
489 })
490 })?;
491 if crate::refname::check_tag_name(&parsed.name).is_err() {
492 return Err(Refusal::TagName { object: hash20(id) }.into());
493 }
494 let target = self.object(&parsed.object, depth + 1, 0, new_pairs, normalized)?;
495 let mut tag = unsigned_tag(
496 id,
497 body,
498 self.signer.public,
499 target,
500 self.sink.kind_of(&target),
501 )?;
502 let raw = raw_git_bytes(GitObjKind::Tag, body);
503 (self.retain_raw)(id, &raw)?;
504 tag.signature = (self.signer.sign_tag)(&tag)?;
505 let bytes = ser(id, &Object::Tag(tag))?;
506 self.sink.write_object(&bytes)
507 }
508}
509
510pub fn translate_tree_metadata(
517 id: &Sha1Id,
518 body: &[u8],
519 fork_mode: bool,
520 mut resolve: impl FnMut(&Sha1Id) -> Result<(Hash, Option<ObjectType>), BridgeError>,
521) -> Result<(Tree, bool), BridgeError> {
522 let parsed = gitparse::parse_tree(body).map_err(|e| {
523 BridgeError::from(Refusal::Unparsable {
524 object: hash20(id),
525 detail: format!("tree: {e}"),
526 })
527 })?;
528 if parsed.len() > mkit_core::serialize::MAX_TREE_ENTRIES as usize {
532 return Err(Refusal::TooManyTreeEntries {
533 object: hash20(id),
534 count: parsed.len(),
535 }
536 .into());
537 }
538 let mut normalized = false;
539 let mut entries = Vec::with_capacity(parsed.len());
540 for e in parsed {
541 let mode = match gitparse::map_mode(&e.mode) {
542 ModeMapping::Canonical(m) => m,
543 ModeMapping::Normalized(m) => {
544 if fork_mode {
545 return Err(Refusal::NormalizedModeInFork {
546 object: hash20(id),
547 mode: String::from_utf8_lossy(&e.mode).into_owned(),
548 }
549 .into());
550 }
551 normalized = true;
552 m
553 }
554 ModeMapping::Gitlink => {
555 return Err(Refusal::Gitlink {
556 object: hash20(id),
557 path: String::from_utf8_lossy(&e.name).into_owned(),
558 }
559 .into());
560 }
561 ModeMapping::Unknown => {
562 return Err(Refusal::UnknownTreeMode {
563 object: hash20(id),
564 mode: String::from_utf8_lossy(&e.mode).into_owned(),
565 }
566 .into());
567 }
568 };
569 if !TreeEntry::validate_name(&e.name) {
570 return Err(Refusal::TreeEntryName {
571 object: hash20(id),
572 name: String::from_utf8_lossy(&e.name).into_owned(),
573 }
574 .into());
575 }
576 let (child, actual_kind) = resolve(&e.id)?;
577 if let Some(kind) = actual_kind {
582 let ok = match mode {
583 EntryMode::Tree => kind == ObjectType::Tree,
584 _ => matches!(kind, ObjectType::Blob | ObjectType::ChunkedBlob),
585 };
586 if !ok {
587 return Err(Refusal::TreeEntryKind {
588 object: hash20(id),
589 name: String::from_utf8_lossy(&e.name).into_owned(),
590 }
591 .into());
592 }
593 }
594 entries.push(TreeEntry {
595 name: e.name,
596 mode,
597 object_hash: child,
598 });
599 }
600 entries.sort_by(|a, b| a.name.cmp(&b.name));
606 if entries.windows(2).any(|w| w[0].name == w[1].name) {
607 return Err(Refusal::DuplicateTreeEntry { object: hash20(id) }.into());
608 }
609 Ok((Tree { entries }, normalized))
610}
611
612pub fn unsigned_commit(
618 id: &Sha1Id,
619 body: &[u8],
620 signer: [u8; 32],
621 tree: Hash,
622 parents: Vec<Hash>,
623) -> Result<Commit, BridgeError> {
624 let parsed =
625 gitparse::parse_commit(body).map_err(|e| BridgeError::Integrity(format!("commit: {e}")))?;
626 if parsed.committer.timestamp < 0 {
627 return Err(Refusal::NegativeTimestamp {
628 object: hash20(id),
629 timestamp: parsed.committer.timestamp,
630 }
631 .into());
632 }
633 if parsed.parents.len() > 1000 {
634 return Err(Refusal::TooManyParents { object: hash20(id) }.into());
635 }
636 if parsed.author.identity.is_empty() || parsed.author.identity.len() > 4096 {
637 return Err(Refusal::AuthorPayload { object: hash20(id) }.into());
638 }
639 if parents.len() != parsed.parents.len() {
640 return Err(BridgeError::Integrity(
641 "incorrect imported parent count".into(),
642 ));
643 }
644 let raw = raw_git_bytes(GitObjKind::Commit, body);
645 #[allow(clippy::cast_sign_loss)] let timestamp = parsed.committer.timestamp as u64;
647 let commit = Commit {
648 tree_hash: tree,
649 parents,
650 author: Identity::opaque(parsed.author.identity),
651 signer,
652 message: parsed.message,
653 timestamp,
654 message_hash: mkit_core::hash::ZERO,
655 content_digest: mkit_core::hash::hash(&raw),
656 signature: [0u8; 64],
657 };
658 Ok(commit)
659}
660
661pub fn unsigned_tag(
666 id: &Sha1Id,
667 body: &[u8],
668 signer: [u8; 32],
669 target: Hash,
670 actual: Option<ObjectType>,
671) -> Result<Tag, BridgeError> {
672 let parsed =
673 gitparse::parse_tag(body).map_err(|e| BridgeError::Integrity(format!("tag: {e}")))?;
674 if crate::refname::check_tag_name(&parsed.name).is_err() {
675 return Err(Refusal::TagName { object: hash20(id) }.into());
676 }
677 let target_type = match parsed.target_type.as_slice() {
678 b"commit" => ObjectType::Commit,
679 b"tree" => ObjectType::Tree,
680 b"blob" => ObjectType::Blob,
681 b"tag" => ObjectType::Tag,
682 other => {
683 return Err(Refusal::Unparsable {
684 object: hash20(id),
685 detail: format!(
686 "tag target type {:?} unknown",
687 String::from_utf8_lossy(other)
688 ),
689 }
690 .into());
691 }
692 };
693 let target_type = match (target_type, actual) {
699 (ObjectType::Blob, Some(ObjectType::ChunkedBlob)) => ObjectType::ChunkedBlob,
700 (declared, Some(actual)) if actual != declared => {
701 return Err(Refusal::Unparsable {
702 object: hash20(id),
703 detail: format!(
704 "tag declares target type {declared:?} but the target is {actual:?}"
705 ),
706 }
707 .into());
708 }
709 (declared, _) => declared,
710 };
711 let (tagger_identity, timestamp) = match parsed.tagger {
712 Some(p) => {
713 if p.timestamp < 0 {
714 return Err(Refusal::NegativeTimestamp {
715 object: hash20(id),
716 timestamp: p.timestamp,
717 }
718 .into());
719 }
720 if p.identity.is_empty() || p.identity.len() > 4096 {
721 return Err(Refusal::AuthorPayload { object: hash20(id) }.into());
722 }
723 #[allow(clippy::cast_sign_loss)]
724 let ts = p.timestamp as u64;
725 (Identity::opaque(p.identity), ts)
726 }
727 None => (Identity::opaque(b"(no tagger)".to_vec()), 0),
730 };
731 let tag = Tag {
732 target,
733 target_type,
734 name: parsed.name,
735 tagger: tagger_identity,
736 signer,
737 message: parsed.message,
738 timestamp,
739 signature: [0u8; 64],
740 };
741 Ok(tag)
742}
743
744fn ser(id: &Sha1Id, obj: &Object) -> Result<Vec<u8>, BridgeError> {
749 mkit_core::serialize(obj).map_err(|e| {
750 Refusal::Unrepresentable {
751 object: hash20(id),
752 detail: e.to_string(),
753 }
754 .into()
755 })
756}
757
758fn raw_git_bytes(kind: GitObjKind, body: &[u8]) -> Vec<u8> {
764 crate::gitobj::GitObject {
765 gtype: kind.into(),
766 body: body.to_vec(),
767 }
768 .raw()
769}
770
771fn hash20(id: &Sha1Id) -> Hash {
774 let mut h = [0u8; 32];
775 h[..20].copy_from_slice(id);
776 h
777}