use super::intpack::{IntpackReader, pack_u64s};
use super::zstd_codec::{zstd_decode, zstd_encode};
use crate::bytes::le_u64;
use crate::error::UrnaError;
use crate::layout::{
SECTION_CHUNKS_CANONICAL, SECTION_PAYLOAD_PREFIX_SIZE, SECTION_PAYLOAD_VERSION,
};
pub const TXT_STREAMS_V1: u8 = 0;
pub(super) const HEADER: usize = 9;
pub(super) fn malformed(reason: impl Into<String>) -> UrnaError {
UrnaError::MalformedSectionPayload {
section_id: SECTION_CHUNKS_CANONICAL,
reason: reason.into(),
}
}
pub(super) fn write_container(kind: u8, count: usize, table: &[u8], streams: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER + table.len() + streams.len());
out.push(kind);
out.extend_from_slice(&(count as u64).to_le_bytes());
out.extend_from_slice(table);
out.extend_from_slice(streams);
out
}
pub(super) fn build_canonical(count: usize, bodies: &[Vec<u8>]) -> crate::Result<Vec<u8>> {
let total: usize = bodies.iter().map(|b| b.len()).sum();
let mut out = Vec::with_capacity(SECTION_PAYLOAD_PREFIX_SIZE + count * 4 + total);
out.extend_from_slice(&SECTION_PAYLOAD_VERSION.to_le_bytes());
out.extend_from_slice(&(count as u64).to_le_bytes());
for raw in bodies {
let len = u32::try_from(raw.len())
.map_err(|_| malformed("txt_streams: stream longer than u32"))?;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(raw);
}
Ok(out)
}
pub fn encode_txt_streams(texts: &[String]) -> crate::Result<Vec<u8>> {
let mut streams: Vec<u8> = Vec::new();
let mut offsets: Vec<u64> = Vec::with_capacity(texts.len() + 1);
offsets.push(0);
for t in texts {
streams.extend_from_slice(&zstd_encode(t.as_bytes())?);
offsets.push(streams.len() as u64);
}
let table = pack_u64s(&offsets);
Ok(write_container(
TXT_STREAMS_V1,
texts.len(),
&table,
&streams,
))
}
pub struct TxtStreams<'a> {
streams: &'a [u8],
offsets: Vec<u64>,
count: usize,
}
impl<'a> TxtStreams<'a> {
pub fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
let (kind, rest) = bytes
.split_first()
.ok_or_else(|| malformed("txt_streams: empty"))?;
if *kind != TXT_STREAMS_V1 {
return Err(malformed(format!("txt_streams: unknown kind {}", *kind)));
}
if rest.len() < 8 {
return Err(malformed("txt_streams: truncated count"));
}
let declared = le_u64(&rest[0..8])?;
let table_bytes = &rest[8..];
let reader = IntpackReader::parse(table_bytes)?;
if reader.is_empty() {
return Err(malformed("txt_streams: offset table must hold n+1 >= 1"));
}
let count = reader.len() - 1;
if declared != count as u64 {
return Err(malformed("txt_streams: declared count != offset count - 1"));
}
let mut offsets = Vec::with_capacity(reader.len());
for i in 0..reader.len() {
offsets.push(reader.get(i)?);
}
if offsets[0] != 0 {
return Err(malformed("txt_streams: first offset must be 0"));
}
let table_len = pack_u64s(&offsets).len();
if table_len > table_bytes.len() {
return Err(malformed("txt_streams: truncated offset table"));
}
let streams = &table_bytes[table_len..];
for w in offsets.windows(2) {
if w[1] < w[0] {
return Err(malformed("txt_streams: non-monotonic offsets"));
}
}
if offsets.last().copied() != Some(streams.len() as u64) {
return Err(malformed("txt_streams: final offset != streams length"));
}
Ok(Self {
streams,
offsets,
count,
})
}
#[inline]
pub fn len(&self) -> usize {
self.count
}
#[inline]
pub fn is_empty(&self) -> bool {
self.count == 0
}
fn stream(&self, i: usize) -> crate::Result<&'a [u8]> {
if i >= self.count {
return Err(malformed("txt_streams: index out of range"));
}
let start = self.offsets[i] as usize;
let end = self.offsets[i + 1] as usize;
self.streams
.get(start..end)
.ok_or_else(|| malformed("txt_streams: stream slice out of bounds"))
}
pub fn text(&self, i: usize) -> crate::Result<String> {
let raw = zstd_decode(self.stream(i)?).map_err(|e| match e {
UrnaError::MalformedSectionPayload { reason, .. } => malformed(reason),
other => other,
})?;
String::from_utf8(raw).map_err(|e| malformed(format!("txt_streams: invalid utf-8: {}", e)))
}
}
pub fn decode(bytes: &[u8]) -> crate::Result<Vec<u8>> {
let parsed = TxtStreams::parse(bytes)?;
let mut bodies: Vec<Vec<u8>> = Vec::with_capacity(parsed.count);
for i in 0..parsed.count {
let raw = zstd_decode(parsed.stream(i)?).map_err(|e| match e {
UrnaError::MalformedSectionPayload { reason, .. } => malformed(reason),
other => other,
})?;
std::str::from_utf8(&raw)
.map_err(|e| malformed(format!("txt_streams: invalid utf-8: {}", e)))?;
bodies.push(raw);
}
build_canonical(parsed.count, &bodies)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sections::encode_chunks_canonical;
fn texts(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn assert_byte_identical(items: &[&str]) {
let t = texts(items);
let packed = encode_txt_streams(&t).unwrap();
assert_eq!(packed[0], TXT_STREAMS_V1);
let raw = encode_chunks_canonical(&t).unwrap();
assert_eq!(decode(&packed).unwrap(), raw, "decode must rebuild raw");
}
#[test]
#[cfg_attr(miri, ignore)] fn byte_identical_across_corpora() {
assert_byte_identical(&[]);
assert_byte_identical(&["only one"]);
assert_byte_identical(&["primeiro", "segundo", "terceiro"]);
assert_byte_identical(&["coração", "informação", "açaí é ótimo", ""]);
}
#[test]
#[cfg_attr(miri, ignore)] fn o1_seek_returns_the_right_stream() {
let t = texts(&["alpha", "beta", "gama", "delta"]);
let packed = encode_txt_streams(&t).unwrap();
let parsed = TxtStreams::parse(&packed).unwrap();
assert_eq!(parsed.len(), 4);
for (i, s) in t.iter().enumerate() {
assert_eq!(&parsed.text(i).unwrap(), s, "text({}) mismatch", i);
}
assert!(parsed.text(4).is_err(), "oob index must error");
}
#[test]
#[cfg_attr(miri, ignore)] fn determinism_two_encodes_byte_identical() {
let t = texts(&["a", "bb", "ccc", "coração"]);
assert_eq!(
encode_txt_streams(&t).unwrap(),
encode_txt_streams(&t).unwrap()
);
}
}