1use std::fmt;
22use std::str::FromStr;
23
24use crate::error::{Error, Result};
25
26#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct ObjectId([u8; 20]);
29
30impl ObjectId {
31 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
37 let arr: [u8; 20] = bytes
38 .try_into()
39 .map_err(|_| Error::InvalidObjectId(hex::encode(bytes)))?;
40 Ok(Self(arr))
41 }
42
43 #[must_use]
45 pub fn as_bytes(&self) -> &[u8; 20] {
46 &self.0
47 }
48
49 #[must_use]
51 pub fn is_zero(&self) -> bool {
52 self.0 == [0u8; 20]
53 }
54
55 #[must_use]
57 pub fn to_hex(&self) -> String {
58 hex::encode(self.0)
59 }
60
61 #[must_use]
65 pub fn loose_prefix(&self) -> String {
66 hex::encode(&self.0[..1])
67 }
68
69 pub fn from_hex(s: &str) -> Result<Self> {
76 s.parse()
77 }
78
79 #[must_use]
81 pub fn loose_suffix(&self) -> String {
82 hex::encode(&self.0[1..])
83 }
84}
85
86impl fmt::Display for ObjectId {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(&self.to_hex())
89 }
90}
91
92impl fmt::Debug for ObjectId {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "ObjectId({})", self.to_hex())
95 }
96}
97
98impl FromStr for ObjectId {
99 type Err = Error;
100
101 fn from_str(s: &str) -> Result<Self> {
102 if s.len() != 40 {
103 return Err(Error::InvalidObjectId(s.to_owned()));
104 }
105 let bytes = hex::decode(s).map_err(|_| Error::InvalidObjectId(s.to_owned()))?;
106 Self::from_bytes(&bytes)
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum ObjectKind {
113 Blob,
115 Tree,
117 Commit,
119 Tag,
121}
122
123impl ObjectKind {
124 pub fn from_bytes(b: &[u8]) -> Result<Self> {
130 match b {
131 b"blob" => Ok(Self::Blob),
132 b"tree" => Ok(Self::Tree),
133 b"commit" => Ok(Self::Commit),
134 b"tag" => Ok(Self::Tag),
135 other => Err(Error::UnknownObjectType(
136 String::from_utf8_lossy(other).into_owned(),
137 )),
138 }
139 }
140
141 #[must_use]
143 pub fn as_str(&self) -> &'static str {
144 match self {
145 Self::Blob => "blob",
146 Self::Tree => "tree",
147 Self::Commit => "commit",
148 Self::Tag => "tag",
149 }
150 }
151}
152
153impl fmt::Display for ObjectKind {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str(self.as_str())
156 }
157}
158
159impl FromStr for ObjectKind {
160 type Err = Error;
161
162 fn from_str(s: &str) -> Result<Self> {
163 Self::from_bytes(s.as_bytes())
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct Object {
170 pub kind: ObjectKind,
172 pub data: Vec<u8>,
174}
175
176impl Object {
177 #[must_use]
179 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
180 Self { kind, data }
181 }
182
183 #[must_use]
185 pub fn to_store_bytes(&self) -> Vec<u8> {
186 let header = format!("{} {}\0", self.kind, self.data.len());
187 let mut out = Vec::with_capacity(header.len() + self.data.len());
188 out.extend_from_slice(header.as_bytes());
189 out.extend_from_slice(&self.data);
190 out
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct TreeEntry {
197 pub mode: u32,
199 pub name: Vec<u8>,
201 pub oid: ObjectId,
203}
204
205impl TreeEntry {
206 #[must_use]
210 pub fn mode_str(&self) -> String {
211 if self.mode == 0o040000 {
213 "40000".to_owned()
214 } else {
215 format!("{:o}", self.mode)
216 }
217 }
218}
219
220pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
231 let mut entries = Vec::new();
232 let mut pos = 0;
233
234 while pos < data.len() {
235 let sp = data[pos..]
237 .iter()
238 .position(|&b| b == b' ')
239 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
240 let mode_bytes = &data[pos..pos + sp];
241 let mode = std::str::from_utf8(mode_bytes)
242 .ok()
243 .and_then(|s| u32::from_str_radix(s, 8).ok())
244 .ok_or_else(|| {
245 Error::CorruptObject(format!(
246 "invalid tree mode: {}",
247 String::from_utf8_lossy(mode_bytes)
248 ))
249 })?;
250 pos += sp + 1;
251
252 let nul = data[pos..]
254 .iter()
255 .position(|&b| b == 0)
256 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
257 let name = data[pos..pos + nul].to_vec();
258 pos += nul + 1;
259
260 if pos + 20 > data.len() {
261 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
262 }
263 let oid = ObjectId::from_bytes(&data[pos..pos + 20])?;
264 pos += 20;
265
266 entries.push(TreeEntry { mode, name, oid });
267 }
268
269 Ok(entries)
270}
271
272#[must_use]
277pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
278 let mut out = Vec::new();
279 for e in entries {
280 out.extend_from_slice(e.mode_str().as_bytes());
281 out.push(b' ');
282 out.extend_from_slice(&e.name);
283 out.push(0);
284 out.extend_from_slice(e.oid.as_bytes());
285 }
286 out
287}
288
289#[must_use]
302pub fn tree_entry_cmp(
303 a_name: &[u8],
304 a_is_tree: bool,
305 b_name: &[u8],
306 b_is_tree: bool,
307) -> std::cmp::Ordering {
308 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
309 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
310
311 let min_len = a_name.len().min(b_name.len());
312 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
313 if cmp != std::cmp::Ordering::Equal {
314 return cmp;
315 }
316 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
318 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
319 ac.cmp(&bc)
320}
321
322#[derive(Debug, Clone)]
324pub struct CommitData {
325 pub tree: ObjectId,
327 pub parents: Vec<ObjectId>,
329 pub author: String,
331 pub committer: String,
333 pub encoding: Option<String>,
335 pub message: String,
337 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
340 pub raw_message: Option<Vec<u8>>,
341}
342
343pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
349 let text = String::from_utf8_lossy(data);
353
354 let mut tree = None;
355 let mut parents = Vec::new();
356 let mut author = None;
357 let mut committer = None;
358 let mut encoding = None;
359 let mut message = String::new();
360 let mut in_message = false;
361
362 for line in text.split('\n') {
363 if in_message {
364 message.push_str(line);
365 message.push('\n');
366 continue;
367 }
368 if line.is_empty() {
369 in_message = true;
370 continue;
371 }
372 if let Some(rest) = line.strip_prefix("tree ") {
373 tree = Some(rest.trim().parse::<ObjectId>()?);
374 } else if let Some(rest) = line.strip_prefix("parent ") {
375 parents.push(rest.trim().parse::<ObjectId>()?);
376 } else if let Some(rest) = line.strip_prefix("author ") {
377 author = Some(rest.to_owned());
378 } else if let Some(rest) = line.strip_prefix("committer ") {
379 committer = Some(rest.to_owned());
380 } else if let Some(rest) = line.strip_prefix("encoding ") {
381 encoding = Some(rest.to_owned());
382 }
383 }
384
385 if message.ends_with('\n') {
387 message.pop();
388 }
389
390 Ok(CommitData {
391 tree: tree.ok_or_else(|| Error::CorruptObject("commit missing tree header".to_owned()))?,
392 parents,
393 author: author
394 .ok_or_else(|| Error::CorruptObject("commit missing author header".to_owned()))?,
395 committer: committer
396 .ok_or_else(|| Error::CorruptObject("commit missing committer header".to_owned()))?,
397 encoding,
398 message,
399 raw_message: None,
400 })
401}
402
403#[derive(Debug, Clone)]
405pub struct TagData {
406 pub object: ObjectId,
408 pub object_type: String,
410 pub tag: String,
412 pub tagger: Option<String>,
414 pub message: String,
416}
417
418pub fn parse_tag(data: &[u8]) -> Result<TagData> {
424 let text = std::str::from_utf8(data)
425 .map_err(|_| Error::CorruptObject("tag is not valid UTF-8".to_owned()))?;
426
427 let mut object = None;
428 let mut object_type = None;
429 let mut tag_name = None;
430 let mut tagger = None;
431 let mut message = String::new();
432 let mut in_message = false;
433
434 for line in text.split('\n') {
435 if in_message {
436 message.push_str(line);
437 message.push('\n');
438 continue;
439 }
440 if line.is_empty() {
441 in_message = true;
442 continue;
443 }
444 if let Some(rest) = line.strip_prefix("object ") {
445 object = Some(rest.trim().parse::<ObjectId>()?);
446 } else if let Some(rest) = line.strip_prefix("type ") {
447 object_type = Some(rest.trim().to_owned());
448 } else if let Some(rest) = line.strip_prefix("tag ") {
449 tag_name = Some(rest.trim().to_owned());
450 } else if let Some(rest) = line.strip_prefix("tagger ") {
451 tagger = Some(rest.to_owned());
452 }
453 }
454
455 if message.ends_with('\n') {
457 message.pop();
458 }
459
460 Ok(TagData {
461 object: object
462 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
463 object_type: object_type
464 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
465 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
466 tagger,
467 message,
468 })
469}
470
471#[must_use]
476pub fn serialize_tag(t: &TagData) -> Vec<u8> {
477 let mut out = String::new();
478 out.push_str(&format!("object {}\n", t.object));
479 out.push_str(&format!("type {}\n", t.object_type));
480 out.push_str(&format!("tag {}\n", t.tag));
481 if let Some(ref tagger) = t.tagger {
482 out.push_str(&format!("tagger {tagger}\n"));
483 }
484 out.push('\n');
485 let msg = t.message.trim_end_matches('\n');
487 if !msg.is_empty() {
488 out.push_str(msg);
489 out.push('\n');
490 }
491 out.into_bytes()
492}
493
494#[must_use]
499pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
500 let mut out = Vec::new();
501 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
502 for p in &c.parents {
503 out.extend_from_slice(format!("parent {p}\n").as_bytes());
504 }
505 out.extend_from_slice(format!("author {}\n", c.author).as_bytes());
506 out.extend_from_slice(format!("committer {}\n", c.committer).as_bytes());
507 if let Some(enc) = &c.encoding {
508 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
509 }
510 out.push(b'\n');
511 if let Some(raw) = &c.raw_message {
516 out.extend_from_slice(raw);
517 } else {
518 out.extend_from_slice(c.message.as_bytes());
519 }
520 out
521}