167 lines
5.7 KiB
Rust
167 lines
5.7 KiB
Rust
//! Release object body, per the v1.1 trust-root revision §2.3.
|
|
//!
|
|
//! Field Type Description
|
|
//! tree 32 bytes
|
|
//! parent_release 32 bytes (zero for first release)
|
|
//! predecessor 32 bytes hash of the commit being released
|
|
//! authority 32 bytes
|
|
//! declarer_key 32 bytes
|
|
//! timestamp 8 bytes Unix microseconds, LE int64
|
|
//! label_len 2 bytes uint16 LE, max 256
|
|
//! label N bytes UTF-8
|
|
//! notes_len 4 bytes uint32 LE
|
|
//! notes N bytes UTF-8
|
|
|
|
use byteorder::{ByteOrder, LittleEndian};
|
|
|
|
use crate::error::Error;
|
|
use crate::hash::ObjectId;
|
|
use crate::object::{ObjectType, SignedObject};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Release {
|
|
pub tree: ObjectId,
|
|
pub parent_release: ObjectId,
|
|
pub predecessor: ObjectId,
|
|
pub authority: ObjectId,
|
|
pub declarer_key: [u8; 32],
|
|
pub timestamp_micros: i64,
|
|
pub label: String,
|
|
pub notes: String,
|
|
}
|
|
|
|
impl Release {
|
|
pub fn body(&self) -> Result<Vec<u8>, Error> {
|
|
if self.label.len() > 256 {
|
|
return Err(Error::MalformedObject("release label too long".into()));
|
|
}
|
|
if self.notes.len() > u32::MAX as usize {
|
|
return Err(Error::MalformedObject("release notes too large".into()));
|
|
}
|
|
let mut out =
|
|
Vec::with_capacity(32 * 4 + 32 + 8 + 2 + self.label.len() + 4 + self.notes.len());
|
|
out.extend_from_slice(self.tree.as_bytes());
|
|
out.extend_from_slice(self.parent_release.as_bytes());
|
|
out.extend_from_slice(self.predecessor.as_bytes());
|
|
out.extend_from_slice(self.authority.as_bytes());
|
|
out.extend_from_slice(&self.declarer_key);
|
|
let mut ts = [0u8; 8];
|
|
LittleEndian::write_i64(&mut ts, self.timestamp_micros);
|
|
out.extend_from_slice(&ts);
|
|
let mut ll = [0u8; 2];
|
|
LittleEndian::write_u16(&mut ll, self.label.len() as u16);
|
|
out.extend_from_slice(&ll);
|
|
out.extend_from_slice(self.label.as_bytes());
|
|
let mut nl = [0u8; 4];
|
|
LittleEndian::write_u32(&mut nl, self.notes.len() as u32);
|
|
out.extend_from_slice(&nl);
|
|
out.extend_from_slice(self.notes.as_bytes());
|
|
Ok(out)
|
|
}
|
|
|
|
pub fn parse_body(body: &[u8]) -> Result<Self, Error> {
|
|
let min = 32 * 5 + 8 + 2;
|
|
if body.len() < min {
|
|
return Err(Error::MalformedObject("release body truncated".into()));
|
|
}
|
|
let mut p = 0usize;
|
|
let take32 = |p: &mut usize| -> [u8; 32] {
|
|
let mut h = [0u8; 32];
|
|
h.copy_from_slice(&body[*p..*p + 32]);
|
|
*p += 32;
|
|
h
|
|
};
|
|
let tree = ObjectId(take32(&mut p));
|
|
let parent_release = ObjectId(take32(&mut p));
|
|
let predecessor = ObjectId(take32(&mut p));
|
|
let authority = ObjectId(take32(&mut p));
|
|
let declarer_key = take32(&mut p);
|
|
let timestamp_micros = LittleEndian::read_i64(&body[p..p + 8]);
|
|
p += 8;
|
|
let label_len = LittleEndian::read_u16(&body[p..p + 2]) as usize;
|
|
p += 2;
|
|
if body.len() < p + label_len + 4 {
|
|
return Err(Error::MalformedObject(
|
|
"release label/notes truncated".into(),
|
|
));
|
|
}
|
|
let label = std::str::from_utf8(&body[p..p + label_len])
|
|
.map_err(|_| Error::MalformedObject("release label not UTF-8".into()))?
|
|
.to_string();
|
|
p += label_len;
|
|
let notes_len = LittleEndian::read_u32(&body[p..p + 4]) as usize;
|
|
p += 4;
|
|
if body.len() < p + notes_len {
|
|
return Err(Error::MalformedObject("release notes truncated".into()));
|
|
}
|
|
let notes = std::str::from_utf8(&body[p..p + notes_len])
|
|
.map_err(|_| Error::MalformedObject("release notes not UTF-8".into()))?
|
|
.to_string();
|
|
p += notes_len;
|
|
if p != body.len() {
|
|
return Err(Error::MalformedObject(
|
|
"trailing bytes after release notes".into(),
|
|
));
|
|
}
|
|
Ok(Self {
|
|
tree,
|
|
parent_release,
|
|
predecessor,
|
|
authority,
|
|
declarer_key,
|
|
timestamp_micros,
|
|
label,
|
|
notes,
|
|
})
|
|
}
|
|
|
|
pub fn into_signed(self) -> Result<SignedObject, Error> {
|
|
Ok(SignedObject::new(ObjectType::Release, self.body()?))
|
|
}
|
|
|
|
pub fn from_signed(s: &SignedObject) -> Result<Self, Error> {
|
|
if s.object_type != ObjectType::Release {
|
|
return Err(Error::MalformedObject(format!(
|
|
"expected release, got {}",
|
|
s.object_type.name()
|
|
)));
|
|
}
|
|
if s.signatures.is_empty() {
|
|
return Err(Error::MalformedObject(
|
|
"release must have at least one signature".into(),
|
|
));
|
|
}
|
|
let r = Release::parse_body(&s.body)?;
|
|
if r.declarer_key != s.signatures[0].public_key {
|
|
return Err(Error::MalformedObject(
|
|
"release declarer_key disagrees with first signature key".into(),
|
|
));
|
|
}
|
|
Ok(r)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn release_body_roundtrip() {
|
|
let r = Release {
|
|
tree: ObjectId([1; 32]),
|
|
parent_release: ObjectId([0; 32]),
|
|
predecessor: ObjectId([2; 32]),
|
|
authority: ObjectId([3; 32]),
|
|
declarer_key: [4; 32],
|
|
timestamp_micros: 1_700_000_000_000_000,
|
|
label: "v0.1.0".into(),
|
|
notes: "first release".into(),
|
|
};
|
|
let body = r.body().unwrap();
|
|
let r2 = Release::parse_body(&body).unwrap();
|
|
assert_eq!(r.label, r2.label);
|
|
assert_eq!(r.notes, r2.notes);
|
|
assert_eq!(r.tree, r2.tree);
|
|
}
|
|
}
|