1use std::fmt;
22use std::str::FromStr;
23
24use crate::commit_encoding;
25use crate::error::{Error, Result};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub enum HashAlgo {
34 #[default]
36 Sha1,
37 Sha256,
39}
40
41impl HashAlgo {
42 #[must_use]
44 pub const fn len(self) -> usize {
45 match self {
46 Self::Sha1 => 20,
47 Self::Sha256 => 32,
48 }
49 }
50
51 #[must_use]
53 pub const fn hex_len(self) -> usize {
54 self.len() * 2
55 }
56
57 #[must_use]
59 pub const fn name(self) -> &'static str {
60 match self {
61 Self::Sha1 => "sha1",
62 Self::Sha256 => "sha256",
63 }
64 }
65
66 #[must_use]
69 pub const fn oid_version(self) -> u8 {
70 match self {
71 Self::Sha1 => 1,
72 Self::Sha256 => 2,
73 }
74 }
75
76 #[must_use]
78 pub fn from_name(name: &str) -> Option<Self> {
79 match name.trim() {
80 "sha1" => Some(Self::Sha1),
81 "sha256" => Some(Self::Sha256),
82 _ => None,
83 }
84 }
85
86 #[must_use]
88 pub const fn from_len(len: usize) -> Option<Self> {
89 match len {
90 20 => Some(Self::Sha1),
91 32 => Some(Self::Sha256),
92 _ => None,
93 }
94 }
95}
96
97const MAX_OID_LEN: usize = 32;
99
100#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
106pub struct ObjectId {
107 bytes: [u8; MAX_OID_LEN],
108 len: u8,
109}
110
111impl ObjectId {
112 #[must_use]
118 pub const fn zero() -> Self {
119 Self {
120 bytes: [0u8; MAX_OID_LEN],
121 len: 20,
122 }
123 }
124
125 #[must_use]
127 pub const fn null(algo: HashAlgo) -> Self {
128 Self {
129 bytes: [0u8; MAX_OID_LEN],
130 len: algo.len() as u8,
131 }
132 }
133
134 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
141 if HashAlgo::from_len(bytes.len()).is_none() {
142 return Err(Error::InvalidObjectId(hex::encode(bytes)));
143 }
144 let mut buf = [0u8; MAX_OID_LEN];
145 buf[..bytes.len()].copy_from_slice(bytes);
146 Ok(Self {
147 bytes: buf,
148 len: bytes.len() as u8,
149 })
150 }
151
152 #[must_use]
154 pub fn as_bytes(&self) -> &[u8] {
155 &self.bytes[..self.len as usize]
156 }
157
158 #[must_use]
160 pub fn algo(&self) -> HashAlgo {
161 HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
162 }
163
164 #[must_use]
166 pub fn is_zero(&self) -> bool {
167 self.as_bytes().iter().all(|&b| b == 0)
168 }
169
170 #[must_use]
172 pub fn to_hex(&self) -> String {
173 hex::encode(self.as_bytes())
174 }
175
176 #[must_use]
180 pub fn loose_prefix(&self) -> String {
181 hex::encode(&self.bytes[..1])
182 }
183
184 pub fn from_hex(s: &str) -> Result<Self> {
191 s.parse()
192 }
193
194 #[must_use]
197 pub fn loose_suffix(&self) -> String {
198 hex::encode(&self.bytes[1..self.len as usize])
199 }
200
201 #[must_use]
204 pub fn is_full_hex(s: &str) -> bool {
205 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
206 && s.bytes().all(|b| b.is_ascii_hexdigit())
207 }
208
209 #[must_use]
211 pub const fn is_hex_len(len: usize) -> bool {
212 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
213 }
214
215 #[must_use]
218 pub const fn is_loose_suffix_len(len: usize) -> bool {
219 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
220 }
221}
222
223impl fmt::Display for ObjectId {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(&self.to_hex())
226 }
227}
228
229impl fmt::Debug for ObjectId {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(f, "ObjectId({})", self.to_hex())
232 }
233}
234
235impl FromStr for ObjectId {
236 type Err = Error;
237
238 fn from_str(s: &str) -> Result<Self> {
239 if s.len() != HashAlgo::Sha1.hex_len() && s.len() != HashAlgo::Sha256.hex_len() {
240 return Err(Error::InvalidObjectId(s.to_owned()));
241 }
242 let bytes = hex::decode(s).map_err(|_| Error::InvalidObjectId(s.to_owned()))?;
243 Self::from_bytes(&bytes)
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum ObjectKind {
250 Blob,
252 Tree,
254 Commit,
256 Tag,
258}
259
260impl ObjectKind {
261 pub fn from_bytes(b: &[u8]) -> Result<Self> {
267 match b {
268 b"blob" => Ok(Self::Blob),
269 b"tree" => Ok(Self::Tree),
270 b"commit" => Ok(Self::Commit),
271 b"tag" => Ok(Self::Tag),
272 other => Err(Error::UnknownObjectType(
273 String::from_utf8_lossy(other).into_owned(),
274 )),
275 }
276 }
277
278 #[must_use]
283 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
284 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
285 if field.is_empty() {
286 return false;
287 }
288 for (i, &bc) in field.iter().enumerate() {
289 let sc = canonical.get(i).copied().unwrap_or(0);
290 if sc != bc {
291 return false;
292 }
293 }
294 canonical.get(field.len()).copied().unwrap_or(0) == 0
295 }
296
297 const NAMES: &[(ObjectKind, &[u8])] = &[
298 (ObjectKind::Blob, b"blob"),
299 (ObjectKind::Tree, b"tree"),
300 (ObjectKind::Commit, b"commit"),
301 (ObjectKind::Tag, b"tag"),
302 ];
303 for &(kind, name) in NAMES {
304 if keyword_matches(name, line) {
305 return Some(kind);
306 }
307 }
308 None
309 }
310
311 #[must_use]
313 pub fn as_str(&self) -> &'static str {
314 match self {
315 Self::Blob => "blob",
316 Self::Tree => "tree",
317 Self::Commit => "commit",
318 Self::Tag => "tag",
319 }
320 }
321}
322
323impl fmt::Display for ObjectKind {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 f.write_str(self.as_str())
326 }
327}
328
329impl FromStr for ObjectKind {
330 type Err = Error;
331
332 fn from_str(s: &str) -> Result<Self> {
333 Self::from_bytes(s.as_bytes())
334 }
335}
336
337#[derive(Debug, Clone)]
339pub struct Object {
340 pub kind: ObjectKind,
342 pub data: Vec<u8>,
344}
345
346impl Object {
347 #[must_use]
349 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
350 Self { kind, data }
351 }
352
353 #[must_use]
355 pub fn to_store_bytes(&self) -> Vec<u8> {
356 let header = format!("{} {}\0", self.kind, self.data.len());
357 let mut out = Vec::with_capacity(header.len() + self.data.len());
358 out.extend_from_slice(header.as_bytes());
359 out.extend_from_slice(&self.data);
360 out
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct TreeEntry {
367 pub mode: u32,
369 pub name: Vec<u8>,
371 pub oid: ObjectId,
373}
374
375impl TreeEntry {
376 #[must_use]
380 pub fn mode_str(&self) -> String {
381 if self.mode == 0o040000 {
383 "40000".to_owned()
384 } else {
385 format!("{:o}", self.mode)
386 }
387 }
388}
389
390pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
401 match parse_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
407 Ok(entries) => Ok(entries),
408 Err(sha1_err) => {
409 parse_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
410 }
411 }
412}
413
414pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
419 let mut entries = Vec::new();
420 let mut pos = 0;
421
422 while pos < data.len() {
423 let sp = data[pos..]
425 .iter()
426 .position(|&b| b == b' ')
427 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
428 let mode_bytes = &data[pos..pos + sp];
429 let mode = std::str::from_utf8(mode_bytes)
430 .ok()
431 .and_then(|s| u32::from_str_radix(s, 8).ok())
432 .ok_or_else(|| {
433 Error::CorruptObject(format!(
434 "invalid tree mode: {}",
435 String::from_utf8_lossy(mode_bytes)
436 ))
437 })?;
438 pos += sp + 1;
439
440 let nul = data[pos..]
442 .iter()
443 .position(|&b| b == 0)
444 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
445 let name = data[pos..pos + nul].to_vec();
446 pos += nul + 1;
447
448 if pos + oid_len > data.len() {
449 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
450 }
451 let oid = ObjectId::from_bytes(&data[pos..pos + oid_len])?;
452 pos += oid_len;
453
454 entries.push(TreeEntry { mode, name, oid });
455 }
456
457 Ok(entries)
458}
459
460#[must_use]
465pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
466 let mut out = Vec::new();
467 for e in entries {
468 out.extend_from_slice(e.mode_str().as_bytes());
469 out.push(b' ');
470 out.extend_from_slice(&e.name);
471 out.push(0);
472 out.extend_from_slice(e.oid.as_bytes());
473 }
474 out
475}
476
477#[must_use]
490pub fn tree_entry_cmp(
491 a_name: &[u8],
492 a_is_tree: bool,
493 b_name: &[u8],
494 b_is_tree: bool,
495) -> std::cmp::Ordering {
496 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
497 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
498
499 let min_len = a_name.len().min(b_name.len());
500 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
501 if cmp != std::cmp::Ordering::Equal {
502 return cmp;
503 }
504 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
506 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
507 ac.cmp(&bc)
508}
509
510#[derive(Debug, Clone)]
512pub struct CommitData {
513 pub tree: ObjectId,
515 pub parents: Vec<ObjectId>,
517 pub author: String,
519 pub committer: String,
521 pub author_raw: Vec<u8>,
525 pub committer_raw: Vec<u8>,
527 pub encoding: Option<String>,
529 pub message: String,
531 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
534 pub raw_message: Option<Vec<u8>>,
535}
536
537pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
543 #[derive(Clone, Copy)]
547 enum Continuation {
548 Author,
549 Committer,
550 Multiline,
551 Ignore,
552 }
553
554 let mut pos = 0usize;
555 let mut tree = None;
556 let mut parents = Vec::new();
557 let mut author_raw: Option<Vec<u8>> = None;
558 let mut committer_raw: Option<Vec<u8>> = None;
559 let mut encoding: Option<String> = None;
560 let mut cont = Continuation::Ignore;
561
562 while pos < data.len() {
563 let line_start = pos;
564 let mut line_end = pos;
565 while line_end < data.len() && data[line_end] != b'\n' {
566 line_end += 1;
567 }
568 let line = &data[line_start..line_end];
569 let after_nl = line_end.saturating_add(1);
570 if line.is_empty() {
571 let body = data.get(after_nl..).unwrap_or_default();
572 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
573 let has_non_utf8_encoding = encoding.as_deref().is_some_and(|label| {
577 !label.eq_ignore_ascii_case("utf-8") && !label.eq_ignore_ascii_case("utf8")
578 });
579 let raw_message = if body.is_empty() {
580 None
581 } else if has_non_utf8_encoding
582 || std::str::from_utf8(body).is_err()
583 || !body.ends_with(b"\n")
584 {
585 Some(body.to_vec())
586 } else {
587 None
588 };
589 let author_bytes = author_raw
590 .ok_or_else(|| Error::CorruptObject("commit missing author header".to_owned()))?;
591 let committer_bytes = committer_raw.ok_or_else(|| {
592 Error::CorruptObject("commit missing committer header".to_owned())
593 })?;
594 let author = commit_encoding::decode_bytes(encoding.as_deref(), &author_bytes);
595 let committer = commit_encoding::decode_bytes(encoding.as_deref(), &committer_bytes);
596 return Ok(CommitData {
597 tree: tree
598 .ok_or_else(|| Error::CorruptObject("commit missing tree header".to_owned()))?,
599 parents,
600 author,
601 committer,
602 author_raw: author_bytes,
603 committer_raw: committer_bytes,
604 encoding,
605 message,
606 raw_message,
607 });
608 }
609
610 if line.first() == Some(&b' ') {
611 let rest = line.get(1..).unwrap_or_default();
612 match cont {
613 Continuation::Author => {
614 let a = author_raw.as_mut().ok_or_else(|| {
615 Error::CorruptObject("orphan header continuation".to_owned())
616 })?;
617 a.extend_from_slice(rest);
618 }
619 Continuation::Committer => {
620 let c = committer_raw.as_mut().ok_or_else(|| {
621 Error::CorruptObject("orphan header continuation".to_owned())
622 })?;
623 c.extend_from_slice(rest);
624 }
625 Continuation::Multiline | Continuation::Ignore => {}
626 }
627 pos = after_nl;
628 continue;
629 }
630
631 let key_end = line
632 .iter()
633 .position(|&b| b == b' ')
634 .ok_or_else(|| Error::CorruptObject("malformed commit header line".to_owned()))?;
635 let key = &line[..key_end];
636 let rest = line.get(key_end + 1..).unwrap_or_default();
637
638 match key {
639 b"tree" => {
640 let line_str = std::str::from_utf8(rest).map_err(|_| {
641 Error::CorruptObject("commit tree line is not valid UTF-8".to_owned())
642 })?;
643 tree = Some(line_str.trim().parse::<ObjectId>()?);
644 cont = Continuation::Ignore;
645 }
646 b"parent" => {
647 let line_str = std::str::from_utf8(rest).map_err(|_| {
648 Error::CorruptObject("commit parent line is not valid UTF-8".to_owned())
649 })?;
650 parents.push(line_str.trim().parse::<ObjectId>()?);
651 cont = Continuation::Ignore;
652 }
653 b"author" => {
654 author_raw = Some(rest.to_vec());
655 cont = Continuation::Author;
656 }
657 b"committer" => {
658 committer_raw = Some(rest.to_vec());
659 cont = Continuation::Committer;
660 }
661 b"encoding" => {
662 let line_str = std::str::from_utf8(rest).map_err(|_| {
663 Error::CorruptObject("commit encoding line is not valid UTF-8".to_owned())
664 })?;
665 encoding = Some(line_str.to_owned());
666 cont = Continuation::Ignore;
667 }
668 _ => {
669 cont = Continuation::Multiline;
670 }
671 }
672 pos = after_nl;
673 }
674
675 Err(Error::CorruptObject(
676 "commit missing blank line before message".to_owned(),
677 ))
678}
679
680#[must_use]
683pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
684 let mut pos = 0usize;
685 while pos < data.len() {
686 let rest = &data[pos..];
687 let nl = rest.iter().position(|&b| b == b'\n');
688 let line = if let Some(i) = nl { &rest[..i] } else { rest };
689 if line.is_empty() {
690 break;
691 }
692 if let Some(after) = line.strip_prefix(prefix) {
693 return Some(String::from_utf8_lossy(after).trim().to_owned());
694 }
695 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
696 if nl.is_none() {
697 break;
698 }
699 }
700 None
701}
702
703#[must_use]
705pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
706 let s = tag_header_field(data, b"object ")?;
707 s.parse().ok()
708}
709
710#[derive(Debug, Clone)]
712pub struct TagData {
713 pub object: ObjectId,
715 pub object_type: String,
717 pub tag: String,
719 pub tagger: Option<String>,
721 pub message: String,
723}
724
725pub fn parse_tag(data: &[u8]) -> Result<TagData> {
731 let text = std::str::from_utf8(data)
732 .map_err(|_| Error::CorruptObject("tag is not valid UTF-8".to_owned()))?;
733
734 let mut object = None;
735 let mut object_type = None;
736 let mut tag_name = None;
737 let mut tagger = None;
738 let mut message = String::new();
739 let mut in_message = false;
740
741 for line in text.split('\n') {
742 if in_message {
743 message.push_str(line);
744 message.push('\n');
745 continue;
746 }
747 if line.is_empty() {
748 in_message = true;
749 continue;
750 }
751 if let Some(rest) = line.strip_prefix("object ") {
752 object = Some(rest.trim().parse::<ObjectId>()?);
753 } else if let Some(rest) = line.strip_prefix("type ") {
754 let typ = rest.trim();
755 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
756 return Err(Error::CorruptObject(format!(
757 "invalid 'type' value in tag: {typ}"
758 )));
759 }
760 object_type = Some(typ.to_owned());
761 } else if let Some(rest) = line.strip_prefix("tag ") {
762 tag_name = Some(rest.trim().to_owned());
763 } else if let Some(rest) = line.strip_prefix("tagger ") {
764 tagger = Some(rest.to_owned());
765 }
766 }
767
768 if message.ends_with('\n') {
770 message.pop();
771 }
772
773 Ok(TagData {
774 object: object
775 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
776 object_type: object_type
777 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
778 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
779 tagger,
780 message,
781 })
782}
783
784#[must_use]
789pub fn serialize_tag(t: &TagData) -> Vec<u8> {
790 let mut out = String::new();
791 out.push_str(&format!("object {}\n", t.object));
792 out.push_str(&format!("type {}\n", t.object_type));
793 out.push_str(&format!("tag {}\n", t.tag));
794 if let Some(ref tagger) = t.tagger {
795 out.push_str(&format!("tagger {tagger}\n"));
796 }
797 out.push('\n');
798 let msg = t.message.trim_end_matches('\n');
800 if !msg.is_empty() {
801 out.push_str(msg);
802 out.push('\n');
803 }
804 out.into_bytes()
805}
806
807#[must_use]
815pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
816 let mut out = Vec::new();
817 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
818 for p in &c.parents {
819 out.extend_from_slice(format!("parent {p}\n").as_bytes());
820 }
821 out.extend_from_slice(b"author ");
822 if c.author_raw.is_empty() {
823 out.extend_from_slice(c.author.as_bytes());
824 } else {
825 out.extend_from_slice(&c.author_raw);
826 }
827 out.push(b'\n');
828 out.extend_from_slice(b"committer ");
829 if c.committer_raw.is_empty() {
830 out.extend_from_slice(c.committer.as_bytes());
831 } else {
832 out.extend_from_slice(&c.committer_raw);
833 }
834 out.push(b'\n');
835 if let Some(enc) = &c.encoding {
836 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
837 }
838 out.push(b'\n');
839 if let Some(raw) = &c.raw_message {
840 out.extend_from_slice(raw);
841 } else if !c.message.is_empty() {
842 out.extend_from_slice(c.message.as_bytes());
843 }
844 out
845}
846
847#[cfg(test)]
848mod commit_parse_tests {
849 use super::*;
850
851 #[test]
852 fn parse_commit_skips_multiline_gpgsig_continuation() {
853 let raw = concat!(
854 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
855 "author A U Thor <author@example.com> 1 +0000\n",
856 "committer C O Mitter <committer@example.com> 1 +0000\n",
857 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
858 " abcdef\n",
859 " -----END PGP SIGNATURE-----\n",
860 "\n",
861 "msg\n",
862 );
863 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
864 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
865 assert_eq!(c.message, "msg\n");
866 }
867}