use crate::error::{try_vec, ParseError};
pub const HEADER_LEN: usize = 9;
pub const CONTAINER: &[u8; 3] = b"NWS";
pub const HDR: &[u8; 3] = b"hdr";
pub const CAT: &[u8; 3] = b"cat";
pub const MAP: &[u8; 3] = b"map";
pub const STK: &[u8; 3] = b"stk";
pub const STY: &[u8; 3] = b"sty";
#[derive(Clone, PartialEq, Eq)]
pub struct Section {
pub tag: [u8; 3],
pub version: u8,
pub payload: Vec<u8>,
}
impl Section {
pub fn encoded_len(&self) -> usize {
HEADER_LEN + self.payload.len()
}
pub fn tag_str(&self) -> String {
String::from_utf8_lossy(&self.tag).into_owned()
}
pub fn is(&self, tag: &[u8; 3]) -> bool {
&self.tag == tag
}
pub fn write_to(&self, w: &mut impl std::io::Write) -> Result<(), ParseError> {
let len = wire_len(self.payload.len())?;
let head = |w: &mut dyn std::io::Write| -> std::io::Result<()> {
w.write_all(&self.tag)?;
w.write_all(&[0, self.version])?;
w.write_all(&len.to_be_bytes())?;
w.write_all(&self.payload)
};
head(w).map_err(|e| ParseError::AssertFail(format!("writing a section: {e}")))
}
}
fn wrong_opener(expected: &[u8], found: &[u8]) -> ParseError {
ParseError::AssertFail(format!(
"the body does not open with the {} container section; found {}",
expected.escape_ascii(),
found.escape_ascii(),
))
}
fn unpadded_tag(tag: &[u8], at: u64, found: u8) -> ParseError {
ParseError::AssertFail(format!(
"section {} at {at} holds {found} where its tag's padding NUL belongs",
String::from_utf8_lossy(tag),
))
}
fn missing_opener(expected: &[u8]) -> ParseError {
ParseError::AssertFail(format!(
"the body does not open with the {} container section; found end of body",
expected.escape_ascii(),
))
}
pub fn read_chain(r: &mut impl std::io::Read, remaining: u64) -> Result<Vec<Section>, ParseError> {
let mut sections = Vec::new();
let mut pos: u64 = 0;
loop {
let mut head = [0u8; HEADER_LEN];
if !read_exact_or_end(r, &mut head, pos)? {
return if pos == 0 {
Err(missing_opener(CONTAINER))
} else {
Ok(sections)
};
}
if pos == 0 && &head[..3] != CONTAINER {
return Err(wrong_opener(CONTAINER, &head[..3]));
}
if head[3] != 0 {
return Err(unpadded_tag(&head[..3], pos, head[3]));
}
let len = u32::from_be_bytes([head[5], head[6], head[7], head[8]]) as usize;
let end = section_end(pos, HEADER_LEN, len, remaining, &head[..3])?;
let payload = read_payload(r, len, pos, &head[..3])?;
pos = end;
sections.push(Section {
tag: [head[0], head[1], head[2]],
version: head[4],
payload,
});
}
}
pub const HEADER4_LEN: usize = 12;
pub const CONTAINER4: &[u8; 4] = b"NSMP";
pub const HDR4: &[u8; 4] = b"\0hdr";
pub const CAT4: &[u8; 4] = b"\0cat";
pub const MAP4: &[u8; 4] = b"\0map";
pub const STK4: &[u8; 4] = b"\0stk";
pub const STY4: &[u8; 4] = b"\0sty";
pub const META4: &[u8; 4] = b"meta";
#[derive(Clone, PartialEq, Eq)]
pub struct Section4 {
pub tag: [u8; 4],
pub version: u32,
pub payload: Vec<u8>,
}
impl Section4 {
pub fn encoded_len(&self) -> usize {
HEADER4_LEN + self.payload.len()
}
pub fn tag_str(&self) -> String {
String::from_utf8_lossy(&self.tag)
.trim_start_matches('\0')
.to_owned()
}
pub fn is(&self, tag: &[u8; 4]) -> bool {
&self.tag == tag
}
pub fn write_to(&self, w: &mut impl std::io::Write) -> Result<(), ParseError> {
let len = wire_len(self.payload.len())?;
let head = |w: &mut dyn std::io::Write| -> std::io::Result<()> {
w.write_all(&self.tag)?;
w.write_all(&self.version.to_be_bytes())?;
w.write_all(&len.to_be_bytes())?;
w.write_all(&self.payload)
};
head(w).map_err(|e| ParseError::AssertFail(format!("writing a section: {e}")))
}
}
pub fn read_chain4(
r: &mut impl std::io::Read,
remaining: u64,
) -> Result<Vec<Section4>, ParseError> {
let mut sections = Vec::new();
let mut pos: u64 = 0;
loop {
let mut head = [0u8; HEADER4_LEN];
if !read_exact_or_end(r, &mut head, pos)? {
return if pos == 0 {
Err(missing_opener(CONTAINER4))
} else {
Ok(sections)
};
}
if pos == 0 && &head[..4] != CONTAINER4 {
return Err(wrong_opener(CONTAINER4, &head[..4]));
}
let len = u32::from_be_bytes([head[8], head[9], head[10], head[11]]) as usize;
let end = section_end(pos, HEADER4_LEN, len, remaining, &head[..4])?;
let payload = read_payload(r, len, pos, &head[..4])?;
pos = end;
sections.push(Section4 {
tag: [head[0], head[1], head[2], head[3]],
version: u32::from_be_bytes([head[4], head[5], head[6], head[7]]),
payload,
});
}
}
fn read_exact_or_end(
r: &mut impl std::io::Read,
buf: &mut [u8],
at: u64,
) -> Result<bool, ParseError> {
let mut got = 0;
while got < buf.len() {
match r.read(&mut buf[got..]) {
Ok(0) if got == 0 => return Ok(false),
Ok(0) => {
return Err(ParseError::AssertFail(format!(
"truncated section header at {at}"
)))
}
Ok(n) => got += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(ParseError::AssertFail(format!("reading a section: {e}"))),
}
}
Ok(true)
}
fn section_end(
at: u64,
header: usize,
len: usize,
remaining: u64,
tag: &[u8],
) -> Result<u64, ParseError> {
match at
.checked_add(header as u64)
.and_then(|end| end.checked_add(len as u64))
{
Some(end) if end <= remaining => Ok(end),
_ => Err(body_ends_first(tag, at, len)),
}
}
fn body_ends_first(tag: &[u8], at: u64, len: usize) -> ParseError {
ParseError::AssertFail(format!(
"section {} at {at} declares {len} bytes but the body ends first",
String::from_utf8_lossy(tag),
))
}
fn read_payload(
r: &mut impl std::io::Read,
len: usize,
at: u64,
tag: &[u8],
) -> Result<Vec<u8>, ParseError> {
let mut payload = try_vec(len)?;
r.read_exact(&mut payload)
.map_err(|_| body_ends_first(tag, at, len))?;
Ok(payload)
}
fn wire_len(len: usize) -> Result<u32, ParseError> {
u32::try_from(len).map_err(|_| ParseError::OutOfBounds {
value: format!("{len} payload bytes"),
bound: "a payload length that fits u32".into(),
})
}
pub fn find4<'a>(sections: &'a [Section4], tag: &[u8; 4]) -> Option<&'a Section4> {
sections.iter().find(|s| s.is(tag))
}
pub fn find_mut4<'a>(sections: &'a mut [Section4], tag: &[u8; 4]) -> Option<&'a mut Section4> {
sections.iter_mut().find(|s| s.is(tag))
}
impl std::fmt::Debug for Section4 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Section4")
.field("tag", &self.tag_str())
.field("version", &self.version)
.field("len", &self.payload.len())
.finish()
}
}
pub fn find<'a>(sections: &'a [Section], tag: &[u8; 3]) -> Option<&'a Section> {
sections.iter().find(|s| s.is(tag))
}
pub fn find_mut<'a>(sections: &'a mut [Section], tag: &[u8; 3]) -> Option<&'a mut Section> {
sections.iter_mut().find(|s| s.is(tag))
}
impl std::fmt::Debug for Section {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Section")
.field("tag", &self.tag_str())
.field("version", &self.version)
.field("len", &self.payload.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn walk(body: &[u8]) -> Result<Vec<Section>, ParseError> {
read_chain(&mut { body }, body.len() as u64)
}
fn walk4(body: &[u8]) -> Result<Vec<Section4>, ParseError> {
read_chain4(&mut { body }, body.len() as u64)
}
fn section(tag: &[u8; 3], version: u8, payload: &[u8]) -> Vec<u8> {
let mut v = tag.to_vec();
v.push(0);
v.push(version);
v.extend_from_slice(&(payload.len() as u32).to_be_bytes());
v.extend_from_slice(payload);
v
}
#[test]
fn chain_round_trips() {
let mut bytes = section(CONTAINER, 11, &[]);
bytes.extend(section(HDR, 9, &[1, 2, 3]));
bytes.extend(section(STK, 9, &[4; 20]));
let chain = walk(&bytes).unwrap();
assert_eq!(chain.len(), 3);
assert_eq!(chain[0].payload.len(), 0);
assert_eq!(chain[1].payload, vec![1, 2, 3]);
assert_eq!(chain[2].version, 9);
let mut out = Vec::new();
for s in &chain {
s.write_to(&mut out).unwrap();
}
assert_eq!(out, bytes);
}
fn opener() -> Vec<u8> {
section(CONTAINER, 11, &[])
}
#[test]
fn length_is_big_endian() {
let mut bytes = opener();
bytes.extend(section(HDR, 1, &[0; 258]));
let chain = walk(&bytes).unwrap();
assert_eq!(chain[1].payload.len(), 258);
}
#[test]
fn a_length_past_the_end_of_the_body_is_refused_without_allocating_it() {
let mut one_over = section(HDR, 1, &[7; 4]);
one_over[5..9].copy_from_slice(&5u32.to_be_bytes());
let mut bytes = opener();
bytes.extend(one_over);
assert_eq!(
walk(&bytes).unwrap_err().to_string(),
"section hdr at 9 declares 5 bytes but the body ends first"
);
let mut huge = section(HDR, 1, &[7; 4]);
huge[5..9].copy_from_slice(&u32::MAX.to_be_bytes());
let mut bytes = opener();
bytes.extend(huge);
assert_eq!(
walk(&bytes).unwrap_err().to_string(),
format!(
"section hdr at 9 declares {} bytes but the body ends first",
u32::MAX
)
);
}
#[test]
fn trailing_bytes_are_an_error() {
let mut bytes = opener();
bytes.extend(section(HDR, 1, &[7; 4]));
bytes.extend_from_slice(&[0, 0, 0]); assert!(walk(&bytes).is_err());
}
#[test]
fn a_chain_not_opening_with_its_container_names_the_expected_tag() {
let bytes = section(HDR, 1, &[7; 4]);
assert_eq!(
walk(&bytes).unwrap_err().to_string(),
"the body does not open with the NWS container section; found hdr"
);
let bytes = section4(HDR4, 1, &[7; 4]);
assert_eq!(
walk4(&bytes).unwrap_err().to_string(),
"the body does not open with the NSMP container section; found \\x00hdr"
);
}
#[test]
fn an_empty_body_reports_its_missing_container() {
assert_eq!(
walk(&[]).unwrap_err().to_string(),
"the body does not open with the NWS container section; found end of body"
);
assert_eq!(
walk4(&[]).unwrap_err().to_string(),
"the body does not open with the NSMP container section; found end of body"
);
}
#[test]
fn a_tag_not_padded_with_a_nul_is_refused() {
let mut hdr = section(HDR, 1, &[7; 4]);
hdr[3] = 2;
let mut bytes = opener();
bytes.extend(hdr);
assert_eq!(
walk(&bytes).unwrap_err().to_string(),
"section hdr at 9 holds 2 where its tag's padding NUL belongs"
);
}
#[test]
fn a_corrupt_opener_is_reported_before_its_length() {
let bytes = [0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, 0xff, 0xff, 0x3e];
assert_eq!(
walk(&bytes).unwrap_err().to_string(),
"the body does not open with the NWS container section; found \\x00\\x00\\x00"
);
}
fn section4(tag: &[u8; 4], version: u32, payload: &[u8]) -> Vec<u8> {
let mut v = tag.to_vec();
v.extend_from_slice(&version.to_be_bytes());
v.extend_from_slice(&(payload.len() as u32).to_be_bytes());
v.extend_from_slice(payload);
v
}
#[test]
fn chain4_round_trips() {
let mut bytes = section4(CONTAINER4, 30, &[0, 2, 0, 0x0c]);
bytes.extend(section4(HDR4, 10, &[1; 112]));
bytes.extend(section4(STK4, 11, &[4; 20]));
let chain = walk4(&bytes).unwrap();
assert_eq!(chain.len(), 3);
assert_eq!(chain[0].payload.len(), 4);
assert_eq!(chain[1].version, 10);
assert_eq!(chain[2].tag_str(), "stk");
let mut out = Vec::new();
for s in &chain {
s.write_to(&mut out).unwrap();
}
assert_eq!(out, bytes);
}
#[test]
fn chain4_overrun_and_truncation_are_errors() {
let opener4 = || section4(CONTAINER4, 30, &[0, 2, 0, 0x0c]);
let mut hdr = section4(HDR4, 1, &[7; 4]);
hdr[8..12].copy_from_slice(&u32::MAX.to_be_bytes());
let mut bytes = opener4();
bytes.extend(hdr);
assert!(walk4(&bytes).is_err());
let mut bytes = opener4();
bytes.extend(section4(HDR4, 1, &[7; 4]));
bytes.extend_from_slice(&[0; 5]); assert!(walk4(&bytes).is_err());
}
#[cfg(target_pointer_width = "64")]
#[test]
fn a_section_writer_refuses_lengths_that_do_not_fit_the_wire() {
assert_eq!(wire_len(u32::MAX as usize).unwrap(), u32::MAX);
assert!(wire_len(u32::MAX as usize + 1).is_err());
}
#[test]
fn repeated_tags_are_all_kept() {
let mut bytes = opener();
bytes.extend(section(STK, 9, &[1]));
bytes.extend(section(STK, 9, &[2]));
bytes.extend(section(STK, 9, &[3]));
let chain = walk(&bytes).unwrap();
assert_eq!(chain.len(), 4);
assert_eq!(
chain[1..].iter().map(|s| s.payload[0]).collect::<Vec<_>>(),
vec![1, 2, 3]
);
}
}