use chrono::{DateTime, Utc};
use cortex_core::Event;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RowSignature {
pub schema_version: u16,
pub key_id: String,
pub signed_at: DateTime<Utc>,
pub bytes: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedRow {
#[serde(flatten)]
pub event: Event,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<RowSignature>,
}
impl SignedRow {
#[must_use]
pub fn unsigned(event: Event) -> Self {
Self {
event,
signature: None,
}
}
}
#[must_use]
pub fn b64_encode(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity((bytes.len() * 4).div_ceil(3));
let mut i = 0;
while i + 3 <= bytes.len() {
let b0 = bytes[i];
let b1 = bytes[i + 1];
let b2 = bytes[i + 2];
out.push(ALPHABET[(b0 >> 2) as usize] as char);
out.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
out.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
out.push(ALPHABET[(b2 & 0x3f) as usize] as char);
i += 3;
}
let rem = bytes.len() - i;
if rem == 1 {
let b0 = bytes[i];
out.push(ALPHABET[(b0 >> 2) as usize] as char);
out.push(ALPHABET[((b0 & 0x03) << 4) as usize] as char);
} else if rem == 2 {
let b0 = bytes[i];
let b1 = bytes[i + 1];
out.push(ALPHABET[(b0 >> 2) as usize] as char);
out.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
out.push(ALPHABET[((b1 & 0x0f) << 2) as usize] as char);
}
out
}
#[must_use]
pub fn b64_decode(s: &str) -> Option<Vec<u8>> {
fn decode_char(c: u8) -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c - b'A'),
b'a'..=b'z' => Some(c - b'a' + 26),
b'0'..=b'9' => Some(c - b'0' + 52),
b'-' => Some(62),
b'_' => Some(63),
_ => None,
}
}
let bytes = s.as_bytes();
if bytes.len() % 4 == 1 {
return None;
}
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
let mut i = 0;
while i + 4 <= bytes.len() {
let v0 = decode_char(bytes[i])?;
let v1 = decode_char(bytes[i + 1])?;
let v2 = decode_char(bytes[i + 2])?;
let v3 = decode_char(bytes[i + 3])?;
out.push((v0 << 2) | (v1 >> 4));
out.push((v1 << 4) | (v2 >> 2));
out.push((v2 << 6) | v3);
i += 4;
}
let rem = bytes.len() - i;
if rem == 2 {
let v0 = decode_char(bytes[i])?;
let v1 = decode_char(bytes[i + 1])?;
out.push((v0 << 2) | (v1 >> 4));
} else if rem == 3 {
let v0 = decode_char(bytes[i])?;
let v1 = decode_char(bytes[i + 1])?;
let v2 = decode_char(bytes[i + 2])?;
out.push((v0 << 2) | (v1 >> 4));
out.push((v1 << 4) | (v2 >> 2));
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b64_roundtrip_aligned() {
let bytes: Vec<u8> = (0..64).collect();
let s = b64_encode(&bytes);
let back = b64_decode(&s).unwrap();
assert_eq!(bytes, back);
}
#[test]
fn b64_roundtrip_one_byte_remainder() {
let bytes = vec![0xAB, 0xCD, 0xEF, 0x12];
let s = b64_encode(&bytes);
assert_eq!(s.len(), 6); assert_eq!(b64_decode(&s).unwrap(), bytes);
}
#[test]
fn b64_roundtrip_two_byte_remainder() {
let bytes = vec![0xAB, 0xCD, 0xEF, 0x12, 0x34];
let s = b64_encode(&bytes);
assert_eq!(s.len(), 7);
assert_eq!(b64_decode(&s).unwrap(), bytes);
}
#[test]
fn b64_rejects_invalid_chars() {
assert!(b64_decode("AAAA!AAA").is_none());
}
#[test]
fn b64_rejects_invalid_length() {
assert!(b64_decode("A").is_none());
}
}