urna_format/sections/blob/
data.rs1use crate::bytes::{le_u32, le_u64};
16use crate::error::UrnaError;
17use crate::layout::SECTION_BLOB_DATA;
18
19pub const BLOB_DATA_PAYLOAD_VERSION: u32 = 1;
20
21const HEADER_SIZE: usize = 12;
23const ENTRY_SIZE: usize = 16;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct BlobDataTable {
31 pub entries: Vec<(u64, u64)>,
32 pub data_start: usize,
33}
34
35fn malformed(reason: impl Into<String>) -> UrnaError {
36 UrnaError::MalformedSectionPayload {
37 section_id: SECTION_BLOB_DATA,
38 reason: reason.into(),
39 }
40}
41
42pub fn encode_blob_data(blobs: &[Option<&[u8]>]) -> Result<Vec<u8>, UrnaError> {
46 let data_len: usize = blobs.iter().flatten().map(|b| b.len()).sum();
47 let mut out = Vec::with_capacity(HEADER_SIZE + blobs.len() * ENTRY_SIZE + data_len);
48 out.extend_from_slice(&BLOB_DATA_PAYLOAD_VERSION.to_le_bytes());
49 out.extend_from_slice(&(blobs.len() as u64).to_le_bytes());
50 let mut offset = 0u64;
51 for b in blobs {
52 match b {
53 Some(bytes) => {
54 out.extend_from_slice(&offset.to_le_bytes());
55 out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
56 offset += bytes.len() as u64;
57 }
58 None => out.extend_from_slice(&[0u8; ENTRY_SIZE]),
59 }
60 }
61 for b in blobs.iter().flatten() {
62 out.extend_from_slice(b);
63 }
64 Ok(out)
65}
66
67pub fn decode_blob_data_table(bytes: &[u8]) -> Result<BlobDataTable, UrnaError> {
71 if bytes.len() < HEADER_SIZE {
72 return Err(malformed("blob_data: payload shorter than header"));
73 }
74 let version = le_u32(&bytes[0..4])?;
75 if version != BLOB_DATA_PAYLOAD_VERSION {
76 return Err(UrnaError::UnsupportedSectionVersion {
77 section_id: SECTION_BLOB_DATA,
78 version,
79 });
80 }
81 let n = le_u64(&bytes[4..12])? as usize;
82 if n > (bytes.len() - HEADER_SIZE) / ENTRY_SIZE {
84 return Err(malformed("blob_data: entry count exceeds payload"));
85 }
86 let data_start = HEADER_SIZE + n * ENTRY_SIZE;
87 let data_len = (bytes.len() - data_start) as u64;
88 let mut entries = Vec::with_capacity(n);
89 for i in 0..n {
90 let at = HEADER_SIZE + i * ENTRY_SIZE;
91 let offset = le_u64(&bytes[at..at + 8])?;
92 let len = le_u64(&bytes[at + 8..at + 16])?;
93 let end = offset
94 .checked_add(len)
95 .ok_or_else(|| malformed(format!("blob_data: entry {} offset overflow", i)))?;
96 if end > data_len {
97 return Err(malformed(format!(
98 "blob_data: entry {} spans past payload ({} > {})",
99 i, end, data_len
100 )));
101 }
102 entries.push((offset, len));
103 }
104 Ok(BlobDataTable {
105 entries,
106 data_start,
107 })
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn roundtrip_with_gaps() {
116 let a = b"av1-shard-bytes".as_slice();
117 let b = b"x".as_slice();
118 let payload = encode_blob_data(&[Some(a), None, Some(b)]).unwrap();
119 let table = decode_blob_data_table(&payload).unwrap();
120 assert_eq!(table.entries, vec![(0, 15), (0, 0), (15, 1)]);
121 let data = &payload[table.data_start..];
122 assert_eq!(&data[0..15], a);
123 assert_eq!(&data[15..16], b);
124 }
125
126 #[test]
127 fn empty_table_roundtrips() {
128 let payload = encode_blob_data(&[]).unwrap();
129 let table = decode_blob_data_table(&payload).unwrap();
130 assert!(table.entries.is_empty());
131 assert_eq!(table.data_start, payload.len());
132 }
133
134 #[test]
135 fn hostile_count_is_rejected() {
136 let mut payload = encode_blob_data(&[Some(b"abc".as_slice())]).unwrap();
137 payload[4..12].copy_from_slice(&u64::MAX.to_le_bytes());
138 assert!(decode_blob_data_table(&payload).is_err());
139 }
140
141 #[test]
142 fn entry_past_payload_is_rejected() {
143 let mut payload = encode_blob_data(&[Some(b"abc".as_slice())]).unwrap();
144 payload[20..28].copy_from_slice(&1000u64.to_le_bytes());
146 assert!(decode_blob_data_table(&payload).is_err());
147 }
148
149 #[test]
150 fn bad_version_is_typed() {
151 let mut payload = encode_blob_data(&[]).unwrap();
152 payload[0..4].copy_from_slice(&9u32.to_le_bytes());
153 assert!(matches!(
154 decode_blob_data_table(&payload),
155 Err(UrnaError::UnsupportedSectionVersion { .. })
156 ));
157 }
158}