urna_format/sections/
spans.rs1use std::collections::HashMap;
13
14use super::REPACK_KIND_SPANS;
15use super::codec::{Cursor, read_prefix, write_lp_str, write_prefix};
16use crate::encoding::{pack_u64s, unpack_u64s};
17use crate::error::UrnaError;
18use crate::layout::SECTION_CHUNKS_ORIGINAL_SPANS;
19
20#[derive(Clone, Debug, PartialEq)]
21pub struct OriginalSpan {
22 pub source_uri: String,
23 pub byte_start: u64,
24 pub byte_end: u64,
25}
26
27pub fn encode_chunks_original_spans(spans: &[OriginalSpan]) -> crate::Result<Vec<u8>> {
28 let mut buf = Vec::new();
29 write_prefix(&mut buf, spans.len() as u64);
30 for s in spans {
31 write_lp_str(&mut buf, &s.source_uri)?;
32 buf.extend_from_slice(&s.byte_start.to_le_bytes());
33 buf.extend_from_slice(&s.byte_end.to_le_bytes());
34 }
35 Ok(buf)
36}
37
38pub fn decode_chunks_original_spans(
39 data: &[u8],
40 expected_count: usize,
41) -> crate::Result<Vec<OriginalSpan>> {
42 let mut c = Cursor::new(data, SECTION_CHUNKS_ORIGINAL_SPANS);
43 let count = read_prefix(&mut c)? as usize;
44 if count != expected_count {
45 return Err(UrnaError::SectionCountMismatch {
46 section_id: SECTION_CHUNKS_ORIGINAL_SPANS,
47 expected: expected_count,
48 got: count,
49 });
50 }
51 let mut out = Vec::with_capacity(count);
52 for _ in 0..count {
53 let source_uri = c.read_lp_str()?;
54 let byte_start = c.read_u64()?;
55 let byte_end = c.read_u64()?;
56 out.push(OriginalSpan {
57 source_uri,
58 byte_start,
59 byte_end,
60 });
61 }
62 c.finish()?;
63 Ok(out)
64}
65
66fn read_blob<'a>(c: &mut Cursor<'a>) -> crate::Result<&'a [u8]> {
67 let len = c.read_u32()? as usize;
68 c.read_bytes(len)
69}
70
71pub fn encode_chunks_original_spans_intpack(spans: &[OriginalSpan]) -> Vec<u8> {
76 let mut pool: Vec<&str> = Vec::new();
77 let mut pos_of: HashMap<&str, usize> = HashMap::new();
78 let mut idx: Vec<u64> = Vec::with_capacity(spans.len());
79 let mut starts: Vec<u64> = Vec::with_capacity(spans.len());
80 let mut lens: Vec<u64> = Vec::with_capacity(spans.len());
81 for s in spans {
82 let uri = s.source_uri.as_str();
83 let pos = *pos_of.entry(uri).or_insert_with(|| {
88 let p = pool.len();
89 pool.push(uri);
90 p
91 });
92 idx.push(pos as u64);
93 starts.push(s.byte_start);
94 lens.push(s.byte_end.wrapping_sub(s.byte_start));
95 }
96 let mut out = Vec::new();
97 out.push(REPACK_KIND_SPANS);
98 out.extend_from_slice(&(spans.len() as u32).to_le_bytes());
99 out.extend_from_slice(&(pool.len() as u32).to_le_bytes());
100 for uri in &pool {
101 out.extend_from_slice(&(uri.len() as u32).to_le_bytes());
102 out.extend_from_slice(uri.as_bytes());
103 }
104 for col in [&idx, &starts, &lens] {
105 let blob = pack_u64s(col);
106 out.extend_from_slice(&(blob.len() as u32).to_le_bytes());
107 out.extend_from_slice(&blob);
108 }
109 out
110}
111
112pub fn decode_chunks_original_spans_intpack(rest: &[u8]) -> crate::Result<Vec<u8>> {
116 let mut c = Cursor::new(rest, SECTION_CHUNKS_ORIGINAL_SPANS);
117 let count = c.read_u32()? as usize;
118 let n_uris = c.read_u32()? as usize;
119 if n_uris > (rest.len() - c.pos) / 4 {
124 return Err(c.malformed("spans intpack: uri pool count exceeds payload"));
125 }
126 let mut pool: Vec<String> = Vec::with_capacity(n_uris);
127 for _ in 0..n_uris {
128 pool.push(c.read_lp_str()?);
129 }
130 let idx = unpack_u64s(read_blob(&mut c)?)?;
131 let starts = unpack_u64s(read_blob(&mut c)?)?;
132 let lens = unpack_u64s(read_blob(&mut c)?)?;
133 if idx.len() != count || starts.len() != count || lens.len() != count {
134 return Err(c.malformed("spans intpack: column length mismatch"));
135 }
136 c.finish()?;
137 let mut buf = Vec::with_capacity(12 + count * 24);
138 write_prefix(&mut buf, count as u64);
139 for i in 0..count {
140 let uri = pool
141 .get(idx[i] as usize)
142 .ok_or_else(|| UrnaError::MalformedSectionPayload {
143 section_id: SECTION_CHUNKS_ORIGINAL_SPANS,
144 reason: "spans intpack: uri index out of range".into(),
145 })?;
146 write_lp_str(&mut buf, uri)?;
147 buf.extend_from_slice(&starts[i].to_le_bytes());
148 buf.extend_from_slice(&starts[i].wrapping_add(lens[i]).to_le_bytes());
149 }
150 Ok(buf)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn roundtrip() {
159 let spans = vec![
160 OriginalSpan {
161 source_uri: "doc.txt".into(),
162 byte_start: 0,
163 byte_end: 10,
164 },
165 OriginalSpan {
166 source_uri: "doc.txt".into(),
167 byte_start: 10,
168 byte_end: 25,
169 },
170 ];
171 let bytes = encode_chunks_original_spans(&spans).unwrap();
172 let back = decode_chunks_original_spans(&bytes, 2).unwrap();
173 assert_eq!(spans, back);
174 }
175
176 fn corpus(n: u64) -> Vec<OriginalSpan> {
177 (0..n)
178 .map(|i| OriginalSpan {
179 source_uri: "corpus-next/v1.txt".into(), byte_start: i * 500,
181 byte_end: i * 500 + 480,
182 })
183 .collect()
184 }
185
186 #[test]
187 fn intpack_decodes_byte_identical_to_raw() {
188 let spans = corpus(200);
189 let raw = encode_chunks_original_spans(&spans).unwrap();
190 let packed = encode_chunks_original_spans_intpack(&spans);
191 assert_eq!(packed[0], REPACK_KIND_SPANS);
192 assert!(packed.len() < raw.len(), "dedup+intpack must shrink spans");
193 let reconstructed = decode_chunks_original_spans_intpack(&packed[1..]).unwrap();
194 assert_eq!(reconstructed, raw, "spans repack must rebuild raw bytes");
195 assert_eq!(
196 decode_chunks_original_spans(&reconstructed, spans.len()).unwrap(),
197 spans
198 );
199 }
200
201 #[test]
202 fn intpack_handles_multiple_uris_and_empty() {
203 let spans = vec![
204 OriginalSpan {
205 source_uri: "a.txt".into(),
206 byte_start: 5,
207 byte_end: 9,
208 },
209 OriginalSpan {
210 source_uri: "b.txt".into(),
211 byte_start: 0,
212 byte_end: 100,
213 },
214 OriginalSpan {
215 source_uri: "a.txt".into(),
216 byte_start: 9,
217 byte_end: 12,
218 },
219 ];
220 let raw = encode_chunks_original_spans(&spans).unwrap();
221 let packed = encode_chunks_original_spans_intpack(&spans);
222 assert_eq!(
223 decode_chunks_original_spans_intpack(&packed[1..]).unwrap(),
224 raw
225 );
226
227 let empty = encode_chunks_original_spans_intpack(&[]);
228 assert_eq!(
229 decode_chunks_original_spans_intpack(&empty[1..]).unwrap(),
230 encode_chunks_original_spans(&[]).unwrap()
231 );
232 }
233
234 #[test]
235 fn intpack_truncated_errors_never_panic() {
236 let packed = encode_chunks_original_spans_intpack(&corpus(10));
237 for cut in 1..packed.len() {
238 let _ = decode_chunks_original_spans_intpack(&packed[1..cut]);
239 }
240 }
241}