1//! OneNote packages and the copies Save As writes (`corpus/notebook-package`).
2
3use notebook::{package, session::Notebook};
4use std::path::Path;
5
6fn header(image: &[u8]) -> onestore::Header {
7 onestore::Store::parse(image).unwrap().header
8}
9
10/// Each entry of the table of contents `image` holds: its filename and file identity.
11fn listed(image: &[u8]) -> Vec<(String, [u8; 16])> {
12 let store = onestore::Store::parse(image).unwrap();
13 let index = onestore::RevisionIndex::parse(&store).unwrap();
14 let document = onestore::document::Document::parse(&index).unwrap();
15 let revision = document.active(document.root).unwrap();
16 let onestore::document::Kind::Toc { entries, .. } = &revision.nodes[&revision.roots[&1]].kind
17 else {
18 panic!()
19 };
20 (entries.iter())
21 .map(|id| match &revision.nodes[id].kind {
22 onestore::document::Kind::Toc {
23 filename, identity, ..
24 } => (filename.clone().unwrap_or_default(), identity.unwrap()),
25 _ => panic!(),
26 })
27 .collect()
28}
29
30/// A notebook of a section, a group holding one, and a page deleted into the recycle bin.
31fn notebook(root: &Path, cache: &Path) -> Notebook {
32 let page = |title: &str| onestore::PageCreation::new(None, Some(title), "Author").unwrap();
33 let mut notebook = Notebook::create(root, cache, 0x00d7ff, &page("First")).unwrap();
34 notebook.create_group("", "Group").unwrap();
35 notebook
36 .create_section("Group", "Inner", &page("Inside"))
37 .unwrap();
38 notebook
39 .create_section("", "Doomed", &page("Binned"))
40 .unwrap();
41 let image = notebook.read_section("Doomed.one").unwrap();
42 let pages = notebook::session::stored_pages(&image).unwrap();
43 notebook
44 .recycle_pages(&[pages[0].page.clone()], "Author")
45 .unwrap();
46 notebook.delete("Doomed.one").unwrap();
47 notebook
48}
49
50/// Pack writes the notebook's tables of contents and sections, the recycle bin left out,
51/// outside any notebook; Unpack Notebook gives every file a new identity, placed under its
52/// folder's table of contents, whose entries follow, and the notebook opens as it was.
53/// `NOTEBOOK_PACKAGE_EXPORT` names a new directory receiving the package and the notebook
54/// unpacked, for OneNote 2010 to unpack and to open cold.
55#[test]
56fn a_notebook_packs_and_unpacks_as_onenote_packs_one() {
57 let temporary = tempfile::tempdir().unwrap();
58 let root = temporary.path().join("Packed");
59 let cache = temporary.path().join("cache");
60 let notebook = notebook(&root, &cache);
61 let files = package::notebook_files(&notebook, |_| None).unwrap();
62 let paths: Vec<&str> = files.iter().map(|(path, _)| path.as_str()).collect();
63 assert_eq!(
64 paths,
65 [
66 "Open Notebook.onetoc2",
67 "New Section 1.one",
68 "Group/Open Notebook.onetoc2",
69 "Group/Inner.one"
70 ]
71 );
72 let packed = package::pack(files.clone()).unwrap();
73 let read = package::read(&packed, 1 << 30).unwrap();
74 assert_eq!(read.len(), files.len());
75 for ((path, bytes), (stored, original)) in read.iter().zip(&files) {
76 assert_eq!(path, stored);
77 let (unplaced, placed) = (header(bytes), header(original));
78 assert_eq!(
79 unplaced.file_id, placed.file_id,
80 "{path} keeps its identity"
81 );
82 assert_eq!(
83 (unplaced.ancestor, unplaced.name_crc),
84 ([0; 16], 0),
85 "{path}"
86 );
87 assert_eq!(bytes[..128], original[..128]);
88 assert_eq!(bytes[148..], original[148..]);
89 }
90
91 let unpacked = temporary.path().join("Unpacked");
92 package::unpack(read, &unpacked).unwrap();
93 let opened = Notebook::open(&unpacked, &cache).unwrap();
94 let catalog = opened.catalog();
95 let sections = |folder: &notebook::discover::Folder| -> Vec<String> {
96 (folder.sections.iter())
97 .map(|section| section.path.clone())
98 .collect()
99 };
100 assert_eq!(sections(catalog), ["New Section 1.one"]);
101 assert_eq!(sections(&catalog.groups[0]), ["Group/Inner.one"]);
102 assert!(catalog.unavailable.is_empty() && catalog.groups[0].unavailable.is_empty());
103 let read = |path: &str| std::fs::read(unpacked.join(path)).unwrap();
104 let root_toc = read("Open Notebook.onetoc2");
105 let group_toc = read("Group/Open Notebook.onetoc2");
106 let section = read("New Section 1.one");
107 let inner = read("Group/Inner.one");
108 for (unpacked, (_, original)) in [&root_toc, &section, &group_toc, &inner]
109 .into_iter()
110 .zip(&files)
111 {
112 assert_ne!(header(unpacked).file_id, header(original).file_id);
113 }
114 assert_eq!(header(&root_toc).ancestor, [0; 16]);
115 assert_eq!(header(&section).ancestor, header(&root_toc).file_id);
116 assert_eq!(header(&group_toc).ancestor, header(&root_toc).file_id);
117 assert_eq!(header(&inner).ancestor, header(&group_toc).file_id);
118 // The recycle bin's entry stays, as in OneNote's packages, naming a folder not there.
119 let bin = listed(&files[0].1)[2].clone();
120 assert_eq!(bin.0, "OneNote_RecycleBin");
121 assert_eq!(
122 listed(&root_toc),
123 [
124 ("New Section 1.one".to_owned(), header(&section).file_id),
125 ("Group".to_owned(), header(&group_toc).file_id),
126 bin
127 ]
128 );
129 assert_eq!(
130 listed(&group_toc),
131 [("Inner.one".to_owned(), header(&inner).file_id)]
132 );
133 let pages = notebook::session::stored_pages(&inner).unwrap();
134 assert_eq!(pages[0].page.title, "Inside");
135 // Placed as they are, nothing is re-identified when the notebook is read again.
136 drop(opened);
137 let reopened = Notebook::open(&unpacked, &cache).unwrap();
138 assert_eq!(
139 reopened.catalog().sections[0].file_id,
140 header(&section).file_id
141 );
142 assert!(
143 package::unpack(Vec::new(), &unpacked).is_err(),
144 "the folder is new"
145 );
146
147 if let Some(directory) = std::env::var_os("NOTEBOOK_PACKAGE_EXPORT") {
148 let directory = Path::new(&directory);
149 std::fs::create_dir(directory).unwrap();
150 std::fs::write(directory.join("Packed.onepkg"), &packed).unwrap();
151 copy(&unpacked, &directory.join("Unpacked"));
152 }
153}
154
155fn copy(from: &Path, to: &Path) {
156 std::fs::create_dir(to).unwrap();
157 for entry in std::fs::read_dir(from).unwrap() {
158 let entry = entry.unwrap();
159 if entry.file_type().unwrap().is_dir() {
160 copy(&entry.path(), &to.join(entry.file_name()));
161 } else {
162 std::fs::copy(entry.path(), to.join(entry.file_name())).unwrap();
163 }
164 }
165}
166
167/// OneNote 2010's own package (LZX) unpacks and opens.
168#[test]
169fn onenotes_package_unpacks() {
170 let package = Path::new(env!("CARGO_MANIFEST_DIR"))
171 .join("../../corpus/notebook-package/native/Lab.onepkg");
172 let files = package::read(&std::fs::read(package).unwrap(), 1 << 30).unwrap();
173 let paths: Vec<&str> = files.iter().map(|(path, _)| path.as_str()).collect();
174 assert_eq!(paths, ["Alpha.one", "Beta.one", "Open Notebook.onetoc2"]);
175 let temporary = tempfile::tempdir().unwrap();
176 let root = temporary.path().join("Lab");
177 package::unpack(files, &root).unwrap();
178 let notebook = Notebook::open(&root, temporary.path().join("cache")).unwrap();
179 let sections: Vec<&str> = (notebook.catalog().sections.iter())
180 .map(|section| section.path.as_str())
181 .collect();
182 assert_eq!(sections, ["Alpha.one", "Beta.one"]);
183 let alpha =
184 notebook::session::stored_pages(&notebook.read_section("Alpha.one").unwrap()).unwrap();
185 let titles: Vec<&str> = alpha.iter().map(|page| page.page.title.as_str()).collect();
186 assert_eq!(titles, ["Alpha one", "Alpha three"]);
187}
188
189/// A package naming a file outside its folder, or unpacking past the limit, is refused.
190#[test]
191fn a_package_stays_in_its_folder() {
192 let section = onestore::create_empty_section("A.one", None).unwrap();
193 for path in ["../A.one", "Group/../../A.one", "C:/A.one", ""] {
194 let packed = package::pack(vec![(path.to_owned(), section.clone())]).unwrap();
195 assert!(package::read(&packed, 1 << 30).is_err(), "{path}");
196 }
197 let packed = package::pack(vec![("A.one".to_owned(), section.clone())]).unwrap();
198 assert!(package::read(&packed, 10).is_err());
199}
200
201/// Save As writes a section as a copy outside any notebook, and a page as a section of its
202/// own holding it, as OneNote 2010 does. `NOTEBOOK_SAVE_AS_EXPORT` names a new directory
203/// receiving both, for OneNote 2010 to open cold.
204#[test]
205fn save_as_writes_copies_outside_the_notebook() {
206 let temporary = tempfile::tempdir().unwrap();
207 let notebook = notebook(
208 &temporary.path().join("Notebook"),
209 &temporary.path().join("cache"),
210 );
211 let image = notebook.read_section("Group/Inner.one").unwrap();
212 let copy = package::section_copy(image.clone()).unwrap();
213 assert_ne!(header(&copy).file_id, header(&image).file_id);
214 assert_eq!(header(&copy).ancestor, [0; 16]);
215 assert_eq!(
216 notebook::session::stored_pages(&copy).unwrap()[0]
217 .page
218 .title,
219 "Inside"
220 );
221
222 let page = notebook::session::stored_pages(&image)
223 .unwrap()
224 .remove(0)
225 .page;
226 let saved = package::page_section(&page, "Inside.one", Some(0xe4a88a), "Author").unwrap();
227 let pages = notebook::session::stored_pages(&saved).unwrap();
228 assert_eq!(pages.len(), 1);
229 assert_eq!(
230 (pages[0].page.title.as_str(), pages[0].page.identity),
231 ("Inside", page.identity)
232 );
233 assert_eq!(header(&saved).ancestor, [0; 16]);
234 if let Some(directory) = std::env::var_os("NOTEBOOK_SAVE_AS_EXPORT") {
235 let directory = Path::new(&directory);
236 std::fs::create_dir(directory).unwrap();
237 std::fs::write(directory.join("Inner copy.one"), &copy).unwrap();
238 std::fs::write(directory.join("Inside.one"), &saved).unwrap();
239 }
240}
241
242/// Empty Recycle Bin deletes Deleted Pages' pages and every binned section's file, leaving the
243/// bin's table of contents as it was, as OneNote 2010 does.
244#[test]
245fn emptying_the_recycle_bin_leaves_its_table_of_contents() {
246 let temporary = tempfile::tempdir().unwrap();
247 let root = temporary.path().join("Notebook");
248 let mut notebook = notebook(&root, &temporary.path().join("cache"));
249 let bin = root.join("OneNote_RecycleBin");
250 let toc = std::fs::read(bin.join("Open Notebook.onetoc2")).unwrap();
251 assert!(bin.join("Doomed.one").exists());
252 notebook.empty_recycle_bin().unwrap();
253 assert!(!bin.join("Doomed.one").exists());
254 let deleted = std::fs::read(bin.join("OneNote_DeletedPages.one")).unwrap();
255 assert!(
256 notebook::session::stored_pages(&deleted)
257 .unwrap()
258 .is_empty()
259 );
260 assert_eq!(
261 std::fs::read(bin.join("Open Notebook.onetoc2")).unwrap(),
262 toc
263 );
264 notebook.empty_recycle_bin().unwrap();
265}