use crate::bzz_encode::bzz_encode;
use crate::djvu_document::DjVuBookmark;
pub fn encode_navm(bookmarks: &[DjVuBookmark]) -> Vec<u8> {
if bookmarks.is_empty() {
return Vec::new();
}
let total = count_bookmarks(bookmarks);
let mut raw = Vec::new();
raw.push((total >> 8) as u8);
raw.push(total as u8);
for bm in bookmarks {
write_bookmark(&mut raw, bm);
}
bzz_encode(&raw)
}
fn count_bookmarks(bookmarks: &[DjVuBookmark]) -> usize {
bookmarks
.iter()
.map(|bm| 1 + count_bookmarks(&bm.children))
.sum()
}
fn write_bookmark(buf: &mut Vec<u8>, bm: &DjVuBookmark) {
let n_children = bm.children.len().min(255) as u8;
buf.push(n_children);
write_navm_str(buf, &bm.title);
write_navm_str(buf, &bm.url);
for child in &bm.children {
write_bookmark(buf, child);
}
}
fn write_navm_str(buf: &mut Vec<u8>, s: &str) {
let bytes = s.as_bytes();
let len = bytes.len();
buf.push((len >> 16) as u8);
buf.push((len >> 8) as u8);
buf.push(len as u8);
buf.extend_from_slice(bytes);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bzz_new::bzz_decode;
use crate::djvu_document::DjVuBookmark;
fn bm(title: &str, url: &str, children: Vec<DjVuBookmark>) -> DjVuBookmark {
DjVuBookmark {
title: title.to_string(),
url: url.to_string(),
children,
}
}
fn decode_raw(compressed: &[u8]) -> (usize, Vec<DjVuBookmark>) {
let decoded = bzz_decode(compressed).expect("bzz_decode");
assert!(decoded.len() >= 2, "too short: {}", decoded.len());
let total = u16::from_be_bytes([decoded[0], decoded[1]]) as usize;
let mut pos = 2usize;
let mut bookmarks = Vec::new();
let mut count = 0usize;
while count < total {
bookmarks.push(decode_bookmark(&decoded, &mut pos, &mut count));
}
(total, bookmarks)
}
fn decode_bookmark(data: &[u8], pos: &mut usize, count: &mut usize) -> DjVuBookmark {
let n_children = data[*pos] as usize;
*pos += 1;
let title = decode_str(data, pos);
let url = decode_str(data, pos);
*count += 1;
let mut children = Vec::new();
for _ in 0..n_children {
children.push(decode_bookmark(data, pos, count));
}
DjVuBookmark {
title,
url,
children,
}
}
fn decode_str(data: &[u8], pos: &mut usize) -> String {
let len = ((data[*pos] as usize) << 16)
| ((data[*pos + 1] as usize) << 8)
| (data[*pos + 2] as usize);
*pos += 3;
let s = core::str::from_utf8(&data[*pos..*pos + len])
.unwrap()
.to_string();
*pos += len;
s
}
#[test]
fn empty_bookmarks_returns_empty() {
assert!(encode_navm(&[]).is_empty());
}
#[test]
fn single_bookmark_roundtrip() {
let bookmarks = vec![bm("Chapter 1", "#page=1", vec![])];
let encoded = encode_navm(&bookmarks);
assert!(!encoded.is_empty());
let (total, decoded) = decode_raw(&encoded);
assert_eq!(total, 1);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].title, "Chapter 1");
assert_eq!(decoded[0].url, "#page=1");
assert!(decoded[0].children.is_empty());
}
#[test]
fn nested_bookmarks_roundtrip() {
let bookmarks = vec![
bm(
"Part I",
"#page=1",
vec![
bm("Chapter 1", "#page=5", vec![]),
bm("Chapter 2", "#page=12", vec![]),
],
),
bm(
"Part II",
"#page=20",
vec![bm("Chapter 3", "#page=25", vec![])],
),
];
let encoded = encode_navm(&bookmarks);
let (total, decoded) = decode_raw(&encoded);
assert_eq!(total, 5);
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].title, "Part I");
assert_eq!(decoded[0].children.len(), 2);
assert_eq!(decoded[0].children[0].title, "Chapter 1");
assert_eq!(decoded[1].title, "Part II");
assert_eq!(decoded[1].children[0].title, "Chapter 3");
}
#[test]
fn unicode_title_roundtrip() {
let bookmarks = vec![bm("Раздел 1 — Введение", "#page=1", vec![])];
let encoded = encode_navm(&bookmarks);
let (total, decoded) = decode_raw(&encoded);
assert_eq!(total, 1);
assert_eq!(decoded[0].title, "Раздел 1 — Введение");
}
#[test]
fn total_count_flat_traversal() {
let bookmarks = vec![bm(
"A",
"#1",
vec![bm("B", "#2", vec![bm("C", "#3", vec![])])],
)];
let encoded = encode_navm(&bookmarks);
let (total, _) = decode_raw(&encoded);
assert_eq!(total, 3); }
}