use gix_hash::{Kind as HashKind, ObjectId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Kind {
Tree,
Blob,
Commit,
Tag,
}
impl Kind {
pub fn resolved(ty: git_storage_trait::ObjType) -> Option<Self> {
use git_storage_trait::ObjType;
Some(match ty {
ObjType::Commit => Kind::Commit,
ObjType::Tree => Kind::Tree,
ObjType::Blob => Kind::Blob,
ObjType::Tag => Kind::Tag,
_ => return None,
})
}
pub fn as_str(self) -> &'static str {
match self {
Kind::Tree => "tree",
Kind::Blob => "blob",
Kind::Commit => "commit",
Kind::Tag => "tag",
}
}
pub fn from_bytes(b: &[u8]) -> Option<Self> {
Some(match b {
b"tree" => Kind::Tree,
b"blob" => Kind::Blob,
b"commit" => Kind::Commit,
b"tag" => Kind::Tag,
_ => return None,
})
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<Kind> for git_storage_trait::ObjType {
fn from(k: Kind) -> Self {
use git_storage_trait::ObjType;
match k {
Kind::Commit => ObjType::Commit,
Kind::Tree => ObjType::Tree,
Kind::Blob => ObjType::Blob,
Kind::Tag => ObjType::Tag,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntryKind {
Tree,
Blob,
BlobExecutable,
Link,
Commit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TreeEntry {
pub oid: ObjectId,
pub kind: EntryKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NamedTreeEntry<'a> {
pub oid: ObjectId,
pub kind: EntryKind,
pub name: &'a [u8],
pub mode: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub name: Vec<u8>,
pub email: Vec<u8>,
pub seconds: i64,
pub offset_seconds: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commit {
pub tree: ObjectId,
pub parents: Vec<ObjectId>,
pub author: Signature,
pub committer: Signature,
pub message: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
pub target: ObjectId,
pub target_kind: Option<Kind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError {
kind: Kind,
what: &'static str,
}
impl DecodeError {
fn new(kind: Kind, what: &'static str) -> Self {
DecodeError { kind, what }
}
pub fn object_kind(&self) -> Kind {
self.kind
}
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "malformed {}: {}", self.kind, self.what)
}
}
impl std::error::Error for DecodeError {}
pub type DecodeResult<T> = std::result::Result<T, DecodeError>;
fn split_headers(bytes: &[u8]) -> (&[u8], &[u8]) {
let mut i = 0;
while i < bytes.len() {
match memchr(b'\n', &bytes[i..]) {
Some(rel) => {
let nl = i + rel;
if bytes.get(nl + 1) == Some(&b'\n') {
return (&bytes[..nl + 1], &bytes[nl + 2..]);
}
i = nl + 1;
}
None => break,
}
}
(bytes, &[])
}
struct HeaderLines<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for HeaderLines<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
if self.rest.is_empty() {
return None;
}
match memchr(b'\n', self.rest) {
Some(nl) => {
let line = &self.rest[..nl];
self.rest = &self.rest[nl + 1..];
Some(line)
}
None => {
let line = self.rest;
self.rest = &[];
Some(line)
}
}
}
}
fn header_lines(headers: &[u8]) -> HeaderLines<'_> {
HeaderLines { rest: headers }
}
#[inline]
fn memchr(needle: u8, haystack: &[u8]) -> Option<usize> {
haystack.iter().position(|&b| b == needle)
}
#[inline]
fn strip<'a>(line: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
line.strip_prefix(prefix)
}
#[inline]
fn hex_oid(field: &[u8], hash: HashKind, kind: Kind, what: &'static str) -> DecodeResult<ObjectId> {
if field.len() != hash.len_in_hex() {
return Err(DecodeError::new(kind, what));
}
ObjectId::from_hex(field).map_err(|_| DecodeError::new(kind, what))
}
fn signature(line: &[u8]) -> Signature {
let close = line.iter().rposition(|&b| b == b'>');
let (name_email, tail) = match close {
Some(at) => (&line[..at], &line[at + 1..]),
None => (line, &line[line.len()..]),
};
let open = name_email.iter().position(|&b| b == b'<');
let (name, email) = match open {
Some(at) => (&name_email[..at], &name_email[at + 1..]),
None => (name_email, &name_email[name_email.len()..]),
};
let mut fields = tail.split(|&b| b == b' ').filter(|f| !f.is_empty());
let seconds = fields
.next()
.and_then(|f| std::str::from_utf8(f).ok())
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let offset_seconds = fields.next().map(tz_offset).unwrap_or(0);
Signature {
name: trim_ascii_end(name).to_vec(),
email: email.to_vec(),
seconds,
offset_seconds,
}
}
fn tz_offset(field: &[u8]) -> i32 {
let (sign, digits) = match field.first() {
Some(b'-') => (-1, &field[1..]),
Some(b'+') => (1, &field[1..]),
_ => (1, field),
};
if digits.len() != 4 || !digits.iter().all(|b| b.is_ascii_digit()) {
return 0;
}
let hours = (digits[0] - b'0') as i32 * 10 + (digits[1] - b'0') as i32;
let minutes = (digits[2] - b'0') as i32 * 10 + (digits[3] - b'0') as i32;
sign * (hours * 3600 + minutes * 60)
}
#[inline]
fn trim_ascii_end(mut b: &[u8]) -> &[u8] {
while let Some((&last, rest)) = b.split_last() {
if last.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
b
}
pub fn commit_links(
bytes: &[u8],
hash: HashKind,
parents: &mut Vec<ObjectId>,
) -> DecodeResult<ObjectId> {
let (headers, _) = split_headers(bytes);
let mut lines = header_lines(headers);
let tree = lines
.next()
.and_then(|l| strip(l, b"tree "))
.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?;
let tree = hex_oid(tree, hash, Kind::Commit, "tree header")?;
parents.clear();
for line in lines {
match strip(line, b"parent ") {
Some(field) => parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?),
None => break,
}
}
Ok(tree)
}
pub fn commit_facts(
bytes: &[u8],
hash: HashKind,
parents: &mut Vec<ObjectId>,
) -> DecodeResult<(ObjectId, i64)> {
let (headers, _) = split_headers(bytes);
let mut lines = header_lines(headers);
let tree = lines
.next()
.and_then(|l| strip(l, b"tree "))
.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?;
let tree = hex_oid(tree, hash, Kind::Commit, "tree header")?;
parents.clear();
let mut committed_at = 0;
let mut in_parents = true;
for line in lines {
if in_parents {
if let Some(field) = strip(line, b"parent ") {
parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?);
continue;
}
in_parents = false;
}
if let Some(field) = strip(line, b"committer ") {
committed_at = signature(field).seconds;
break;
}
}
Ok((tree, committed_at))
}
pub fn commit(bytes: &[u8], hash: HashKind) -> DecodeResult<Commit> {
let (headers, body) = split_headers(bytes);
let mut tree = None;
let mut parents = Vec::new();
let mut author = None;
let mut committer = None;
for line in header_lines(headers) {
if let Some(field) = strip(line, b"tree ") {
if tree.is_none() {
tree = Some(hex_oid(field, hash, Kind::Commit, "tree header")?);
}
} else if let Some(field) = strip(line, b"parent ") {
parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?);
} else if let Some(field) = strip(line, b"author ") {
if author.is_none() {
author = Some(signature(field));
}
} else if let Some(field) = strip(line, b"committer ") {
if committer.is_none() {
committer = Some(signature(field));
}
}
}
Ok(Commit {
tree: tree.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?,
parents,
author: author.ok_or_else(|| DecodeError::new(Kind::Commit, "no author header"))?,
committer: committer
.ok_or_else(|| DecodeError::new(Kind::Commit, "no committer header"))?,
message: body.to_vec(),
})
}
pub fn tag(bytes: &[u8], hash: HashKind) -> DecodeResult<Tag> {
let (headers, _) = split_headers(bytes);
let mut target = None;
let mut target_kind = None;
for line in header_lines(headers) {
if let Some(field) = strip(line, b"object ") {
if target.is_none() {
target = Some(hex_oid(field, hash, Kind::Tag, "object header")?);
}
} else if let Some(field) = strip(line, b"type ") {
if target_kind.is_none() {
target_kind = Kind::from_bytes(field);
}
}
}
Ok(Tag {
target: target.ok_or_else(|| DecodeError::new(Kind::Tag, "no object header"))?,
target_kind,
})
}
pub fn tag_target(bytes: &[u8], hash: HashKind) -> DecodeResult<ObjectId> {
tag(bytes, hash).map(|t| t.target)
}
pub struct TreeIter<'a> {
inner: NamedTreeIter<'a>,
}
impl<'a> TreeIter<'a> {
pub fn from_bytes(bytes: &'a [u8], hash: HashKind) -> Self {
TreeIter {
inner: NamedTreeIter::from_bytes(bytes, hash),
}
}
pub fn named(self) -> NamedTreeIter<'a> {
self.inner
}
}
impl Iterator for TreeIter<'_> {
type Item = DecodeResult<TreeEntry>;
fn next(&mut self) -> Option<DecodeResult<TreeEntry>> {
Some(self.inner.next()?.map(|entry| TreeEntry {
oid: entry.oid,
kind: entry.kind,
}))
}
}
pub struct NamedTreeIter<'a> {
rest: &'a [u8],
hash_len: usize,
failed: bool,
}
impl<'a> NamedTreeIter<'a> {
pub fn from_bytes(bytes: &'a [u8], hash: HashKind) -> Self {
NamedTreeIter {
rest: bytes,
hash_len: hash.len_in_bytes(),
failed: false,
}
}
}
impl<'a> Iterator for NamedTreeIter<'a> {
type Item = DecodeResult<NamedTreeEntry<'a>>;
fn next(&mut self) -> Option<DecodeResult<NamedTreeEntry<'a>>> {
if self.failed || self.rest.is_empty() {
return None;
}
match self.entry() {
Ok(entry) => Some(Ok(entry)),
Err(err) => {
self.failed = true;
Some(Err(err))
}
}
}
}
impl<'a> NamedTreeIter<'a> {
fn entry(&mut self) -> DecodeResult<NamedTreeEntry<'a>> {
let bad = |what| DecodeError::new(Kind::Tree, what);
let space =
memchr(b' ', self.rest).ok_or_else(|| bad("an entry with no mode delimiter"))?;
let mode =
octal(&self.rest[..space]).ok_or_else(|| bad("an entry whose mode is not octal"))?;
let after_mode = &self.rest[space + 1..];
let nul = memchr(0, after_mode).ok_or_else(|| bad("an entry with no name terminator"))?;
let name = &after_mode[..nul];
if name.is_empty() {
return Err(bad("an entry with an empty name"));
}
let after_name = &after_mode[nul + 1..];
if after_name.len() < self.hash_len {
return Err(bad("an entry whose oid runs off the end"));
}
let (raw, rest) = after_name.split_at(self.hash_len);
self.rest = rest;
Ok(NamedTreeEntry {
oid: ObjectId::from_bytes_or_panic(raw),
kind: entry_kind(mode),
name,
mode,
})
}
}
pub fn tree_entry_named(
bytes: &[u8],
hash: HashKind,
name: &[u8],
) -> DecodeResult<Option<TreeEntry>> {
let bad = |what| DecodeError::new(Kind::Tree, what);
let hash_len = hash.len_in_bytes();
let mut rest = bytes;
while !rest.is_empty() {
let space = memchr(b' ', rest).ok_or_else(|| bad("an entry with no mode delimiter"))?;
let mode = octal(&rest[..space]).ok_or_else(|| bad("an entry whose mode is not octal"))?;
let after_mode = &rest[space + 1..];
let nul = memchr(0, after_mode).ok_or_else(|| bad("an entry with no name terminator"))?;
let entry_name = &after_mode[..nul];
let after_name = &after_mode[nul + 1..];
if after_name.len() < hash_len {
return Err(bad("an entry whose oid runs off the end"));
}
let (raw, tail) = after_name.split_at(hash_len);
if entry_name == name {
return Ok(Some(TreeEntry {
oid: ObjectId::from_bytes_or_panic(raw),
kind: entry_kind(mode),
}));
}
rest = tail;
}
Ok(None)
}
fn octal(field: &[u8]) -> Option<u32> {
if field.is_empty() || field.len() > 6 {
return None;
}
let mut mode = 0u32;
for &b in field {
let d = b.wrapping_sub(b'0');
if d > 7 {
return None;
}
mode = (mode << 3) | d as u32;
}
Some(mode)
}
fn entry_kind(mode: u32) -> EntryKind {
const IFMT: u32 = 0o170000;
match mode & IFMT {
0o100000 => {
if mode & 0o100 == 0o100 {
EntryKind::BlobExecutable
} else {
EntryKind::Blob
}
}
0o120000 => EntryKind::Link,
0o040000 | 0o140000 => EntryKind::Tree,
_ => EntryKind::Commit,
}
}