use std::fmt;
use std::str::FromStr;
use crate::commit_encoding;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum HashAlgo {
#[default]
Sha1,
Sha256,
}
impl HashAlgo {
#[must_use]
pub const fn len(self) -> usize {
match self {
Self::Sha1 => 20,
Self::Sha256 => 32,
}
}
#[must_use]
pub const fn is_empty(self) -> bool {
false
}
#[must_use]
pub const fn hex_len(self) -> usize {
self.len() * 2
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Sha1 => "sha1",
Self::Sha256 => "sha256",
}
}
#[must_use]
pub const fn oid_version(self) -> u8 {
match self {
Self::Sha1 => 1,
Self::Sha256 => 2,
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name.trim() {
v if v.eq_ignore_ascii_case("sha1") => Some(Self::Sha1),
v if v.eq_ignore_ascii_case("sha256") => Some(Self::Sha256),
_ => None,
}
}
#[must_use]
pub const fn from_len(len: usize) -> Option<Self> {
match len {
20 => Some(Self::Sha1),
32 => Some(Self::Sha256),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("unknown Git hash version byte {0} (expected 1 for SHA-1 or 2 for SHA-256)")]
pub struct HashVersionError(pub u8);
impl From<HashAlgo> for u8 {
fn from(algo: HashAlgo) -> Self {
algo.oid_version()
}
}
impl TryFrom<u8> for HashAlgo {
type Error = HashVersionError;
fn try_from(version: u8) -> std::result::Result<Self, Self::Error> {
match version {
1 => Ok(Self::Sha1),
2 => Ok(Self::Sha256),
other => Err(HashVersionError(other)),
}
}
}
const MAX_OID_LEN: usize = 32;
const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
0xfb, 0xee, 0x49, 0x04,
];
const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
0x7c, 0x69, 0x48, 0xd4,
];
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ObjectId {
bytes: [u8; MAX_OID_LEN],
len: u8,
}
impl ObjectId {
#[must_use]
pub const fn zero() -> Self {
Self {
bytes: [0u8; MAX_OID_LEN],
len: 20,
}
}
#[must_use]
pub const fn null(algo: HashAlgo) -> Self {
Self {
bytes: [0u8; MAX_OID_LEN],
len: algo.len() as u8,
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if HashAlgo::from_len(bytes.len()).is_none() {
return Err(Error::InvalidObjectId(hex::encode(bytes)));
}
let mut buf = [0u8; MAX_OID_LEN];
buf[..bytes.len()].copy_from_slice(bytes);
Ok(Self {
bytes: buf,
len: bytes.len() as u8,
})
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
#[must_use]
pub fn algo(&self) -> HashAlgo {
HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.as_bytes().iter().all(|&b| b == 0)
}
#[must_use]
pub(crate) fn is_well_known_empty_tree(&self) -> bool {
self.as_bytes().len() == HashAlgo::Sha1.len()
&& (self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
|| self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
}
#[must_use]
pub(crate) fn is_canonical_empty_tree(&self) -> bool {
self.as_bytes().len() == HashAlgo::Sha1.len()
&& self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
}
pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
const HEX: &[u8; 16] = b"0123456789abcdef";
let bytes = self.as_bytes();
let prefix = [
HEX[(bytes[0] >> 4) as usize],
HEX[(bytes[0] & 0xf) as usize],
];
path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
let suffix_len = (bytes.len() - 1) * 2;
let mut suffix = [0u8; 62];
for (i, &b) in bytes[1..].iter().enumerate() {
suffix[i * 2] = HEX[(b >> 4) as usize];
suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
}
path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
}
#[must_use]
pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
let mut path = objects_dir.to_path_buf();
self.push_loose_relative(&mut path);
path
}
#[must_use]
pub(crate) fn loose_tmp_path_in_prefix(
&self,
prefix_dir: &std::path::Path,
) -> std::path::PathBuf {
const HEX: &[u8; 16] = b"0123456789abcdef";
let bytes = self.as_bytes();
let suffix_len = (bytes.len() - 1) * 2;
let mut name = [0u8; 4 + 62];
name[..4].copy_from_slice(b"tmp_");
for (i, &b) in bytes[1..].iter().enumerate() {
name[4 + i * 2] = HEX[(b >> 4) as usize];
name[4 + i * 2 + 1] = HEX[(b & 0xf) as usize];
}
prefix_dir.join(unsafe { std::str::from_utf8_unchecked(&name[..4 + suffix_len]) })
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.as_bytes())
}
#[must_use]
pub fn loose_prefix(&self) -> String {
hex::encode(&self.bytes[..1])
}
pub fn from_hex(s: &str) -> Result<Self> {
Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
}
pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
if bytes.len() != HashAlgo::Sha1.hex_len() {
return Err(Error::InvalidObjectId(
String::from_utf8_lossy(bytes).into_owned(),
));
}
Self::decode_hex(bytes)
.ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
}
fn decode_hex(hex: &[u8]) -> Option<Self> {
let len = hex.len() / 2;
if !Self::is_hex_len(hex.len()) {
return None;
}
let mut bytes = [0u8; MAX_OID_LEN];
let mut invalid = 0u8;
for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
let hi = HEX_DECODE[usize::from(pair[0])];
let lo = HEX_DECODE[usize::from(pair[1])];
invalid |= hi | lo;
*out = (hi << 4) | lo;
}
(invalid & HEX_INVALID == 0).then_some(Self {
bytes,
len: len as u8,
})
}
#[must_use]
pub fn loose_suffix(&self) -> String {
hex::encode(&self.bytes[1..self.len as usize])
}
#[must_use]
pub fn is_full_hex(s: &str) -> bool {
(s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
&& s.bytes().all(|b| b.is_ascii_hexdigit())
}
#[must_use]
pub const fn is_hex_len(len: usize) -> bool {
len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
}
#[must_use]
pub const fn is_loose_suffix_len(len: usize) -> bool {
len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
}
}
impl std::hash::Hash for ObjectId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(self.as_bytes());
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ObjectId({})", self.to_hex())
}
}
const HEX_INVALID: u8 = 0x10;
const HEX_DECODE: [u8; 256] = {
let mut table = [HEX_INVALID; 256];
let mut i = 0u8;
while i < 10 {
table[(b'0' + i) as usize] = i;
i += 1;
}
let mut i = 0u8;
while i < 6 {
table[(b'a' + i) as usize] = 10 + i;
table[(b'A' + i) as usize] = 10 + i;
i += 1;
}
table
};
impl FromStr for ObjectId {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::from_hex(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObjectKind {
Blob,
Tree,
Commit,
Tag,
}
impl ObjectKind {
pub fn from_bytes(b: &[u8]) -> Result<Self> {
match b {
b"blob" => Ok(Self::Blob),
b"tree" => Ok(Self::Tree),
b"commit" => Ok(Self::Commit),
b"tag" => Ok(Self::Tag),
other => Err(Error::UnknownObjectType(
String::from_utf8_lossy(other).into_owned(),
)),
}
}
#[must_use]
pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
if field.is_empty() {
return false;
}
for (i, &bc) in field.iter().enumerate() {
let sc = canonical.get(i).copied().unwrap_or(0);
if sc != bc {
return false;
}
}
canonical.get(field.len()).copied().unwrap_or(0) == 0
}
const NAMES: &[(ObjectKind, &[u8])] = &[
(ObjectKind::Blob, b"blob"),
(ObjectKind::Tree, b"tree"),
(ObjectKind::Commit, b"commit"),
(ObjectKind::Tag, b"tag"),
];
for &(kind, name) in NAMES {
if keyword_matches(name, line) {
return Some(kind);
}
}
None
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Blob => "blob",
Self::Tree => "tree",
Self::Commit => "commit",
Self::Tag => "tag",
}
}
}
impl fmt::Display for ObjectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ObjectKind {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::from_bytes(s.as_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectInfo {
pub kind: ObjectKind,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct Object {
pub kind: ObjectKind,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectRef<'a> {
pub kind: ObjectKind,
pub data: &'a [u8],
}
impl<'a> ObjectRef<'a> {
#[must_use]
pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
Self { kind, data: buf }
}
}
impl Object {
#[must_use]
pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
Self { kind, data }
}
#[must_use]
pub fn as_ref(&self) -> ObjectRef<'_> {
ObjectRef {
kind: self.kind,
data: self.data.as_slice(),
}
}
#[must_use]
pub fn to_store_bytes(&self) -> Vec<u8> {
let header = format!("{} {}\0", self.kind, self.data.len());
let mut out = Vec::with_capacity(header.len() + self.data.len());
out.extend_from_slice(header.as_bytes());
out.extend_from_slice(&self.data);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeEntry {
pub mode: u32,
pub name: Vec<u8>,
pub oid: ObjectId,
}
impl TreeEntry {
#[must_use]
pub fn mode_str(&self) -> String {
if self.mode == 0o040000 {
"40000".to_owned()
} else {
format!("{:o}", self.mode)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TreeEntryRef<'a> {
pub mode: u32,
pub name: &'a [u8],
pub oid: ObjectId,
}
impl TreeEntryRef<'_> {
#[must_use]
pub fn to_owned(&self) -> TreeEntry {
TreeEntry {
mode: self.mode,
name: self.name.to_vec(),
oid: self.oid,
}
}
}
#[derive(Debug, Clone)]
pub struct TreeRefIter<'a> {
data: &'a [u8],
pos: usize,
oid_len: usize,
failed: bool,
}
impl<'a> TreeRefIter<'a> {
#[must_use]
pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
Self {
data,
pos: 0,
oid_len,
failed: false,
}
}
pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
Err(_) => Err(sha1_err),
},
}
}
fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
}
}
fn parse_tree_entry_at<'a>(
data: &'a [u8],
pos: &mut usize,
oid_len: usize,
) -> Result<Option<TreeEntryRef<'a>>> {
if *pos >= data.len() {
return Ok(None);
}
let sp = data[*pos..]
.iter()
.position(|&b| b == b' ')
.ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
let mode_bytes = &data[*pos..*pos + sp];
let mode = std::str::from_utf8(mode_bytes)
.ok()
.and_then(|s| u32::from_str_radix(s, 8).ok())
.ok_or_else(|| {
Error::CorruptObject(format!(
"invalid tree mode: {}",
String::from_utf8_lossy(mode_bytes)
))
})?;
*pos += sp + 1;
let nul = data[*pos..]
.iter()
.position(|&b| b == 0)
.ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
let name = &data[*pos..*pos + nul];
*pos += nul + 1;
if *pos + oid_len > data.len() {
return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
}
let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
*pos += oid_len;
Ok(Some(TreeEntryRef { mode, name, oid }))
}
impl<'a> Iterator for TreeRefIter<'a> {
type Item = Result<TreeEntryRef<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.failed || self.pos >= self.data.len() {
return None;
}
match self.next_entry() {
Ok(Some(entry)) => Some(Ok(entry)),
Ok(None) => None,
Err(err) => {
self.failed = true;
self.pos = self.data.len();
Some(Err(err))
}
}
}
}
fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
let mut pos = 0usize;
while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
Ok(())
}
fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
let mut pos = 0usize;
let mut entries = Vec::with_capacity(data.len() / 32);
while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
entries.push(TreeEntry {
mode: entry.mode,
name: entry.name.to_vec(),
oid: entry.oid,
});
}
Ok(entries)
}
pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
Ok(entries) => Ok(entries),
Err(sha1_err) => {
collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
}
}
}
pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
collect_tree_with_oid_len(data, oid_len)
}
#[must_use]
pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
let mut out = Vec::new();
for e in entries {
out.extend_from_slice(e.mode_str().as_bytes());
out.push(b' ');
out.extend_from_slice(&e.name);
out.push(0);
out.extend_from_slice(e.oid.as_bytes());
}
out
}
#[must_use]
pub fn tree_entry_cmp(
a_name: &[u8],
a_is_tree: bool,
b_name: &[u8],
b_is_tree: bool,
) -> std::cmp::Ordering {
let a_trailer = if a_is_tree { b'/' } else { 0u8 };
let b_trailer = if b_is_tree { b'/' } else { 0u8 };
let min_len = a_name.len().min(b_name.len());
let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
if cmp != std::cmp::Ordering::Equal {
return cmp;
}
let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
ac.cmp(&bc)
}
fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
if bytes.is_empty() || bytes.len() > 7 {
return None;
}
bytes.iter().try_fold(0u32, |acc, &b| match b {
b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
_ => None,
})
}
pub(crate) fn find_tree_entry_by_name_in_data(
data: &[u8],
oid_len: usize,
name: &[u8],
name_is_tree: bool,
) -> Result<Option<TreeEntry>> {
let mut pos = 0usize;
while pos < data.len() {
let rest = &data[pos..];
let sp = memchr::memchr(b' ', rest)
.ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
let mode = parse_tree_mode_octal(&rest[..sp])
.ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
let name_start = sp + 1;
let nul = memchr::memchr(0, &rest[name_start..])
.ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
let entry_name = &rest[name_start..name_start + nul];
let oid_start = name_start + nul + 1;
let oid_end = oid_start + oid_len;
if oid_end > rest.len() {
return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
}
if entry_name == name {
return Ok(Some(TreeEntry {
mode,
name: entry_name.to_vec(),
oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
}));
}
if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
== std::cmp::Ordering::Greater
{
return Ok(None);
}
pos += oid_end;
}
Ok(None)
}
pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
data: &[u8],
oid_len: usize,
name: &[u8],
) -> Result<Option<TreeEntry>> {
if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
if entry.mode == 0o040000 {
return Ok(Some(entry));
}
}
find_tree_entry_by_name_in_data(data, oid_len, name, false)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitData {
pub tree: ObjectId,
pub parents: Vec<ObjectId>,
pub author: String,
pub committer: String,
pub author_raw: Vec<u8>,
pub committer_raw: Vec<u8>,
pub encoding: Option<String>,
pub message: String,
#[doc = "Optional raw message bytes for non-UTF-8 messages."]
pub raw_message: Option<Vec<u8>>,
pub extra_headers: Vec<u8>,
}
#[must_use]
pub fn commit_subject_pretty(message: &str) -> String {
let mut parts = Vec::new();
for line in message.lines() {
if line.trim().is_empty() {
break;
}
parts.push(line.trim_end());
}
parts.join(" ")
}
pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
let message_start = data
.windows(2)
.position(|w| w == b"\n\n")
.map(|p| p + 2)
.ok_or_else(|| {
Error::CorruptObject("commit missing blank line before message".to_owned())
})?;
let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
Ok(commit_subject_pretty(body))
}
pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
let mut pos = 0usize;
let mut encoding: Option<String> = None;
while pos < data.len() {
let line_start = pos;
let mut line_end = pos;
while line_end < data.len() && data[line_end] != b'\n' {
line_end += 1;
}
let line = &data[line_start..line_end];
let after_nl = line_end.saturating_add(1);
if line.is_empty() {
let body = data.get(after_nl..).unwrap_or_default();
let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
return Ok(message
.lines()
.find(|l| !l.trim().is_empty())
.map(str::trim)
.unwrap_or("(no subject)")
.to_owned());
}
if line.first() == Some(&b' ') {
pos = after_nl;
continue;
}
if let Some(rest) = line.strip_prefix(b"encoding ") {
encoding = Some(
std::str::from_utf8(rest)
.map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
.trim()
.to_owned(),
);
}
pos = after_nl;
}
Err(Error::CorruptObject(
"commit missing blank line before message".to_owned(),
))
}
pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
let s = std::str::from_utf8(value)
.map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
s.trim()
.parse::<ObjectId>()
.map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
}
pub(crate) fn parse_commit_tree_and_parents_prefix(
prefix: &[u8],
hex_len: usize,
) -> Option<(ObjectId, Vec<ObjectId>)> {
canonical_commit_tree_and_parents(prefix, hex_len, false)
}
pub(crate) fn parse_commit_tree_and_parents_lenient(
data: &[u8],
hex_len: usize,
) -> Option<(ObjectId, Vec<ObjectId>)> {
canonical_commit_tree_and_parents(data, hex_len, true)
}
fn canonical_commit_tree_and_parents(
data: &[u8],
hex_len: usize,
complete: bool,
) -> Option<(ObjectId, Vec<ObjectId>)> {
fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
let line = rest.strip_prefix(key)?;
if line.len() <= hex_len || line[hex_len] != b'\n' {
return None;
}
let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
Some((oid, &line[hex_len + 1..]))
}
const PARENT: &[u8] = b"parent ";
let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
let mut parents = Vec::new();
loop {
if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
return None;
}
if !rest.starts_with(PARENT) {
return Some((tree, parents));
}
let (parent, next) = header_oid(rest, PARENT, hex_len)?;
parents.push(parent);
rest = next;
}
}
pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
Ok(super::CommitRef::parse(data)?.to_owned())
}
#[must_use]
pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
let mut pos = 0usize;
while pos < data.len() {
let rest = &data[pos..];
let nl = rest.iter().position(|&b| b == b'\n');
let line = if let Some(i) = nl { &rest[..i] } else { rest };
if line.is_empty() {
break;
}
if let Some(after) = line.strip_prefix(prefix) {
return Some(String::from_utf8_lossy(after).trim().to_owned());
}
pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
if nl.is_none() {
break;
}
}
None
}
#[must_use]
pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
let s = tag_header_field(data, b"object ")?;
s.parse().ok()
}
#[derive(Debug, Clone)]
pub struct TagData {
pub object: ObjectId,
pub object_type: String,
pub tag: String,
pub tagger: Option<String>,
pub extra_headers: Vec<u8>,
pub message: String,
}
pub fn parse_tag(data: &[u8]) -> Result<TagData> {
use super::extra_headers::next_header_block;
#[derive(Clone, Copy)]
enum Continuation {
None,
Extra,
}
let mut pos = 0usize;
let mut object = None;
let mut object_type = None;
let mut tag_name = None;
let mut tagger = None;
let mut extra_headers = Vec::new();
let mut cont = Continuation::None;
let mut past_tagger = false;
while pos < data.len() {
let line_start = pos;
let mut line_end = pos;
while line_end < data.len() && data[line_end] != b'\n' {
line_end += 1;
}
let line = &data[line_start..line_end];
let after_nl = (line_end + 1).min(data.len());
if line.is_empty() {
let body = data.get(after_nl..).unwrap_or_default();
let message = std::str::from_utf8(body)
.map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
.to_owned();
return Ok(TagData {
object: object
.ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
object_type: object_type
.ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
tag: tag_name
.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
tagger,
extra_headers,
message,
});
}
if past_tagger {
let (_, block_end, _) = next_header_block(data, line_start);
extra_headers.extend_from_slice(&data[line_start..block_end]);
pos = block_end;
continue;
}
if line.first() == Some(&b' ') {
match cont {
Continuation::Extra => {
extra_headers.extend_from_slice(&data[line_start..after_nl]);
}
Continuation::None => {}
}
pos = after_nl;
continue;
}
let key_end = line
.iter()
.position(|&b| b == b' ')
.ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
let key = &line[..key_end];
let rest = line.get(key_end + 1..).unwrap_or_default();
match key {
b"object" => {
let line_str = std::str::from_utf8(rest).map_err(|_| {
Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
})?;
object = Some(line_str.trim().parse::<ObjectId>()?);
cont = Continuation::None;
}
b"type" => {
let typ = std::str::from_utf8(rest).map_err(|_| {
Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
})?;
let typ = typ.trim();
if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
return Err(Error::CorruptObject(format!(
"invalid 'type' value in tag: {typ}"
)));
}
object_type = Some(typ.to_owned());
cont = Continuation::None;
}
b"tag" => {
let line_str = std::str::from_utf8(rest).map_err(|_| {
Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
})?;
tag_name = Some(line_str.trim().to_owned());
cont = Continuation::None;
}
b"tagger" => {
tagger = Some(
std::str::from_utf8(rest)
.map_err(|_| {
Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
})?
.to_owned(),
);
past_tagger = true;
cont = Continuation::None;
}
_ => {
cont = Continuation::Extra;
extra_headers.extend_from_slice(&data[line_start..after_nl]);
}
}
pos = after_nl;
}
Err(Error::CorruptObject(
"tag missing blank line before message".to_owned(),
))
}
#[must_use]
pub fn serialize_tag(t: &TagData) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
if let Some(ref tagger) = t.tagger {
out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
}
out.extend_from_slice(&t.extra_headers);
out.push(b'\n');
let msg = t.message.trim_end_matches('\n');
if !msg.is_empty() {
out.extend_from_slice(msg.as_bytes());
out.push(b'\n');
}
out
}
#[must_use]
pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
for p in &c.parents {
out.extend_from_slice(format!("parent {p}\n").as_bytes());
}
out.extend_from_slice(b"author ");
if c.author_raw.is_empty() {
out.extend_from_slice(c.author.as_bytes());
} else {
out.extend_from_slice(&c.author_raw);
}
out.push(b'\n');
out.extend_from_slice(b"committer ");
if c.committer_raw.is_empty() {
out.extend_from_slice(c.committer.as_bytes());
} else {
out.extend_from_slice(&c.committer_raw);
}
out.push(b'\n');
if let Some(enc) = &c.encoding {
out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
}
out.extend_from_slice(&c.extra_headers);
out.push(b'\n');
if let Some(raw) = &c.raw_message {
out.extend_from_slice(raw);
} else if !c.message.is_empty() {
out.extend_from_slice(c.message.as_bytes());
}
out
}
#[cfg(test)]
mod commit_parse_tests {
use super::*;
#[test]
fn commit_subject_pretty_folds_first_paragraph() {
assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
assert_eq!(commit_subject_pretty(""), "");
assert_eq!(
commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
" hello world\tthere next\t line"
);
}
#[test]
fn commit_ref_matches_parse_commit_headers() {
let raw = concat!(
"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
"parent 1111111111111111111111111111111111111111\n",
"author A <a@example.com> 1 +0000\n",
"committer C <c@example.com> 2 +0000\n",
"\n",
"ignored body\n",
);
let full = parse_commit(raw.as_bytes()).expect("parse");
let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
assert_eq!(link.tree(), full.tree);
assert_eq!(link.parents(), full.parents.as_slice());
assert_eq!(link.author(), full.author);
assert_eq!(link.committer(), full.committer);
}
#[test]
fn parse_commit_skips_multiline_gpgsig_continuation() {
let raw = concat!(
"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
"author A U Thor <author@example.com> 1 +0000\n",
"committer C O Mitter <committer@example.com> 1 +0000\n",
"gpgsig -----BEGIN PGP SIGNATURE-----\n",
" abcdef\n",
" -----END PGP SIGNATURE-----\n",
"\n",
"msg\n",
);
let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
assert_eq!(c.message, "msg\n");
assert_eq!(serialize_commit(&c), raw.as_bytes());
}
#[test]
fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
let oid = ObjectId::from_hex(sha1).unwrap();
assert_eq!(oid.algo(), HashAlgo::Sha1);
assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
let sha256 = "f".repeat(64);
let oid = sha256.parse::<ObjectId>().unwrap();
assert_eq!(oid.algo(), HashAlgo::Sha256);
assert_eq!(oid.as_bytes(), [0xff; 32]);
assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
for bad in [
"",
&sha1[..39],
&format!("{sha1}0"),
"g123456789abcdef0123456789abcdef01234567",
"0123456789abcdef0123456789abcdef0123456 ",
"0123456789abcdef0123456789abcdef012345/7",
"0123456789abcdef0123456789abcdef012345:7",
"0123456789abcdef0123456789abcdef012345`7",
"0123456789abcdef0123456789abcdef012345G7",
] {
assert!(
matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
"{bad:?}"
);
assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
}
let mut non_ascii = sha1.as_bytes().to_vec();
non_ascii[3] = 0xb0;
assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
}
#[test]
fn serialize_commit_applies_public_field_edits_after_parse() {
let raw = concat!(
"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
"author A <a@example.com> 1 +0000\n",
"committer C <c@example.com> 1 +0000\n",
"\n",
"msg\n",
);
let mut commit = parse_commit(raw.as_bytes()).expect("parse");
let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
commit.tree = zero;
let serialized = serialize_commit(&commit);
let tree_line = format!("tree {}\n", zero.to_hex());
assert!(
serialized.starts_with(tree_line.as_bytes()),
"serialize_commit ignored the updated public tree field"
);
}
#[test]
fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
let raw = concat!(
"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
"author A <a@example.com> 1 +0000\n",
"committer C <c@example.com> 1 +0000\n",
"encoding ISO-8859-1\n",
"gpgsig -----BEGIN PGP SIGNATURE-----\n",
" line2\n",
" -----END PGP SIGNATURE-----\n",
"mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
" type commit\n",
" tag merge-tag\n",
" tagger T <t@example.com> 1 +0000\n",
" -----BEGIN PGP SIGNATURE-----\n",
" inner\n",
" -----END PGP SIGNATURE-----\n",
"\n",
"msg-only\n",
);
let parsed = parse_commit(raw.as_bytes()).expect("parse");
assert_eq!(serialize_commit(&parsed), raw.as_bytes());
}
#[test]
fn tree_entry_mode_str_and_tag_object_line_oid() {
let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
let entry = TreeEntry {
mode: 0o040000,
name: b"dir".to_vec(),
oid,
};
assert_eq!(entry.mode_str(), "40000");
let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
}
#[test]
fn parse_commit_non_commit_bytes_does_not_panic() {
let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
assert!(matches!(err, Error::CorruptObject(_)));
}
#[test]
fn parse_tree_with_sha256_oid_width() {
let oid_bytes = [0x6eu8; 32];
let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
let mut raw = b"100644 file\0".to_vec();
raw.extend_from_slice(oid.as_bytes());
let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].oid, oid);
}
#[test]
fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
let tree = "1111111111111111111111111111111111111111";
let p1 = "2222222222222222222222222222222222222222";
let p2 = "3333333333333333333333333333333333333333";
let raw = format!(
"tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
committer C <c@x> 2 +0000\n\nmessage\n"
);
let commit = parse_commit(raw.as_bytes())?;
let full = (commit.tree, commit.parents);
assert_eq!(full.1.len(), 2);
assert_eq!(
parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
Some(full.clone())
);
let hex_len = HashAlgo::Sha1.hex_len();
let header_end = raw.find("author").expect("author line");
for cut in 0..=raw.len() {
let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
if cut > header_end {
assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
} else {
assert_eq!(
got, None,
"cut at {cut} must not claim a complete parent list"
);
}
}
Ok(())
}
#[test]
fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
let tree = "tree 1111111111111111111111111111111111111111\n";
let parent = "parent 2222222222222222222222222222222222222222\n";
let parent2 = "parent 3333333333333333333333333333333333333333\n";
let bodies = [
format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
format!("{tree}{parent}"),
format!("{tree}{parent}{parent2}"),
format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
format!("{tree}{parent}not a header\n"),
format!("{tree}parent zz\n\n"),
format!("{tree}{parent}parent 2222"),
format!("author A <a@x> 1 +0000\n{tree}\n"),
];
let hex_len = HashAlgo::Sha1.hex_len();
for body in &bodies {
let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
for cut in 0..=body.len() {
if let Some(got) =
parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
{
assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
}
}
}
let parents_of = |body: &str| {
parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
};
assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
assert_eq!(
parents_of(&bodies[3]),
Some(2),
"object may end after parents"
);
assert_eq!(
parents_of(&bodies[4]),
Some(0),
"only leading parents count"
);
assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
}
#[test]
fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
let (tree, parents) =
parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
assert!(parents.is_empty());
let t = "a".repeat(64);
let p = "b".repeat(64);
let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
let (tree, parents) =
parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
.expect("sha256");
assert_eq!(tree.to_hex(), t);
assert_eq!(parents.len(), 1);
assert_eq!(parents[0].to_hex(), p);
assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
assert!(parse_commit_tree_and_parents_prefix(
b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
40
)
.is_none());
Ok(())
}
#[test]
fn object_id_loose_path_helpers() {
let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
assert_eq!(oid.loose_prefix(), "4b");
assert_eq!(oid.loose_suffix().len(), 38);
assert!(ObjectId::is_hex_len(40));
assert!(ObjectId::is_loose_suffix_len(38));
}
#[test]
fn tree_ref_iter_yields_at_most_one_error() {
let bad = b"100644 incomplete";
let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
assert!(iter.next().unwrap().is_err());
assert!(iter.next().is_none());
}
#[test]
fn tree_ref_iter_public_api_smoke() {
let data = serialize_tree(&[TreeEntry {
mode: 0o100644,
name: b"a".to_vec(),
oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
}]);
let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
let entry = iter.next().unwrap().expect("entry");
assert_eq!(entry.name, b"a");
let _owned = entry.to_owned();
let _vec = parse_tree(&data).expect("parse");
let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
}
#[test]
fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
let mut entries: Vec<TreeEntry> = (1..=60)
.map(|i| TreeEntry {
mode: 0o100644,
name: format!("f{i}.txt").into_bytes(),
oid: dummy,
})
.collect();
entries.push(TreeEntry {
mode: 0o040000,
name: b"sub".to_vec(),
oid: dummy,
});
entries.sort_by(|a, b| {
tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
});
let data = serialize_tree(&entries);
assert!(entries.len() >= 48);
let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
.expect("sub directory in wide tree");
assert_eq!(found.mode, 0o040000);
assert_eq!(found.name, b"sub");
Ok(())
}
fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
entries.sort_by(|a, b| {
tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
});
serialize_tree(entries)
}
#[test]
fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
let mut entries = vec![
TreeEntry {
mode: 0o040000,
name: b"foo".to_vec(),
oid: oid_a,
},
TreeEntry {
mode: 0o100644,
name: b"foo.c".to_vec(),
oid: oid_b,
},
TreeEntry {
mode: 0o100755,
name: b"foo-bar".to_vec(),
oid: oid_b,
},
TreeEntry {
mode: 0o120000,
name: b"link".to_vec(),
oid: oid_a,
},
TreeEntry {
mode: 0o160000,
name: b"sub".to_vec(),
oid: oid_b,
},
TreeEntry {
mode: 0o100644,
name: b"README".to_vec(),
oid: oid_a,
},
];
let data = sorted_tree_data(&mut entries);
for expected in parse_tree(&data)? {
let name_is_tree = expected.mode == 0o040000;
let found = find_tree_entry_by_name_in_data(
&data,
HashAlgo::Sha1.len(),
&expected.name,
name_is_tree,
)?
.expect("entry present");
assert_eq!(found, expected);
}
for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
assert!(
find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
.is_none(),
"unexpected match for {:?}",
String::from_utf8_lossy(missing)
);
}
Ok(())
}
#[test]
fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
let mut entries = vec![
TreeEntry {
mode: 0o100644,
name: b"a".to_vec(),
oid,
},
TreeEntry {
mode: 0o100644,
name: b"c".to_vec(),
oid,
},
];
let mut data = sorted_tree_data(&mut entries);
data.extend_from_slice(b"garbage without separators");
let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
assert!(
find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
);
assert!(matches!(
find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
Err(Error::CorruptObject(_))
));
Ok(())
}
#[test]
fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
assert!(matches!(
find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
Err(Error::CorruptObject(_))
));
let truncated = b"100644 a\0short";
assert!(matches!(
find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
Err(Error::CorruptObject(_))
));
}
#[test]
fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
let mut raw = b"100644 file\0".to_vec();
raw.extend_from_slice(oid.as_bytes());
let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
.expect("file");
assert_eq!(found.oid, oid);
assert_eq!(found.mode, 0o100644);
Ok(())
}
}