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 parsed = gitparse::parse_tree(body).map_err(|e| {
424 BridgeError::from(Refusal::Unparsable {
425 object: hash20(id),
426 detail: format!("tree: {e}"),
427 })
428 })?;
429 if parsed.len() > mkit_core::serialize::MAX_TREE_ENTRIES as usize {
433 return Err(Refusal::TooManyTreeEntries {
434 object: hash20(id),
435 count: parsed.len(),
436 }
437 .into());
438 }
439 let mut entries = Vec::with_capacity(parsed.len());
440 for e in parsed {
441 let mode = match gitparse::map_mode(&e.mode) {
442 ModeMapping::Canonical(m) => m,
443 ModeMapping::Normalized(m) => {
444 if self.options.fork_mode {
445 return Err(Refusal::NormalizedModeInFork {
446 object: hash20(id),
447 mode: String::from_utf8_lossy(&e.mode).into_owned(),
448 }
449 .into());
450 }
451 *normalized = true;
452 m
453 }
454 ModeMapping::Gitlink => {
455 return Err(Refusal::Gitlink {
456 object: hash20(id),
457 path: String::from_utf8_lossy(&e.name).into_owned(),
458 }
459 .into());
460 }
461 ModeMapping::Unknown => {
462 return Err(Refusal::UnknownTreeMode {
463 object: hash20(id),
464 mode: String::from_utf8_lossy(&e.mode).into_owned(),
465 }
466 .into());
467 }
468 };
469 if !TreeEntry::validate_name(&e.name) {
470 return Err(Refusal::TreeEntryName {
471 object: hash20(id),
472 name: String::from_utf8_lossy(&e.name).into_owned(),
473 }
474 .into());
475 }
476 let child = self.object(&e.id, 0, depth + 1, new_pairs, normalized)?;
477 if let Some(kind) = self.sink.kind_of(&child) {
482 let ok = match mode {
483 EntryMode::Tree => kind == ObjectType::Tree,
484 _ => matches!(kind, ObjectType::Blob | ObjectType::ChunkedBlob),
485 };
486 if !ok {
487 return Err(Refusal::TreeEntryKind {
488 object: hash20(id),
489 name: String::from_utf8_lossy(&e.name).into_owned(),
490 }
491 .into());
492 }
493 }
494 entries.push(TreeEntry {
495 name: e.name,
496 mode,
497 object_hash: child,
498 });
499 }
500 entries.sort_by(|a, b| a.name.cmp(&b.name));
506 if entries.windows(2).any(|w| w[0].name == w[1].name) {
507 return Err(Refusal::DuplicateTreeEntry { object: hash20(id) }.into());
508 }
509 let bytes = ser(id, &Object::Tree(Tree { entries }))?;
510 self.sink.write_object(&bytes)
511 }
512
513 fn commit(
515 &mut self,
516 id: &Sha1Id,
517 body: &[u8],
518 new_pairs: &mut Vec<(Sha1Id, Hash)>,
519 normalized: &mut bool,
520 ) -> Result<Hash, BridgeError> {
521 let parsed = gitparse::parse_commit(body).map_err(|e| {
522 BridgeError::from(Refusal::Unparsable {
523 object: hash20(id),
524 detail: format!("commit: {e}"),
525 })
526 })?;
527 if parsed.committer.timestamp < 0 {
528 return Err(Refusal::NegativeTimestamp {
529 object: hash20(id),
530 timestamp: parsed.committer.timestamp,
531 }
532 .into());
533 }
534 if parsed.parents.len() > 1000 {
535 return Err(Refusal::TooManyParents { object: hash20(id) }.into());
536 }
537 if parsed.author.identity.is_empty() || parsed.author.identity.len() > 4096 {
538 return Err(Refusal::AuthorPayload { object: hash20(id) }.into());
539 }
540 let tree = self.object(&parsed.tree, 0, 0, new_pairs, normalized)?;
541 let mut parents = Vec::with_capacity(parsed.parents.len());
542 for p in &parsed.parents {
543 parents.push(self.object(p, 0, 0, new_pairs, normalized)?);
544 }
545 let raw = raw_git_bytes(GitObjKind::Commit, body);
548 (self.retain_raw)(id, &raw)?;
549
550 #[allow(clippy::cast_sign_loss)] let timestamp = parsed.committer.timestamp as u64;
552 let mut commit = Commit {
553 tree_hash: tree,
554 parents,
555 author: Identity::opaque(parsed.author.identity),
556 signer: self.signer.public,
557 message: parsed.message,
558 timestamp,
559 message_hash: mkit_core::hash::ZERO,
560 content_digest: mkit_core::hash::hash(&raw),
561 signature: [0u8; 64],
562 };
563 commit.signature = (self.signer.sign_commit)(&commit)?;
564 let bytes = ser(id, &Object::Commit(commit))?;
565 self.sink.write_object(&bytes)
566 }
567
568 fn tag(
570 &mut self,
571 id: &Sha1Id,
572 body: &[u8],
573 depth: usize,
574 new_pairs: &mut Vec<(Sha1Id, Hash)>,
575 normalized: &mut bool,
576 ) -> Result<Hash, BridgeError> {
577 let parsed = gitparse::parse_tag(body).map_err(|e| {
578 BridgeError::from(Refusal::Unparsable {
579 object: hash20(id),
580 detail: format!("tag: {e}"),
581 })
582 })?;
583 if crate::refname::check_tag_name(&parsed.name).is_err() {
584 return Err(Refusal::TagName { object: hash20(id) }.into());
585 }
586 let target_type = match parsed.target_type.as_slice() {
587 b"commit" => ObjectType::Commit,
588 b"tree" => ObjectType::Tree,
589 b"blob" => ObjectType::Blob,
590 b"tag" => ObjectType::Tag,
591 other => {
592 return Err(Refusal::Unparsable {
593 object: hash20(id),
594 detail: format!(
595 "tag target type {:?} unknown",
596 String::from_utf8_lossy(other)
597 ),
598 }
599 .into());
600 }
601 };
602 let target = self.object(&parsed.object, depth + 1, 0, new_pairs, normalized)?;
603 let actual = self.sink.kind_of(&target);
609 let target_type = match (target_type, actual) {
610 (ObjectType::Blob, Some(ObjectType::ChunkedBlob)) => ObjectType::ChunkedBlob,
611 (declared, Some(actual)) if actual != declared => {
612 return Err(Refusal::Unparsable {
613 object: hash20(id),
614 detail: format!(
615 "tag declares target type {declared:?} but the target is {actual:?}"
616 ),
617 }
618 .into());
619 }
620 (declared, _) => declared,
621 };
622 let (tagger_identity, timestamp) = match parsed.tagger {
623 Some(p) => {
624 if p.timestamp < 0 {
625 return Err(Refusal::NegativeTimestamp {
626 object: hash20(id),
627 timestamp: p.timestamp,
628 }
629 .into());
630 }
631 if p.identity.is_empty() || p.identity.len() > 4096 {
632 return Err(Refusal::AuthorPayload { object: hash20(id) }.into());
633 }
634 #[allow(clippy::cast_sign_loss)]
635 let ts = p.timestamp as u64;
636 (Identity::opaque(p.identity), ts)
637 }
638 None => (Identity::opaque(b"(no tagger)".to_vec()), 0),
641 };
642 let raw = raw_git_bytes(GitObjKind::Tag, body);
643 (self.retain_raw)(id, &raw)?;
644 let mut tag = Tag {
645 target,
646 target_type,
647 name: parsed.name,
648 tagger: tagger_identity,
649 signer: self.signer.public,
650 message: parsed.message,
651 timestamp,
652 signature: [0u8; 64],
653 };
654 tag.signature = (self.signer.sign_tag)(&tag)?;
655 let bytes = ser(id, &Object::Tag(tag))?;
656 self.sink.write_object(&bytes)
657 }
658}
659
660fn ser(id: &Sha1Id, obj: &Object) -> Result<Vec<u8>, BridgeError> {
665 mkit_core::serialize(obj).map_err(|e| {
666 Refusal::Unrepresentable {
667 object: hash20(id),
668 detail: e.to_string(),
669 }
670 .into()
671 })
672}
673
674fn raw_git_bytes(kind: GitObjKind, body: &[u8]) -> Vec<u8> {
680 crate::gitobj::GitObject {
681 gtype: kind.into(),
682 body: body.to_vec(),
683 }
684 .raw()
685}
686
687fn hash20(id: &Sha1Id) -> Hash {
690 let mut h = [0u8; 32];
691 h[..20].copy_from_slice(id);
692 h
693}