use crate::error::{Error, Result};
use crate::pack::{Guid, Pack, ReadCursor, WriteCursor};
use crate::types::FileId;
pub const NAME_DH2Q: &[u8] = b"DH2Q";
pub const NAME_DH2C: &[u8] = b"DH2C";
pub const NAME_QFID: &[u8] = b"QFid";
const FLAG_PERSISTENT: u32 = 0x0000_0002;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateContext {
pub name: Vec<u8>,
pub data: Vec<u8>,
}
impl CreateContext {
pub fn new(name: &[u8], data: Vec<u8>) -> Self {
Self {
name: name.to_vec(),
data,
}
}
}
pub fn pack_contexts(contexts: &[CreateContext]) -> Vec<u8> {
let mut out: Vec<Vec<u8>> = Vec::with_capacity(contexts.len());
for ctx in contexts {
let mut cursor = WriteCursor::new();
let name_offset: u16 = 16;
let name_end = usize::from(name_offset) + ctx.name.len();
let data_offset = if ctx.data.is_empty() {
0u16
} else {
align8(name_end) as u16
};
cursor.write_u32_le(0); cursor.write_u16_le(name_offset);
cursor.write_u16_le(ctx.name.len() as u16);
cursor.write_u16_le(0); cursor.write_u16_le(data_offset);
cursor.write_u32_le(ctx.data.len() as u32);
cursor.write_bytes(&ctx.name);
let mut bytes = cursor.into_inner();
if !ctx.data.is_empty() {
bytes.resize(usize::from(data_offset), 0);
bytes.extend_from_slice(&ctx.data);
}
out.push(bytes);
}
let mut chain = Vec::new();
for (i, mut bytes) in out.into_iter().enumerate() {
bytes.resize(align8(bytes.len()), 0);
if i + 1 != contexts.len() {
let next = bytes.len() as u32;
bytes[0..4].copy_from_slice(&next.to_le_bytes());
}
chain.extend_from_slice(&bytes);
}
chain
}
pub fn parse_contexts(mut buf: &[u8]) -> Result<Vec<CreateContext>> {
let mut out = Vec::new();
loop {
if buf.len() < 16 {
if buf.is_empty() {
return Ok(out);
}
return Err(Error::invalid_data(
"create context chain ends inside an entry header",
));
}
let mut cursor = ReadCursor::new(buf);
let next = cursor.read_u32_le()? as usize;
let name_offset = cursor.read_u16_le()? as usize;
let name_length = cursor.read_u16_le()? as usize;
let _reserved = cursor.read_u16_le()?;
let data_offset = cursor.read_u16_le()? as usize;
let data_length = cursor.read_u32_le()? as usize;
let entry_len = if next == 0 { buf.len() } else { next };
if next != 0 && next > buf.len() {
return Err(Error::invalid_data(
"create context Next points past the end of the chain",
));
}
let name_end = name_offset
.checked_add(name_length)
.ok_or_else(|| Error::invalid_data("create context name overflows"))?;
if name_end > entry_len {
return Err(Error::invalid_data(
"create context name runs past the entry",
));
}
let data = if data_length == 0 {
Vec::new()
} else {
let data_end = data_offset
.checked_add(data_length)
.ok_or_else(|| Error::invalid_data("create context data overflows"))?;
if data_end > entry_len {
return Err(Error::invalid_data(
"create context data runs past the entry",
));
}
buf[data_offset..data_end].to_vec()
};
out.push(CreateContext {
name: buf[name_offset..name_end].to_vec(),
data,
});
if next == 0 {
return Ok(out);
}
buf = &buf[next..];
}
}
pub fn find<'a>(contexts: &'a [CreateContext], name: &[u8]) -> Option<&'a CreateContext> {
contexts.iter().find(|c| c.name == name)
}
fn align8(n: usize) -> usize {
n.div_ceil(8) * 8
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DurableRequestV2 {
pub timeout_ms: u32,
pub persistent: bool,
pub create_guid: Guid,
}
impl DurableRequestV2 {
pub fn to_bytes(self) -> Vec<u8> {
let mut c = WriteCursor::new();
c.write_u32_le(self.timeout_ms);
c.write_u32_le(if self.persistent { FLAG_PERSISTENT } else { 0 });
c.write_u64_le(0); self.create_guid.pack(&mut c);
c.into_inner()
}
pub fn context(self) -> CreateContext {
CreateContext::new(NAME_DH2Q, self.to_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DurableGrant {
pub timeout_ms: u32,
pub persistent: bool,
}
impl DurableGrant {
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let mut c = ReadCursor::new(data);
let timeout_ms = c.read_u32_le()?;
let flags = c.read_u32_le()?;
Ok(Self {
timeout_ms,
persistent: flags & FLAG_PERSISTENT != 0,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DurableReconnectV2 {
pub file_id: FileId,
pub create_guid: Guid,
pub persistent: bool,
}
impl DurableReconnectV2 {
pub fn to_bytes(self) -> Vec<u8> {
let mut c = WriteCursor::new();
c.write_u64_le(self.file_id.persistent);
c.write_u64_le(self.file_id.volatile);
self.create_guid.pack(&mut c);
c.write_u32_le(if self.persistent { FLAG_PERSISTENT } else { 0 });
c.into_inner()
}
pub fn context(self) -> CreateContext {
CreateContext::new(NAME_DH2C, self.to_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OnDiskId {
pub disk_file_id: u64,
pub volume_id: u64,
}
impl OnDiskId {
pub fn request() -> CreateContext {
CreateContext::new(NAME_QFID, Vec::new())
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let mut c = ReadCursor::new(data);
Ok(Self {
disk_file_id: c.read_u64_le()?,
volume_id: c.read_u64_le()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn guid(s: &str) -> Guid {
let hex: Vec<u8> = s
.chars()
.filter(|c| *c != '-')
.collect::<Vec<_>>()
.chunks(2)
.map(|p| u8::from_str_radix(&p.iter().collect::<String>(), 16).unwrap())
.collect();
Guid {
data1: u32::from_be_bytes([hex[0], hex[1], hex[2], hex[3]]),
data2: u16::from_be_bytes([hex[4], hex[5]]),
data3: u16::from_be_bytes([hex[6], hex[7]]),
data4: [
hex[8], hex[9], hex[10], hex[11], hex[12], hex[13], hex[14], hex[15],
],
}
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
})
}
#[test]
fn a_durable_request_matches_the_reference_wire_bytes() {
let req = DurableRequestV2 {
timeout_ms: 0,
persistent: false,
create_guid: guid("5a08e844-45c3-234d-87c6-596d2bc8bca5"),
};
assert_eq!(
hex(&req.to_bytes()),
"0000000000000000000000000000000044e8085ac3454d2387c6596d2bc8bca5"
);
assert_eq!(req.to_bytes().len(), 32);
}
#[test]
fn a_durable_reconnect_matches_the_reference_wire_bytes() {
let req = DurableReconnectV2 {
file_id: FileId {
persistent: 0x0000_0008_0000_00b3,
volatile: 0x0000_0008_0000_00dd,
},
create_guid: guid("a23e428c-1bac-7e43-8451-91f9f2277a95"),
persistent: false,
};
assert_eq!(
hex(&req.to_bytes()),
"b300000008000000dd000000080000008c423ea2ac1b437e845191f9f2277a9500000000"
);
assert_eq!(req.to_bytes().len(), 36);
}
#[test]
fn a_durable_grant_reads_its_timeout_and_persistence() {
let grant = DurableGrant::from_bytes(&[0x20, 0xbf, 0x02, 0x00, 0, 0, 0, 0]).unwrap();
assert_eq!(grant.timeout_ms, 180_000);
assert!(!grant.persistent);
let persistent =
DurableGrant::from_bytes(&[0, 0, 0, 0, FLAG_PERSISTENT as u8, 0, 0, 0]).unwrap();
assert!(persistent.persistent);
}
#[test]
fn an_on_disk_id_reads_the_reference_wire_bytes() {
let mut body = Vec::new();
body.extend_from_slice(&0x2ae7_0100_0000_0400u64.to_le_bytes());
body.extend_from_slice(&0xd9cf_17b0_0000_0000u64.to_le_bytes());
body.extend_from_slice(&[0u8; 16]);
let id = OnDiskId::from_bytes(&body).unwrap();
assert_eq!(id.disk_file_id, 0x2ae7_0100_0000_0400);
assert_eq!(id.volume_id, 0xd9cf_17b0_0000_0000);
}
#[test]
fn a_context_chain_round_trips() {
let chain = vec![
DurableRequestV2 {
timeout_ms: 0,
persistent: false,
create_guid: guid("5a08e844-45c3-234d-87c6-596d2bc8bca5"),
}
.context(),
OnDiskId::request(),
];
let bytes = pack_contexts(&chain);
assert_eq!(bytes.len() % 8, 0, "the chain stays 8-byte aligned");
let parsed = parse_contexts(&bytes).unwrap();
assert_eq!(parsed, chain);
assert!(find(&parsed, NAME_DH2Q).is_some());
assert!(find(&parsed, NAME_QFID).is_some());
assert!(find(&parsed, NAME_DH2C).is_none());
}
#[test]
fn a_context_with_no_data_reports_a_zero_data_offset() {
let bytes = pack_contexts(&[OnDiskId::request()]);
assert_eq!(u16::from_le_bytes([bytes[10], bytes[11]]), 0);
assert_eq!(
u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
0
);
assert_eq!(parse_contexts(&bytes).unwrap(), vec![OnDiskId::request()]);
}
#[test]
fn an_empty_chain_parses_to_nothing() {
assert_eq!(parse_contexts(&[]).unwrap(), vec![]);
}
#[test]
fn a_chain_that_points_outside_itself_is_rejected() {
let mut bytes = pack_contexts(&[OnDiskId::request()]);
bytes[0..4].copy_from_slice(&9999u32.to_le_bytes()); assert!(parse_contexts(&bytes).is_err());
let mut bytes = pack_contexts(&[OnDiskId::request()]);
bytes[6..8].copy_from_slice(&9999u16.to_le_bytes()); assert!(parse_contexts(&bytes).is_err());
let mut bytes = pack_contexts(&[DurableRequestV2 {
timeout_ms: 0,
persistent: false,
create_guid: Guid::ZERO,
}
.context()]);
bytes[12..16].copy_from_slice(&9999u32.to_le_bytes()); assert!(parse_contexts(&bytes).is_err());
assert!(parse_contexts(&[0u8; 8]).is_err(), "truncated entry header");
}
}