1//! A rewritten object stores the global id table entries it names, not the table of the
2//! revision it was read from.
3
4#[path = "support/ops.rs"]
5mod ops;
6
7use onestore::{
8 RevisionIndex, Store,
9 document::Document,
10 page::{Page, PageObject, Paragraph},
11};
12
13const SOURCE: &[u8] = include_bytes!("../../../corpus/native/20260905-05/notebook/synthetic.one");
14
15fn pages(bytes: &[u8]) -> Vec<(onestore::ExGuid, Page)> {
16 let store = Store::parse(bytes).unwrap();
17 let index = RevisionIndex::parse(&store).unwrap();
18 let document = Document::parse(&index).unwrap();
19 document
20 .pages()
21 .unwrap()
22 .into_iter()
23 .map(|(space, _)| (space, Page::from_space(&document, space).unwrap()))
24 .collect()
25}
26
27/// Table entries per object group written past `from`, as `(entries, highest index + 1)`.
28fn tables(bytes: &[u8], from: usize) -> Vec<(usize, u32)> {
29 let store = Store::parse(bytes).unwrap();
30 store
31 .lists
32 .values()
33 .filter(|list| list.nodes.first().is_some_and(|node| node.offset >= from))
34 .map(|list| {
35 let entries: Vec<u32> = list
36 .nodes
37 .iter()
38 .filter(|node| node.id == 0x24)
39 .map(|node| u32::from_le_bytes(node.payload[..4].try_into().unwrap()))
40 .collect();
41 (entries.len(), entries.iter().max().map_or(0, |max| max + 1))
42 })
43 .filter(|(entries, _)| *entries > 0)
44 .collect()
45}
46
47/// `ONESTORE_TABLE_EXPORT` names a directory receiving the edited notebook for a cold reopen.
48#[test]
49fn a_text_edit_stores_the_table_entries_it_names() {
50 let mut bytes = SOURCE.to_vec();
51 let mut sparse = false;
52 let mut edited = Vec::new();
53 for (space, mut page) in pages(SOURCE) {
54 let Some(text) = page
55 .objects
56 .iter_mut()
57 .filter_map(|object| match object {
58 PageObject::Outline(outline) if !outline.title => Some(&mut outline.paragraphs),
59 _ => None,
60 })
61 .flatten()
62 .find_map(|paragraph| paragraph.text_mut())
63 else {
64 continue;
65 };
66 let format = text.text.format_at(0).unwrap().clone();
67 text.text
68 .append(Paragraph::new(" and a few more words".to_owned(), format))
69 .unwrap();
70 let written = ops::saved(&bytes, space, &page)
71 .unwrap()
72 .as_slice()
73 .to_vec();
74 for (entries, span) in tables(&written, bytes.len()) {
75 sparse |= (entries as u32) < span;
76 }
77 assert_eq!(
78 ops::saved(&written, space, &page).unwrap().as_slice(),
79 written
80 );
81 bytes = written;
82 edited.push((space, page));
83 }
84 // The fixture's pages were written over several sessions, so a text edit names a
85 // subset of its revision's table and the stored indices have gaps.
86 assert!(sparse);
87 let stored = pages(&bytes);
88 for (space, page) in &edited {
89 let (_, stored) = stored.iter().find(|(id, _)| id == space).unwrap();
90 assert!(stored.objects == page.objects);
91 }
92 if let Some(export) = std::env::var_os("ONESTORE_TABLE_EXPORT") {
93 let export = std::path::Path::new(&export);
94 std::fs::create_dir_all(export).unwrap();
95 std::fs::write(export.join("synthetic.one"), &bytes).unwrap();
96 std::fs::copy(
97 "../../corpus/native/20260905-05/notebook/Open Notebook.onetoc2",
98 export.join("Open Notebook.onetoc2"),
99 )
100 .unwrap();
101 }
102}