pub const GRM_MAGIC: [u8; 4] = [0x47, 0x52, 0x4D, 0x01];
pub const GRM_VERSION: u8 = 0x01;
pub const SIGNATURE_SIZE: usize = 64;
#[derive(Debug, Clone)]
pub struct GrmHeader {
pub schema_id: String,
pub signature: Option<[u8; SIGNATURE_SIZE]>,
}
impl GrmHeader {
pub fn new(schema_id: impl Into<String>) -> Self {
Self {
schema_id: schema_id.into(),
signature: None,
}
}
pub fn signed(schema_id: impl Into<String>, signature: [u8; SIGNATURE_SIZE]) -> Self {
Self {
schema_id: schema_id.into(),
signature: Some(signature),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, HeaderParseError> {
let schema_bytes = self.schema_id.as_bytes();
if schema_bytes.len() > u16::MAX as usize {
return Err(HeaderParseError::SchemaIdTooLong {
len: schema_bytes.len(),
max: u16::MAX as usize,
});
}
let schema_len = schema_bytes.len() as u16;
let capacity = 4 + 2 + schema_bytes.len() + SIGNATURE_SIZE;
let mut bytes = Vec::with_capacity(capacity);
bytes.extend_from_slice(&GRM_MAGIC);
bytes.extend_from_slice(&schema_len.to_le_bytes());
bytes.extend_from_slice(schema_bytes);
match &self.signature {
Some(sig) => bytes.extend_from_slice(sig),
None => bytes.extend_from_slice(&[0u8; SIGNATURE_SIZE]),
}
Ok(bytes)
}
pub fn from_bytes(data: &[u8]) -> Result<(Self, usize), HeaderParseError> {
const MIN_SIZE: usize = 4 + 2 + SIGNATURE_SIZE;
if data.len() < MIN_SIZE {
return Err(HeaderParseError::InsufficientData {
expected: MIN_SIZE,
received: data.len(),
});
}
if data[0..4] != GRM_MAGIC {
return Err(HeaderParseError::InvalidMagicBytes {
received: [data[0], data[1], data[2], data[3]],
});
}
let schema_len = u16::from_le_bytes([data[4], data[5]]) as usize;
let total_header_len = 4 + 2 + schema_len + SIGNATURE_SIZE;
if data.len() < total_header_len {
return Err(HeaderParseError::InsufficientData {
expected: total_header_len,
received: data.len(),
});
}
let schema_start = 6;
let schema_end = schema_start + schema_len;
let schema_id = std::str::from_utf8(&data[schema_start..schema_end])
.map_err(|_| HeaderParseError::InvalidSchemaId)?
.to_string();
let sig_start = schema_end;
let sig_end = sig_start + SIGNATURE_SIZE;
let sig_bytes: [u8; SIGNATURE_SIZE] = data[sig_start..sig_end]
.try_into()
.expect("Signature slice has wrong length");
let signature = if sig_bytes.iter().all(|&b| b == 0) {
None
} else {
Some(sig_bytes)
};
let header = GrmHeader {
schema_id,
signature,
};
Ok((header, total_header_len))
}
pub fn size(&self) -> usize {
4 + 2 + self.schema_id.len() + SIGNATURE_SIZE
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum HeaderParseError {
#[error("Insufficient data: expected {expected}, received {received}")]
InsufficientData {
expected: usize,
received: usize,
},
#[error("Invalid magic bytes: received {:02X?}", received)]
InvalidMagicBytes {
received: [u8; 4],
},
#[error("Invalid schema ID (not valid UTF-8)")]
InvalidSchemaId,
#[error("Schema ID too long: {len} bytes (maximum: {max})")]
SchemaIdTooLong {
len: usize,
max: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_magic_bytes() {
assert_eq!(&GRM_MAGIC[0..3], b"GRM");
assert_eq!(GRM_MAGIC[3], GRM_VERSION);
}
#[test]
fn test_header_roundtrip() {
let original = GrmHeader::new("de.gesundheit.praxis.v1");
let bytes = original.to_bytes().unwrap();
let (parsed, length) = GrmHeader::from_bytes(&bytes).unwrap();
assert_eq!(parsed.schema_id, original.schema_id);
assert_eq!(parsed.signature, None);
assert_eq!(length, bytes.len());
}
#[test]
fn test_header_with_signature() {
let signature = [0xAB; SIGNATURE_SIZE];
let original = GrmHeader::signed("test.v1", signature);
let bytes = original.to_bytes().unwrap();
let (parsed, _) = GrmHeader::from_bytes(&bytes).unwrap();
assert_eq!(parsed.signature, Some(signature));
}
#[test]
fn test_invalid_magic_bytes() {
let data = [0x00; 100];
let result = GrmHeader::from_bytes(&data);
assert!(matches!(
result,
Err(HeaderParseError::InvalidMagicBytes { .. })
));
}
#[test]
fn test_header_rejects_oversized_schema_id() {
let huge_id = "x".repeat(u16::MAX as usize + 1);
let header = GrmHeader::new(&huge_id);
assert!(matches!(
header.to_bytes(),
Err(HeaderParseError::SchemaIdTooLong { .. })
));
}
}