use crate::codec::bytes;
use crate::wire::tail::RECORD_LIMIT;
use anyhow::{Result, ensure};
const BASE32: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
pub const MAX_MOVE_TASKS: usize = 256;
fn valid(ok: bool) -> Result<()> {
ensure!(ok, "error encrypted-tail-invalid");
Ok(())
}
fn encode_task(bytes: &[u8]) -> String {
let mut value = 0_u128;
for byte in bytes {
value = (value << 8) | u128::from(*byte);
}
(0..16)
.rev()
.map(|shift| BASE32[((value >> (shift * 5)) & 31) as usize] as char)
.collect()
}
#[derive(Clone, PartialEq, Eq)]
pub enum Projection {
None,
Move {
source: String,
target: String,
tasks: Vec<String>,
},
Ref {
workspace: String,
task: String,
reference: String,
descriptor: Vec<u8>,
deleted: bool,
version: Option<String>,
},
Unref {
workspace: String,
task: String,
reference: String,
},
Parent {
action: super::parent::ParentAction,
workspace: String,
task: String,
deleted: bool,
version: Option<String>,
},
}
impl Projection {
pub fn encode(&self) -> Vec<u8> {
match self {
Self::Move {
source,
target,
tasks,
} => {
let mut out = vec![4];
encode_text(&mut out, source);
encode_text(&mut out, target);
out.extend((tasks.len() as u16).to_be_bytes());
for task in tasks {
let mut bits = 0_u128;
for byte in task.bytes() {
bits = (bits << 5)
| BASE32
.iter()
.position(|v| *v == byte)
.expect("validated task ID") as u128;
}
out.extend_from_slice(&bits.to_be_bytes()[6..]);
}
return out;
}
Self::Ref {
workspace,
task,
reference,
descriptor,
deleted,
version,
} => {
let mut out = vec![2];
for text in [workspace, task, reference] {
encode_text(&mut out, text);
}
out.extend((descriptor.len() as u32).to_be_bytes());
out.extend(descriptor);
out.push(u8::from(*deleted));
out.push(u8::from(version.is_some()));
if let Some(v) = version {
encode_text(&mut out, v);
}
return out;
}
Self::Unref {
workspace,
task,
reference,
} => {
let mut out = vec![3];
for text in [workspace, task, reference] {
encode_text(&mut out, text);
}
return out;
}
_ => {}
}
let Self::Parent {
action,
workspace,
task,
deleted,
version,
} = self
else {
return vec![0];
};
let mut out = vec![1, *action as u8];
encode_text(&mut out, workspace);
encode_text(&mut out, task);
out.push(u8::from(*deleted));
out.push(u8::from(version.is_some()));
if let Some(v) = version {
encode_text(&mut out, v);
}
out
}
pub fn decode(input: &[u8]) -> Result<Self> {
decode_projection(input)
}
}
struct Reader<'a>(&'a [u8]);
impl<'a> Reader<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
valid(n <= self.0.len())?;
let (a, b) = self.0.split_at(n);
self.0 = b;
Ok(a)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
Ok(self.take(N)?.try_into()?)
}
fn blob(&mut self, max: usize) -> Result<&'a [u8]> {
let n = u32::from_be_bytes(self.array()?) as usize;
valid(n <= max)?;
self.take(n)
}
}
pub struct Envelope<'a> {
pub vault: [u8; 32],
pub stream: [u8; 32],
pub generation: [u8; 32],
pub id: String,
pub projection: Projection,
pub header: &'a [u8],
pub body: &'a [u8],
pub nonce: [u8; 24],
}
pub fn parse(record: &[u8]) -> Result<Envelope<'_>> {
valid(record.len() <= RECORD_LIMIT)?;
let mut r = Reader(record);
let header = r.blob(4544)?;
let body = r.blob(131072 + 16)?;
valid(r.0.is_empty() && body.len() >= 16)?;
let mut r = Reader(header);
valid(r.take(8)? == b"AVEN\x01\x00\x01\x01")?;
let vault = r.blob(32)?.try_into()?;
let stream = r.blob(32)?.try_into()?;
let generation = r.blob(32)?.try_into()?;
valid(r.blob(32)?.len() == 32)?;
let id = std::str::from_utf8(r.blob(256)?)?.to_owned();
valid(!id.is_empty())?;
valid(r.array::<4>()? == 18u32.to_be_bytes())?;
let projection = Projection::decode(r.blob(4096)?)?;
let nonce = r.blob(24)?.try_into()?;
valid(r.0.is_empty())?;
Ok(Envelope {
vault,
stream,
generation,
id,
projection,
header,
body,
nonce,
})
}
pub fn encode_text(out: &mut Vec<u8>, s: &str) {
bytes(out, s.as_bytes());
}
pub fn decode_projection(input: &[u8]) -> Result<Projection> {
let mut r = Reader(input);
let result = match r.take(1)?[0] {
0 => Projection::None,
1 => {
let action = super::parent::ParentAction::from_byte(r.take(1)?[0])
.ok_or_else(|| anyhow::anyhow!("error encrypted-tail-invalid"))?;
let workspace = std::str::from_utf8(r.blob(256)?)?.to_owned();
let task = std::str::from_utf8(r.blob(256)?)?.to_owned();
let deleted = r.take(1)?[0];
valid(deleted <= 1)?;
let version = match r.take(1)?[0] {
0 => None,
1 => Some(std::str::from_utf8(r.blob(256)?)?.to_owned()),
_ => anyhow::bail!("error encrypted-tail-projection"),
};
valid(
!workspace.is_empty()
&& !task.is_empty()
&& version.as_ref().is_none_or(|s| !s.is_empty()),
)?;
valid(
action != super::parent::ParentAction::Create
|| (deleted == 0 && version.is_some()),
)?;
Projection::Parent {
action,
workspace,
task,
deleted: deleted == 1,
version,
}
}
kind @ (2 | 3) => {
let mut text = || -> Result<String> {
let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
valid(!s.is_empty())?;
Ok(s)
};
let workspace = text()?;
let task = text()?;
let reference = text()?;
if kind == 3 {
Projection::Unref {
workspace,
task,
reference,
}
} else {
let descriptor = r.blob(1984)?.to_vec();
crate::images::Descriptor::decode(&descriptor)?;
let deleted = r.take(1)?[0];
valid(deleted <= 1)?;
let version = match r.take(1)?[0] {
0 => None,
1 => {
let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
valid(!s.is_empty())?;
Some(s)
}
_ => anyhow::bail!("error encrypted-image-hint"),
};
Projection::Ref {
workspace,
task,
reference,
descriptor,
deleted: deleted == 1,
version,
}
}
}
4 => {
let source = std::str::from_utf8(r.blob(16)?)?.to_owned();
let target = std::str::from_utf8(r.blob(16)?)?.to_owned();
for id in [&source, &target] {
crate::workspace::validate(id)?;
}
valid(source != target)?;
let count = u16::from_be_bytes(r.array()?) as usize;
valid((1..=MAX_MOVE_TASKS).contains(&count))?;
let mut tasks: Vec<String> = Vec::with_capacity(count);
for _ in 0..count {
let mut bytes = [0_u8; 16];
bytes[6..].copy_from_slice(r.take(10)?);
let task = encode_task(&bytes[6..]);
valid(tasks.last().is_none_or(|previous| previous < &task))?;
tasks.push(task);
}
Projection::Move {
source,
target,
tasks,
}
}
_ => anyhow::bail!("error encrypted-tail-projection"),
};
valid(r.0.is_empty())?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn move_workspace_validation_keeps_fixed_bytes_and_refusals() {
let projection = Projection::Move {
source: "0123456789ABCDEF".into(),
target: "GHJKMNPQRSTVWXYZ".into(),
tasks: vec!["0000000000000000".into()],
};
let bytes = projection.encode();
assert_eq!(
hex::encode(&bytes),
"0400000010303132333435363738394142434445460000001047484a4b4d4e5051525354565758595a000100000000000000000000"
);
assert!(Projection::decode(&bytes).unwrap() == projection);
for field in [0, 1] {
for invalid in [
"0123456789ABCDE",
"0123456789abcdef",
"0123456789ABCDEI",
"0123456789ABCDEL",
"0123456789ABCDEO",
"0123456789ABCDEU",
"00000000000000é",
] {
let mut bad = vec![4];
encode_text(
&mut bad,
if field == 0 {
invalid
} else {
"0123456789ABCDEF"
},
);
encode_text(
&mut bad,
if field == 1 {
invalid
} else {
"GHJKMNPQRSTVWXYZ"
},
);
bad.extend(1_u16.to_be_bytes());
bad.extend([0; 10]);
assert_eq!(
Projection::decode(&bad).err().unwrap().to_string(),
"workspace ID must be 16 Crockford Base32 characters"
);
}
}
}
}