1use crate::author;
18use crate::error::BridgeError;
19use crate::gitobj::{GitObject, GitType, Sha1Id, sha1_from_hex};
20use crate::headers;
21use crate::translate;
22use mkit_core::object::{
23 Blob, ChunkedBlob, Commit, EntryMode, Object, ObjectType, Tag, Tree, TreeEntry,
24};
25use mkit_core::worktree::CHUNK_THRESHOLD;
26use mkit_core::{ChunkIterator, FastCdc, Hash};
27use std::collections::HashMap;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Reconstructed {
34 pub hash: Hash,
35 pub bytes: Vec<u8>,
36 pub object: Object,
37 pub extras: Vec<(Hash, Vec<u8>)>,
38}
39
40fn finish(object: Object, extras: Vec<(Hash, Vec<u8>)>) -> Result<Reconstructed, BridgeError> {
41 let bytes = mkit_core::serialize(&object)
42 .map_err(|e| BridgeError::Integrity(format!("reserialize: {e}")))?;
43 match mkit_core::deserialize(&bytes) {
49 Ok(round) if round == object => {}
50 Ok(_) => {
51 return Err(BridgeError::Integrity(
52 "reconstructed bytes round-trip to a different object".into(),
53 ));
54 }
55 Err(e) => {
56 return Err(BridgeError::NotBridgeObject(format!(
57 "reconstructed object is not legal under SPEC-OBJECTS: {e}"
58 )));
59 }
60 }
61 let hash = object
65 .id()
66 .map_err(|e| BridgeError::Integrity(format!("object id: {e}")))?;
67 Ok(Reconstructed {
68 hash,
69 bytes,
70 object,
71 extras,
72 })
73}
74
75pub fn reconstruct_blob(body: &[u8]) -> Result<Reconstructed, BridgeError> {
78 if body.len() as u64 <= CHUNK_THRESHOLD {
79 return finish(
80 Object::Blob(Blob {
81 data: body.to_vec(),
82 }),
83 Vec::new(),
84 );
85 }
86 let mut extras = Vec::new();
87 let chunks: Vec<Hash> = ChunkIterator::new(FastCdc::v1(), body)
88 .map(|b| {
89 let chunk = Object::Blob(Blob {
90 data: body[b.offset..b.offset + b.length].to_vec(),
91 });
92 let bytes = mkit_core::serialize(&chunk)
93 .map_err(|e| BridgeError::Integrity(format!("chunk serialize: {e}")))?;
94 let h = mkit_core::hash::hash(&bytes);
95 extras.push((h, bytes));
96 Ok::<_, BridgeError>(h)
97 })
98 .collect::<Result<_, _>>()?;
99 let manifest = Object::ChunkedBlob(ChunkedBlob {
100 total_size: body.len() as u64,
101 chunk_size: 0,
102 chunks,
103 });
104 finish(manifest, extras)
105}
106
107pub fn reconstruct_tree(
111 body: &[u8],
112 resolve: &impl Fn(&Sha1Id) -> Option<Hash>,
113) -> Result<Reconstructed, BridgeError> {
114 let mut entries = Vec::new();
115 let mut local: HashMap<Hash, Sha1Id> = HashMap::new();
116 let mut rest = body;
117 while !rest.is_empty() {
118 let sp = rest
119 .iter()
120 .position(|&b| b == b' ')
121 .ok_or_else(|| not_bridge("tree entry missing mode terminator"))?;
122 let mode = match &rest[..sp] {
123 b"100644" => EntryMode::Blob,
124 b"40000" => EntryMode::Tree,
125 b"120000" => EntryMode::Symlink,
126 b"100755" => EntryMode::Executable,
127 other => {
128 return Err(not_bridge(&format!(
129 "git tree mode {:?} has no mkit equivalent",
130 String::from_utf8_lossy(other)
131 )));
132 }
133 };
134 rest = &rest[sp + 1..];
135 let nul = rest
136 .iter()
137 .position(|&b| b == 0)
138 .ok_or_else(|| not_bridge("tree entry missing NUL"))?;
139 let name = rest[..nul].to_vec();
140 rest = &rest[nul + 1..];
141 if rest.len() < 20 {
142 return Err(not_bridge("tree entry truncated id"));
143 }
144 let mut id = [0u8; 20];
145 id.copy_from_slice(&rest[..20]);
146 rest = &rest[20..];
147 let child = resolve(&id).ok_or_else(|| not_bridge("tree child id not reconstructible"))?;
148 local.insert(child, id);
149 entries.push(TreeEntry {
150 name,
151 mode,
152 object_hash: child,
153 });
154 }
155 entries.sort_by(|a, b| a.name.cmp(&b.name));
157 let tree = Tree { entries };
158 let retrans = translate::translate_tree(&tree, &|h| local.get(h).copied())?;
160 if retrans.body != body {
161 return Err(BridgeError::Integrity(
162 "tree re-translation mismatch (not a bridge-emitted tree)".into(),
163 ));
164 }
165 finish(Object::Tree(tree), Vec::new())
166}
167
168pub fn reconstruct_commit(body: &[u8]) -> Result<Reconstructed, BridgeError> {
170 let parsed = ParsedBody::parse(body)?;
171 parsed.check_schema()?;
172 let tree_id = parsed.required_git_id("tree")?;
173 let parent_ids = parsed.all_git_ids("parent")?;
174 let author_line = parsed.required(b"author")?;
175 let timestamp = author::parse_timestamp(author_line)
176 .ok_or_else(|| not_bridge("author line is not bridge-synthesized"))?;
177 let identity = headers::parse_identity(parsed.required_str(headers::MKIT_AUTHOR)?)
178 .ok_or_else(|| not_bridge("mkit-author header malformed"))?;
179 let commit = Commit {
180 tree_hash: parsed.required_hash(headers::MKIT_TREE)?,
181 parents: parsed.all_hashes(headers::MKIT_PARENT)?,
182 author: identity,
183 signer: parsed.required_hash(headers::MKIT_SIGNER)?,
184 message: parsed.message.to_vec(),
185 timestamp,
186 message_hash: parsed
187 .optional_hash(headers::MKIT_MESSAGE_HASH)?
188 .unwrap_or(mkit_core::hash::ZERO),
189 content_digest: parsed
190 .optional_hash(headers::MKIT_CONTENT_DIGEST)?
191 .unwrap_or(mkit_core::hash::ZERO),
192 signature: parsed.required_signature(headers::MKIT_SIGNATURE)?,
193 };
194 if commit.parents.len() != parent_ids.len() {
195 return Err(not_bridge("parent / mkit-parent count mismatch"));
196 }
197 let probe = mkit_core::hash::ZERO; let retrans = translate::translate_commit(&probe, &commit, &tree_id, &parent_ids)?;
199 if retrans.body != body {
200 return Err(BridgeError::Integrity(
201 "commit re-translation mismatch (not a bridge-emitted commit)".into(),
202 ));
203 }
204 finish(Object::Commit(commit), Vec::new())
205}
206
207pub fn reconstruct_tag(body: &[u8]) -> Result<Reconstructed, BridgeError> {
209 let parsed = ParsedBody::parse(body)?;
210 parsed.check_schema()?;
211 let target_id = parsed.required_git_id("object")?;
212 let name = parsed.required(b"tag")?.to_vec();
213 let tagger_line = parsed.required(b"tagger")?;
214 let timestamp = author::parse_timestamp(tagger_line)
215 .ok_or_else(|| not_bridge("tagger line is not bridge-synthesized"))?;
216 let tagger = headers::parse_identity(parsed.required_str(headers::MKIT_TAGGER)?)
217 .ok_or_else(|| not_bridge("mkit-tagger header malformed"))?;
218 let tt_hex = parsed.required_str(headers::MKIT_TARGET_TYPE)?;
219 let tt_byte = crate::gitobj::bytes_from_hex(tt_hex, 1)
220 .ok_or_else(|| not_bridge("mkit-target-type malformed"))?[0];
221 let target_type = match tt_byte {
225 0x01 => ObjectType::Blob,
226 0x02 => ObjectType::Tree,
227 0x03 => ObjectType::Commit,
228 0x05 => ObjectType::ChunkedBlob,
229 0x07 => ObjectType::Tag,
230 _ => return Err(not_bridge("mkit-target-type not bridge-emittable")),
231 };
232 let tag = Tag {
233 target: parsed.required_hash(headers::MKIT_TARGET)?,
234 target_type,
235 name,
236 tagger,
237 signer: parsed.required_hash(headers::MKIT_SIGNER)?,
238 message: parsed.message.to_vec(),
239 timestamp,
240 signature: parsed.required_signature(headers::MKIT_SIGNATURE)?,
241 };
242 let probe = mkit_core::hash::ZERO;
243 let retrans = translate::translate_tag(&probe, &tag, &target_id)?;
244 if retrans.body != body {
245 return Err(BridgeError::Integrity(
246 "tag re-translation mismatch (not a bridge-emitted tag)".into(),
247 ));
248 }
249 finish(Object::Tag(tag), Vec::new())
250}
251
252pub fn reconstruct(
254 obj: &GitObject,
255 resolve: &impl Fn(&Sha1Id) -> Option<Hash>,
256) -> Result<Reconstructed, BridgeError> {
257 match obj.gtype {
258 GitType::Blob => reconstruct_blob(&obj.body),
259 GitType::Tree => reconstruct_tree(&obj.body, resolve),
260 GitType::Commit => reconstruct_commit(&obj.body),
261 GitType::Tag => reconstruct_tag(&obj.body),
262 }
263}
264
265fn not_bridge(msg: &str) -> BridgeError {
266 BridgeError::NotBridgeObject(msg.to_owned())
267}
268
269struct ParsedBody<'a> {
272 headers: Vec<(&'a [u8], &'a [u8])>,
273 message: &'a [u8],
274}
275
276impl<'a> ParsedBody<'a> {
277 fn parse(body: &'a [u8]) -> Result<Self, BridgeError> {
278 let split = body
279 .windows(2)
280 .position(|w| w == b"\n\n")
281 .ok_or_else(|| not_bridge("no header/message separator"))?;
282 let (head, message) = (&body[..=split], &body[split + 2..]);
283 let mut headers = Vec::new();
284 for line in head.split(|&b| b == b'\n').filter(|l| !l.is_empty()) {
285 if line.starts_with(b" ") {
286 return Err(not_bridge("continuation header line"));
288 }
289 let sp = line
290 .iter()
291 .position(|&b| b == b' ')
292 .ok_or_else(|| not_bridge("header line without value"))?;
293 let key = &line[..sp];
294 if headers::RESERVED.iter().any(|r| r.as_bytes() == key) {
295 return Err(not_bridge("reserved mkit-* header present"));
296 }
297 headers.push((key, &line[sp + 1..]));
298 }
299 Ok(Self { headers, message })
300 }
301
302 fn check_schema(&self) -> Result<(), BridgeError> {
305 match self.required_str(headers::MKIT_SCHEMA) {
306 Ok(v) if v == headers::SCHEMA_VALUE => Ok(()),
307 Ok(v) => Err(not_bridge(&format!(
308 "mkit-schema {v} is not covered by bridge mapping v1"
309 ))),
310 Err(_) => Err(not_bridge("missing mkit-schema header")),
311 }
312 }
313
314 fn all(&self, key: &[u8]) -> Vec<&'a [u8]> {
315 self.headers
316 .iter()
317 .filter(|(k, _)| *k == key)
318 .map(|(_, v)| *v)
319 .collect()
320 }
321
322 fn required(&self, key: &[u8]) -> Result<&'a [u8], BridgeError> {
323 match self.all(key).as_slice() {
324 [v] => Ok(v),
325 [] => Err(not_bridge(&format!(
326 "missing {}",
327 String::from_utf8_lossy(key)
328 ))),
329 _ => Err(not_bridge(&format!(
330 "duplicate {}",
331 String::from_utf8_lossy(key)
332 ))),
333 }
334 }
335
336 fn required_str(&self, key: &str) -> Result<&'a str, BridgeError> {
337 std::str::from_utf8(self.required(key.as_bytes())?)
338 .map_err(|_| not_bridge(&format!("{key} not UTF-8")))
339 }
340
341 fn required_git_id(&self, key: &str) -> Result<Sha1Id, BridgeError> {
342 sha1_from_hex(self.required_str(key)?)
343 .ok_or_else(|| not_bridge(&format!("{key} is not a 40-hex id")))
344 }
345
346 fn all_git_ids(&self, key: &str) -> Result<Vec<Sha1Id>, BridgeError> {
347 self.all(key.as_bytes())
348 .into_iter()
349 .map(|v| {
350 std::str::from_utf8(v)
351 .ok()
352 .and_then(sha1_from_hex)
353 .ok_or_else(|| not_bridge(&format!("{key} is not a 40-hex id")))
354 })
355 .collect()
356 }
357
358 fn required_hash(&self, key: &str) -> Result<[u8; 32], BridgeError> {
359 headers::parse_hash(self.required_str(key)?)
360 .ok_or_else(|| not_bridge(&format!("{key} is not a 64-hex hash")))
361 }
362
363 fn optional_hash(&self, key: &str) -> Result<Option<[u8; 32]>, BridgeError> {
364 match self.all(key.as_bytes()).as_slice() {
365 [] => Ok(None),
366 [v] => std::str::from_utf8(v)
367 .ok()
368 .and_then(headers::parse_hash)
369 .map(Some)
370 .ok_or_else(|| not_bridge(&format!("{key} is not a 64-hex hash"))),
371 _ => Err(not_bridge(&format!("duplicate {key}"))),
372 }
373 }
374
375 fn required_signature(&self, key: &str) -> Result<[u8; 64], BridgeError> {
376 headers::parse_signature(self.required_str(key)?)
377 .ok_or_else(|| not_bridge(&format!("{key} is not a 128-hex signature")))
378 }
379
380 fn all_hashes(&self, key: &str) -> Result<Vec<[u8; 32]>, BridgeError> {
381 self.all(key.as_bytes())
382 .into_iter()
383 .map(|v| {
384 std::str::from_utf8(v)
385 .ok()
386 .and_then(headers::parse_hash)
387 .ok_or_else(|| not_bridge(&format!("{key} is not a 64-hex hash")))
388 })
389 .collect()
390 }
391}