use std::collections::HashMap;
use super::REPACK_KIND_SPANS;
use super::codec::{Cursor, read_prefix, write_lp_str, write_prefix};
use crate::encoding::{pack_u64s, unpack_u64s};
use crate::error::UrnaError;
use crate::layout::SECTION_CHUNKS_ORIGINAL_SPANS;
#[derive(Clone, Debug, PartialEq)]
pub struct OriginalSpan {
pub source_uri: String,
pub byte_start: u64,
pub byte_end: u64,
}
pub fn encode_chunks_original_spans(spans: &[OriginalSpan]) -> crate::Result<Vec<u8>> {
let mut buf = Vec::new();
write_prefix(&mut buf, spans.len() as u64);
for s in spans {
write_lp_str(&mut buf, &s.source_uri)?;
buf.extend_from_slice(&s.byte_start.to_le_bytes());
buf.extend_from_slice(&s.byte_end.to_le_bytes());
}
Ok(buf)
}
pub fn decode_chunks_original_spans(
data: &[u8],
expected_count: usize,
) -> crate::Result<Vec<OriginalSpan>> {
let mut c = Cursor::new(data, SECTION_CHUNKS_ORIGINAL_SPANS);
let count = read_prefix(&mut c)? as usize;
if count != expected_count {
return Err(UrnaError::SectionCountMismatch {
section_id: SECTION_CHUNKS_ORIGINAL_SPANS,
expected: expected_count,
got: count,
});
}
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let source_uri = c.read_lp_str()?;
let byte_start = c.read_u64()?;
let byte_end = c.read_u64()?;
out.push(OriginalSpan {
source_uri,
byte_start,
byte_end,
});
}
c.finish()?;
Ok(out)
}
fn read_blob<'a>(c: &mut Cursor<'a>) -> crate::Result<&'a [u8]> {
let len = c.read_u32()? as usize;
c.read_bytes(len)
}
pub fn encode_chunks_original_spans_intpack(spans: &[OriginalSpan]) -> Vec<u8> {
let mut pool: Vec<&str> = Vec::new();
let mut pos_of: HashMap<&str, usize> = HashMap::new();
let mut idx: Vec<u64> = Vec::with_capacity(spans.len());
let mut starts: Vec<u64> = Vec::with_capacity(spans.len());
let mut lens: Vec<u64> = Vec::with_capacity(spans.len());
for s in spans {
let uri = s.source_uri.as_str();
let pos = *pos_of.entry(uri).or_insert_with(|| {
let p = pool.len();
pool.push(uri);
p
});
idx.push(pos as u64);
starts.push(s.byte_start);
lens.push(s.byte_end.wrapping_sub(s.byte_start));
}
let mut out = Vec::new();
out.push(REPACK_KIND_SPANS);
out.extend_from_slice(&(spans.len() as u32).to_le_bytes());
out.extend_from_slice(&(pool.len() as u32).to_le_bytes());
for uri in &pool {
out.extend_from_slice(&(uri.len() as u32).to_le_bytes());
out.extend_from_slice(uri.as_bytes());
}
for col in [&idx, &starts, &lens] {
let blob = pack_u64s(col);
out.extend_from_slice(&(blob.len() as u32).to_le_bytes());
out.extend_from_slice(&blob);
}
out
}
pub fn decode_chunks_original_spans_intpack(rest: &[u8]) -> crate::Result<Vec<u8>> {
let mut c = Cursor::new(rest, SECTION_CHUNKS_ORIGINAL_SPANS);
let count = c.read_u32()? as usize;
let n_uris = c.read_u32()? as usize;
if n_uris > (rest.len() - c.pos) / 4 {
return Err(c.malformed("spans intpack: uri pool count exceeds payload"));
}
let mut pool: Vec<String> = Vec::with_capacity(n_uris);
for _ in 0..n_uris {
pool.push(c.read_lp_str()?);
}
let idx = unpack_u64s(read_blob(&mut c)?)?;
let starts = unpack_u64s(read_blob(&mut c)?)?;
let lens = unpack_u64s(read_blob(&mut c)?)?;
if idx.len() != count || starts.len() != count || lens.len() != count {
return Err(c.malformed("spans intpack: column length mismatch"));
}
c.finish()?;
let mut buf = Vec::with_capacity(12 + count * 24);
write_prefix(&mut buf, count as u64);
for i in 0..count {
let uri = pool
.get(idx[i] as usize)
.ok_or_else(|| UrnaError::MalformedSectionPayload {
section_id: SECTION_CHUNKS_ORIGINAL_SPANS,
reason: "spans intpack: uri index out of range".into(),
})?;
write_lp_str(&mut buf, uri)?;
buf.extend_from_slice(&starts[i].to_le_bytes());
buf.extend_from_slice(&starts[i].wrapping_add(lens[i]).to_le_bytes());
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let spans = vec![
OriginalSpan {
source_uri: "doc.txt".into(),
byte_start: 0,
byte_end: 10,
},
OriginalSpan {
source_uri: "doc.txt".into(),
byte_start: 10,
byte_end: 25,
},
];
let bytes = encode_chunks_original_spans(&spans).unwrap();
let back = decode_chunks_original_spans(&bytes, 2).unwrap();
assert_eq!(spans, back);
}
fn corpus(n: u64) -> Vec<OriginalSpan> {
(0..n)
.map(|i| OriginalSpan {
source_uri: "corpus-next/v1.txt".into(), byte_start: i * 500,
byte_end: i * 500 + 480,
})
.collect()
}
#[test]
fn intpack_decodes_byte_identical_to_raw() {
let spans = corpus(200);
let raw = encode_chunks_original_spans(&spans).unwrap();
let packed = encode_chunks_original_spans_intpack(&spans);
assert_eq!(packed[0], REPACK_KIND_SPANS);
assert!(packed.len() < raw.len(), "dedup+intpack must shrink spans");
let reconstructed = decode_chunks_original_spans_intpack(&packed[1..]).unwrap();
assert_eq!(reconstructed, raw, "spans repack must rebuild raw bytes");
assert_eq!(
decode_chunks_original_spans(&reconstructed, spans.len()).unwrap(),
spans
);
}
#[test]
fn intpack_handles_multiple_uris_and_empty() {
let spans = vec![
OriginalSpan {
source_uri: "a.txt".into(),
byte_start: 5,
byte_end: 9,
},
OriginalSpan {
source_uri: "b.txt".into(),
byte_start: 0,
byte_end: 100,
},
OriginalSpan {
source_uri: "a.txt".into(),
byte_start: 9,
byte_end: 12,
},
];
let raw = encode_chunks_original_spans(&spans).unwrap();
let packed = encode_chunks_original_spans_intpack(&spans);
assert_eq!(
decode_chunks_original_spans_intpack(&packed[1..]).unwrap(),
raw
);
let empty = encode_chunks_original_spans_intpack(&[]);
assert_eq!(
decode_chunks_original_spans_intpack(&empty[1..]).unwrap(),
encode_chunks_original_spans(&[]).unwrap()
);
}
#[test]
fn intpack_truncated_errors_never_panic() {
let packed = encode_chunks_original_spans_intpack(&corpus(10));
for cut in 1..packed.len() {
let _ = decode_chunks_original_spans_intpack(&packed[1..cut]);
}
}
}