use std::io::{self, BufReader, Read, Write};
use super::int_index::IntIndexStreamer;
#[cfg(test)]
use super::int_index::PAGE_SIZE_INT;
#[cfg(test)]
fn int_footer_len(size: usize) -> usize {
let pages = size.div_ceil(PAGE_SIZE_INT).max(1);
(pages + 1) * 8 + 4 + 4
}
#[allow(dead_code)]
pub fn write_unsorted<W: Write>(mut w: W, entries: &[Vec<i32>]) -> io::Result<W> {
let mut header: Vec<i64> = Vec::with_capacity(entries.len());
let mut body = IntIndexStreamer::new(Vec::new());
let mut body_size: i64 = 0; for values in entries {
let pos = body_size; header.push(pos);
body.push(values.len() as i32)?;
body_size += 1;
for &v in values {
body.push(v)?;
body_size += 1;
}
}
let body_bytes = body.finish()?; let divider = body_bytes.len() as i64;
write_1n_tail(&mut w, &body_bytes, &header, divider)?;
Ok(w)
}
#[allow(dead_code)]
pub fn write_sorted<W: Write>(mut w: W, entries: &[Vec<i32>]) -> io::Result<W> {
let mut header: Vec<i64> = Vec::with_capacity(entries.len());
let mut body = IntIndexStreamer::new(Vec::new());
let mut body_size: i64 = 0;
for values in entries {
if values.is_empty() {
header.push(0);
continue;
}
let pos = body_size + 1; header.push(pos);
for &v in values {
body.push(v)?;
body_size += 1;
}
}
let body_bytes = body.finish()?;
let divider = body_bytes.len() as i64;
write_1n_tail(&mut w, &body_bytes, &header, divider)?;
Ok(w)
}
struct CountingWriter<W: Write> {
inner: W,
count: u64,
}
impl<W: Write> CountingWriter<W> {
fn new(w: W) -> Self {
Self { inner: w, count: 0 }
}
fn bytes_written(&self) -> u64 {
self.count
}
fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write> Write for CountingWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.count += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
pub fn write_sorted_iter_streaming<W, I, S>(w: W, entries: I) -> io::Result<W>
where
W: Write,
I: Iterator<Item = S>,
S: AsRef<[i32]>,
{
let mut cw = CountingWriter::new(w);
let mut header: Vec<i32> = Vec::new();
let mut body = IntIndexStreamer::new(&mut cw);
let mut body_size: i64 = 0;
for entry in entries {
let values = entry.as_ref();
if values.is_empty() {
header.push(0);
continue;
}
let pos = body_size + 1;
if pos >= (1i64 << 32) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"header2/PosIndexStreamer path not implemented; bodyPos {pos} at entry {}",
header.len()
),
));
}
header.push(pos as i32);
for &v in values {
body.push(v)?;
body_size += 1;
}
}
body.finish()?; let divider = cw.bytes_written() as i64;
let mut w = cw.into_inner();
let mut hdr = IntIndexStreamer::with_position(&mut w, divider);
for &pos in &header {
hdr.push(pos)?;
}
hdr.finish()?;
w.write_all(÷r.to_be_bytes())?;
Ok(w)
}
pub fn write_sorted_cb<W, F>(w: W, n_entries: usize, mut f: F) -> io::Result<W>
where
W: Write,
F: FnMut(&mut dyn FnMut(i32) -> io::Result<()>) -> io::Result<()>,
{
let mut cw = CountingWriter::new(w);
let mut body = IntIndexStreamer::new(&mut cw);
let mut body_size: i64 = 0;
let hdr_out: Vec<u8> = Vec::new();
let mut hdr_enc = zstd::stream::write::Encoder::new(hdr_out, 3).map_err(io::Error::other)?;
let mut total_entries: usize = 0;
let mut prev_pos: i64 = 0;
for _ in 0..n_entries {
let mut had_values = false;
let pos = body_size + 1;
if pos >= (1i64 << 32) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"header2/PosIndexStreamer path not implemented; bodyPos {pos} at entry {total_entries}"
),
));
}
{
let body_ref = &mut body;
let body_size_ref = &mut body_size;
let had_ref = &mut had_values;
f(&mut |v: i32| {
if !*had_ref {
*had_ref = true;
}
body_ref.push(v)?;
*body_size_ref += 1;
Ok(())
})?;
}
let encoded: i32 = if had_values {
let delta = pos - prev_pos; prev_pos = pos;
(delta + 1) as i32 } else {
0
};
hdr_enc
.write_all(&encoded.to_le_bytes())
.map_err(io::Error::other)?;
total_entries += 1;
}
let hdr_blob = hdr_enc.finish().map_err(io::Error::other)?;
body.finish()?;
let divider = cw.bytes_written() as i64;
let mut w = cw.into_inner();
let mut hdr = IntIndexStreamer::with_position(&mut w, divider);
let mut decoder = zstd::stream::Decoder::new(BufReader::new(&hdr_blob[..]))?;
let mut buf = [0u8; 64 * 1024];
let mut carry = [0u8; 4];
let mut carry_len = 0usize;
let mut written = 0usize;
let mut running_pos: i64 = 0;
loop {
let n = decoder.read(&mut buf).map_err(io::Error::other)?;
if n == 0 {
break;
}
let mut i = 0usize;
while carry_len > 0 && i < n {
carry[carry_len] = buf[i];
carry_len += 1;
i += 1;
if carry_len == 4 {
let encoded = i32::from_le_bytes(carry);
let hval = if encoded == 0 {
0
} else {
running_pos += (encoded - 1) as i64;
running_pos as i32
};
hdr.push(hval)?;
written += 1;
carry_len = 0;
}
}
while i + 4 <= n {
let encoded = i32::from_le_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]);
let hval = if encoded == 0 {
0
} else {
running_pos += (encoded - 1) as i64;
running_pos as i32
};
hdr.push(hval)?;
written += 1;
i += 4;
}
while i < n {
carry[carry_len] = buf[i];
carry_len += 1;
i += 1;
}
}
debug_assert_eq!(carry_len, 0);
debug_assert_eq!(written, total_entries);
drop(hdr_blob);
hdr.finish()?;
w.write_all(÷r.to_be_bytes())?;
Ok(w)
}
pub fn write_unsorted_iter_streaming<W, I, S>(w: W, entries: I) -> io::Result<W>
where
W: Write,
I: Iterator<Item = S>,
S: AsRef<[i32]>,
{
let mut cw = CountingWriter::new(w);
let mut header: Vec<i32> = Vec::new();
let mut body = IntIndexStreamer::new(&mut cw);
let mut body_size: i64 = 0;
for entry in entries {
let values = entry.as_ref();
let pos = body_size;
if pos >= (1i64 << 32) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"header2 path not implemented; pos {pos} at entry {}",
header.len()
),
));
}
header.push(pos as i32);
body.push(values.len() as i32)?;
body_size += 1;
for &v in values {
body.push(v)?;
body_size += 1;
}
}
body.finish()?;
let divider = cw.bytes_written() as i64;
let mut w = cw.into_inner();
let mut hdr = IntIndexStreamer::with_position(&mut w, divider);
for &pos in &header {
hdr.push(pos)?;
}
hdr.finish()?;
w.write_all(÷r.to_be_bytes())?;
Ok(w)
}
fn write_1n_tail<W: Write>(
w: &mut W,
body_bytes: &[u8],
header: &[i64],
divider: i64,
) -> io::Result<()> {
for (i, &pos) in header.iter().enumerate() {
if pos >= (1i64 << 32) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"header2/PosIndexStreamer path not implemented; \
bodyPos {pos} exceeds 2^32 for index {i}"
),
));
}
}
w.write_all(body_bytes)?;
let mut hdr = IntIndexStreamer::with_position(&mut *w, divider);
for &pos in header {
hdr.push(pos as i32)?;
}
hdr.finish()?;
w.write_all(÷r.to_be_bytes())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mat::codec::{compress_int, decode_int};
use crate::mat::int_index::PAGE_SIZE_INT;
fn parse_footer(region: &[u8]) -> (usize, i32, i64, Vec<i64>) {
let n = region.len();
let size = i32::from_be_bytes(region[n - 4..n].try_into().unwrap());
let page_size = i32::from_be_bytes(region[n - 8..n - 4].try_into().unwrap());
let value_count = size as i64;
let pages = (value_count as usize).div_ceil(page_size as usize);
let entries = pages + 1;
let footer_start = n - 8 - entries * 8;
let mut page_starts = Vec::with_capacity(entries);
for i in 0..entries {
let off = footer_start + i * 8;
page_starts.push(i64::from_be_bytes(region[off..off + 8].try_into().unwrap()));
}
(pages, page_size, value_count, page_starts)
}
fn decode_int_region(
file: &[u8],
base: i64,
pages: usize,
page_size: i32,
size: i64,
page_starts: &[i64],
) -> Vec<i32> {
let mut values: Vec<i32> = Vec::with_capacity(size as usize);
for i in 0..pages {
let start = (page_starts[i]) as usize;
let end = (page_starts[i + 1]) as usize;
let _ = base;
let n = std::cmp::min(page_size as usize, size as usize - i * page_size as usize);
values.extend_from_slice(&decode_int(&file[start..end], n));
}
values
}
fn sorted_get(header: &[i32], body_vals: &[i32], index: usize) -> Vec<i32> {
let body_size_plus1 = (body_vals.len() + 1) as i64;
let (p0, mut p1);
if index + 1 < header.len() {
p0 = header[index] as i64;
if p0 == 0 {
return Vec::new();
}
p1 = header[index + 1] as i64;
let mut j = index + 2;
while p1 < p0 && j < header.len() {
p1 = header[j] as i64;
j += 1;
}
if p1 < p0 {
p1 = body_size_plus1;
}
} else {
p0 = header[index] as i64;
if p0 == 0 {
return Vec::new();
}
p1 = body_size_plus1;
}
let start = (p0 - 1) as usize;
let len = (p1 - p0) as usize;
body_vals[start..start + len].to_vec()
}
fn first_diff(ours: &[u8], real: &[u8], divider: i64, header_end: usize) -> String {
let min = std::cmp::min(ours.len(), real.len());
let mut off = usize::MAX;
for i in 0..min {
if ours[i] != real[i] {
off = i;
break;
}
}
if off == usize::MAX {
return format!(
"lengths differ: ours.len={} real.len={}",
ours.len(),
real.len()
);
}
let region = if (off as i64) < divider {
"body"
} else if off < header_end {
"header"
} else {
"trailing-divider"
};
format!(
"first diff at offset {off} in {region} (divider={divider}, header_end={header_end}); \
ours.len={} real.len={}",
ours.len(),
real.len()
)
}
#[test]
fn unsorted_roundtrip_small() {
let entries: Vec<Vec<i32>> = vec![vec![7, 8, 9], vec![], vec![42], vec![1, 2, 3, 4, 5]];
let file = write_unsorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (bpages, bpsize, bsize, bstarts) = parse_footer(body_region);
assert_eq!(bstarts[0], 0, "body starts at 0");
let body_vals = decode_int_region(&file, 0, bpages, bpsize, bsize, &bstarts);
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
assert_eq!(hstarts[0], divider, "header starts at divider");
assert_eq!(hsize as usize, entries.len(), "one header entry per object");
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
let mut recon: Vec<Vec<i32>> = Vec::new();
for &pos in &hdr_vals {
let p = pos as usize;
let len = body_vals[p] as usize;
recon.push(body_vals[p + 1..p + 1 + len].to_vec());
}
assert_eq!(recon, entries, "unsorted reconstruct");
let again = write_unsorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file, "unsorted deterministic re-emit");
}
#[test]
fn sorted_roundtrip_small() {
let entries: Vec<Vec<i32>> = vec![
vec![100, 101],
vec![],
vec![200, 201, 202],
vec![],
vec![300],
vec![],
];
let file = write_sorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (bpages, bpsize, bsize, bstarts) = parse_footer(body_region);
let body_vals = decode_int_region(&file, 0, bpages, bpsize, bsize, &bstarts);
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
assert_eq!(hstarts[0], divider);
assert_eq!(hsize as usize, entries.len());
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
let recon: Vec<Vec<i32>> = (0..hdr_vals.len())
.map(|i| sorted_get(&hdr_vals, &body_vals, i))
.collect();
assert_eq!(recon, entries, "sorted reconstruct");
let again = write_sorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file, "sorted deterministic re-emit");
}
#[test]
fn footer_len_helper() {
assert_eq!(int_footer_len(3), 2 * 8 + 8);
assert_eq!(int_footer_len(PAGE_SIZE_INT), 2 * 8 + 8);
assert_eq!(int_footer_len(PAGE_SIZE_INT + 1), 3 * 8 + 8);
}
#[test]
fn sorted_all_empty_entries() {
let entries: Vec<Vec<i32>> = vec![vec![], vec![], vec![]];
let file = write_sorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (_, _, bsize, _) = parse_footer(body_region);
assert_eq!(bsize, 0, "all-empty sorted body must have size 0");
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
assert_eq!(hsize as usize, 3);
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
assert!(
hdr_vals.iter().all(|&v| v == 0),
"all-empty headers must be 0: {hdr_vals:?}"
);
let again = write_sorted(Vec::new(), &entries).unwrap();
assert_eq!(again, file);
}
#[test]
fn unsorted_all_empty_entries() {
let entries: Vec<Vec<i32>> = vec![vec![], vec![], vec![]];
let file = write_unsorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (bpages, bpsize, bsize, bstarts) = parse_footer(body_region);
assert_eq!(bsize, 3, "unsorted all-empty body has 3 length-0 ints");
let body_vals = decode_int_region(&file, 0, bpages, bpsize, bsize, &bstarts);
assert!(
body_vals.iter().all(|&v| v == 0),
"all length ints must be 0"
);
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
let mut recon: Vec<Vec<i32>> = Vec::new();
for &pos in &hdr_vals {
let p = pos as usize;
let len = body_vals[p] as usize;
recon.push(body_vals[p + 1..p + 1 + len].to_vec());
}
assert_eq!(recon, entries);
}
#[test]
fn sorted_single_entry_per_object() {
let entries: Vec<Vec<i32>> = vec![vec![10], vec![20], vec![30]];
let file = write_sorted(Vec::new(), &entries).unwrap();
let (_, _, recon) = decode_sorted_file(&file);
assert_eq!(recon, entries);
let again = write_sorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file);
}
#[test]
fn unsorted_single_entry_per_object() {
let entries: Vec<Vec<i32>> = vec![vec![10], vec![20], vec![30]];
let file = write_unsorted(Vec::new(), &entries).unwrap();
let (_, _, recon) = decode_unsorted_file(&file);
assert_eq!(recon, entries);
let again = write_unsorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file);
}
#[test]
fn sorted_mixed_empty_and_nonempty() {
let entries: Vec<Vec<i32>> = vec![vec![], vec![1, 2, 3], vec![], vec![], vec![99], vec![]];
let file = write_sorted(Vec::new(), &entries).unwrap();
let (_, _, recon) = decode_sorted_file(&file);
assert_eq!(recon, entries, "sorted mixed roundtrip");
let again = write_sorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file, "sorted mixed deterministic");
}
#[test]
fn unsorted_mixed_empty_and_nonempty() {
let entries: Vec<Vec<i32>> = vec![vec![5, 6], vec![], vec![7], vec![], vec![8, 9, 10]];
let file = write_unsorted(Vec::new(), &entries).unwrap();
let (_, _, recon) = decode_unsorted_file(&file);
assert_eq!(recon, entries, "unsorted mixed roundtrip");
let again = write_unsorted(Vec::new(), &recon).unwrap();
assert_eq!(again, file, "unsorted mixed deterministic");
}
#[test]
fn sorted_single_object_single_value() {
let entries: Vec<Vec<i32>> = vec![vec![42]];
let file = write_sorted(Vec::new(), &entries).unwrap();
let (_, _, recon) = decode_sorted_file(&file);
assert_eq!(recon, entries);
}
#[test]
fn sorted_empty_file() {
let entries: Vec<Vec<i32>> = vec![];
let file = write_sorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (_, _, bsize, _) = parse_footer(body_region);
assert_eq!(bsize, 0);
let hdr_region = &file[divider as usize..n - 8];
let (_, _, hsize, _) = parse_footer(hdr_region);
assert_eq!(hsize, 0);
}
#[test]
fn unsorted_empty_file() {
let entries: Vec<Vec<i32>> = vec![];
let file = write_unsorted(Vec::new(), &entries).unwrap();
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (_, _, bsize, _) = parse_footer(body_region);
assert_eq!(bsize, 0);
let hdr_region = &file[divider as usize..n - 8];
let (_, _, hsize, _) = parse_footer(hdr_region);
assert_eq!(hsize, 0);
}
#[test]
fn rejects_oversized_position() {
let body_bytes = compress_int(&[0]);
let header = vec![1i64 << 33];
let mut out = Vec::new();
let err = write_1n_tail(&mut out, &body_bytes, &header, body_bytes.len() as i64)
.expect_err("must reject 2^32+ position");
assert!(err.to_string().contains("exceeds 2^32"), "err: {err}");
}
fn decode_unsorted_file(file: &[u8]) -> (i64, usize, Vec<Vec<i32>>) {
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (bpages, bpsize, bsize, bstarts) = parse_footer(body_region);
let body_vals = decode_int_region(file, 0, bpages, bpsize, bsize, &bstarts);
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
let mut entries: Vec<Vec<i32>> = Vec::with_capacity(hdr_vals.len());
for &pos in &hdr_vals {
let p = pos as usize;
let len = body_vals[p] as usize;
entries.push(body_vals[p + 1..p + 1 + len].to_vec());
}
(divider, n - 8, entries)
}
fn decode_sorted_file(file: &[u8]) -> (i64, usize, Vec<Vec<i32>>) {
let n = file.len();
let divider = i64::from_be_bytes(file[n - 8..n].try_into().unwrap());
let body_region = &file[0..divider as usize];
let (bpages, bpsize, bsize, bstarts) = parse_footer(body_region);
let body_vals = decode_int_region(file, 0, bpages, bpsize, bsize, &bstarts);
let hdr_region = &file[divider as usize..n - 8];
let (hpages, hpsize, hsize, hstarts) = parse_footer(hdr_region);
let hstarts_local: Vec<i64> = hstarts.iter().map(|&s| s - divider).collect();
let hdr_vals = decode_int_region(hdr_region, 0, hpages, hpsize, hsize, &hstarts_local);
let entries: Vec<Vec<i32>> = (0..hdr_vals.len())
.map(|i| sorted_get(&hdr_vals, &body_vals, i))
.collect();
(divider, n - 8, entries)
}
#[test]
fn matches_real_domout() {
let path = "/tmp/matidx/dump_.domOut.index";
let Ok(real) = std::fs::read(path) else {
eprintln!("skip matches_real_domout: fixture absent at {path}");
return;
};
let (divider, header_end, entries) = decode_unsorted_file(&real);
let ours = write_unsorted(Vec::new(), &entries).unwrap();
if ours != real {
panic!(
"matches_real_domout byte mismatch: {}",
first_diff(&ours, &real, divider, header_end)
);
}
}
#[test]
fn matches_real_outbound() {
let path = "/tmp/matidx/dump_.outbound.index";
let Ok(real) = std::fs::read(path) else {
eprintln!("skip matches_real_outbound: fixture absent at {path}");
return;
};
let (divider, header_end, entries) = decode_sorted_file(&real);
let ours = write_sorted(Vec::new(), &entries).unwrap();
if ours != real {
panic!(
"matches_real_outbound byte mismatch: {}",
first_diff(&ours, &real, divider, header_end)
);
}
}
#[test]
fn matches_real_inbound() {
let path = "/tmp/matidx/dump_.inbound.index";
let Ok(real) = std::fs::read(path) else {
eprintln!("skip matches_real_inbound: fixture absent at {path}");
return;
};
let (divider, header_end, entries) = decode_sorted_file(&real);
let ours = write_sorted(Vec::new(), &entries).unwrap();
if ours != real {
panic!(
"matches_real_inbound byte mismatch: {}",
first_diff(&ours, &real, divider, header_end)
);
}
}
}