211 lines
6.9 KiB
Rust
211 lines
6.9 KiB
Rust
//! Commit object body, per the v1.1 trust-root revision §2.2.
|
|
//!
|
|
//! Field Type Description
|
|
//! tree 32 bytes Hash of the root tree
|
|
//! parent_count 1 byte
|
|
//! parents 32*N bytes
|
|
//! authority 32 bytes Hash of the authority object in effect
|
|
//! author_key 32 bytes Public key (Ed25519) of the author
|
|
//! timestamp 8 bytes Unix microseconds, LE int64
|
|
//! flags 1 byte Bit 0: modifies authority
|
|
//! Bit 1: fork commit
|
|
//! message_len 4 bytes
|
|
//! message N bytes UTF-8
|
|
|
|
use byteorder::{ByteOrder, LittleEndian};
|
|
|
|
use crate::error::Error;
|
|
use crate::hash::ObjectId;
|
|
use crate::object::{ObjectType, SignatureEntry, SignedObject};
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct CommitFlags(pub u8);
|
|
|
|
impl CommitFlags {
|
|
pub const NONE: CommitFlags = CommitFlags(0);
|
|
pub const MODIFIES_AUTHORITY: CommitFlags = CommitFlags(0b01);
|
|
pub const FORK: CommitFlags = CommitFlags(0b10);
|
|
|
|
pub fn modifies_authority(self) -> bool {
|
|
self.0 & 0b01 != 0
|
|
}
|
|
pub fn is_fork(self) -> bool {
|
|
self.0 & 0b10 != 0
|
|
}
|
|
|
|
pub fn raw(self) -> u8 {
|
|
self.0
|
|
}
|
|
|
|
pub fn validate(self) -> Result<(), Error> {
|
|
if self.0 & !0b11 != 0 {
|
|
return Err(Error::MalformedObject(format!(
|
|
"commit flags has reserved bits set: {:#x}",
|
|
self.0
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Commit {
|
|
pub tree: ObjectId,
|
|
pub parents: Vec<ObjectId>,
|
|
pub authority: ObjectId,
|
|
pub author_key: [u8; 32],
|
|
pub timestamp_micros: i64,
|
|
pub flags: CommitFlags,
|
|
pub message: String,
|
|
}
|
|
|
|
impl Commit {
|
|
pub fn body(&self) -> Result<Vec<u8>, Error> {
|
|
if self.parents.len() > 255 {
|
|
return Err(Error::MalformedObject("too many parents".into()));
|
|
}
|
|
if self.message.len() > u32::MAX as usize {
|
|
return Err(Error::MalformedObject("message too large".into()));
|
|
}
|
|
self.flags.validate()?;
|
|
let mut out = Vec::with_capacity(
|
|
32 + 1 + self.parents.len() * 32 + 32 + 32 + 8 + 1 + 4 + self.message.len(),
|
|
);
|
|
out.extend_from_slice(self.tree.as_bytes());
|
|
out.push(self.parents.len() as u8);
|
|
for p in &self.parents {
|
|
out.extend_from_slice(p.as_bytes());
|
|
}
|
|
out.extend_from_slice(self.authority.as_bytes());
|
|
out.extend_from_slice(&self.author_key);
|
|
let mut ts = [0u8; 8];
|
|
LittleEndian::write_i64(&mut ts, self.timestamp_micros);
|
|
out.extend_from_slice(&ts);
|
|
out.push(self.flags.0);
|
|
let mut len = [0u8; 4];
|
|
LittleEndian::write_u32(&mut len, self.message.len() as u32);
|
|
out.extend_from_slice(&len);
|
|
out.extend_from_slice(self.message.as_bytes());
|
|
Ok(out)
|
|
}
|
|
|
|
pub fn parse_body(body: &[u8]) -> Result<Self, Error> {
|
|
let need = 32 + 1;
|
|
if body.len() < need {
|
|
return Err(Error::MalformedObject(
|
|
"commit body too short for tree+parent_count".into(),
|
|
));
|
|
}
|
|
let mut tree = [0u8; 32];
|
|
tree.copy_from_slice(&body[0..32]);
|
|
let parent_count = body[32] as usize;
|
|
let parents_end = 33 + parent_count * 32;
|
|
if body.len() < parents_end + 32 + 32 + 8 + 1 + 4 {
|
|
return Err(Error::MalformedObject("commit body truncated".into()));
|
|
}
|
|
let mut parents = Vec::with_capacity(parent_count);
|
|
for i in 0..parent_count {
|
|
let off = 33 + i * 32;
|
|
let mut p = [0u8; 32];
|
|
p.copy_from_slice(&body[off..off + 32]);
|
|
parents.push(ObjectId(p));
|
|
}
|
|
let mut p = parents_end;
|
|
let mut authority = [0u8; 32];
|
|
authority.copy_from_slice(&body[p..p + 32]);
|
|
p += 32;
|
|
let mut author_key = [0u8; 32];
|
|
author_key.copy_from_slice(&body[p..p + 32]);
|
|
p += 32;
|
|
let timestamp_micros = LittleEndian::read_i64(&body[p..p + 8]);
|
|
p += 8;
|
|
let flags = CommitFlags(body[p]);
|
|
flags.validate()?;
|
|
p += 1;
|
|
let msg_len = LittleEndian::read_u32(&body[p..p + 4]) as usize;
|
|
p += 4;
|
|
if body.len() < p + msg_len {
|
|
return Err(Error::MalformedObject("commit message truncated".into()));
|
|
}
|
|
let message = std::str::from_utf8(&body[p..p + msg_len])
|
|
.map_err(|_| Error::MalformedObject("commit message not UTF-8".into()))?
|
|
.to_string();
|
|
p += msg_len;
|
|
if p != body.len() {
|
|
return Err(Error::MalformedObject(format!(
|
|
"trailing {} byte(s) after commit message",
|
|
body.len() - p
|
|
)));
|
|
}
|
|
Ok(Self {
|
|
tree: ObjectId(tree),
|
|
parents,
|
|
authority: ObjectId(authority),
|
|
author_key,
|
|
timestamp_micros,
|
|
flags,
|
|
message,
|
|
})
|
|
}
|
|
|
|
pub fn into_signed(self) -> Result<SignedObject, Error> {
|
|
Ok(SignedObject::new(ObjectType::Commit, self.body()?))
|
|
}
|
|
|
|
/// Parse from a complete signed-object's components.
|
|
pub fn from_signed(s: &SignedObject) -> Result<Self, Error> {
|
|
if s.object_type != ObjectType::Commit {
|
|
return Err(Error::MalformedObject(format!(
|
|
"expected commit, got {}",
|
|
s.object_type.name()
|
|
)));
|
|
}
|
|
if s.signatures.len() != 1 {
|
|
return Err(Error::MalformedObject(format!(
|
|
"commit must have exactly 1 signature, got {}",
|
|
s.signatures.len()
|
|
)));
|
|
}
|
|
let c = Commit::parse_body(&s.body)?;
|
|
if c.author_key != s.signatures[0].public_key {
|
|
return Err(Error::MalformedObject(
|
|
"commit author_key disagrees with trailer signature key".into(),
|
|
));
|
|
}
|
|
Ok(c)
|
|
}
|
|
|
|
/// Convenience: produce a partial signature entry with just the key set;
|
|
/// callers fill in `signature` after computing the Ed25519 signature.
|
|
pub fn signature_template(&self) -> SignatureEntry {
|
|
SignatureEntry {
|
|
public_key: self.author_key,
|
|
signature: [0u8; 64],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn commit_body_roundtrip() {
|
|
let c = Commit {
|
|
tree: ObjectId([1; 32]),
|
|
parents: vec![ObjectId([2; 32]), ObjectId([3; 32])],
|
|
authority: ObjectId([4; 32]),
|
|
author_key: [5; 32],
|
|
timestamp_micros: 1_700_000_000_000_000,
|
|
flags: CommitFlags::NONE,
|
|
message: "hello world".into(),
|
|
};
|
|
let body = c.body().unwrap();
|
|
let c2 = Commit::parse_body(&body).unwrap();
|
|
assert_eq!(c.tree, c2.tree);
|
|
assert_eq!(c.parents, c2.parents);
|
|
assert_eq!(c.message, c2.message);
|
|
assert_eq!(c.flags.0, c2.flags.0);
|
|
}
|
|
}
|