1//! OneNote packages (`.onepkg`) and the other copies OneNote 2010's Save As writes. A
2//! package is a cabinet holding a notebook's tables of contents and sections at their paths
3//! in its folder, the recycle bin left out though its entry stays; OneNote writes each file
4//! outside any notebook (no `guidAncestor` or `crcName`). Its Unpack Notebook gives each file
5//! an identity of its own and lists it anew beside its stale entry; here the entry follows
6//! the file instead (`corpus/notebook-package`). OneNote compresses with LZX; this writes
7//! MSZIP, which every cabinet reader takes, and reads either.
8
9use crate::{
10 Error, Result, fs,
11 session::{Notebook, lists, moved},
12};
13use onestore::{TocEdit, op::Edit, page::Page};
14use std::{
15 collections::BTreeMap,
16 io::{self, Cursor, Read, Write},
17 path::Path,
18};
19
20/// A package's files: each one's path in the notebook's folder, `/`-separated, and bytes.
21pub type Files = Vec<(String, Vec<u8>)>;
22
23fn invalid(message: &str) -> Error {
24 io::Error::new(io::ErrorKind::InvalidData, message.to_owned()).into()
25}
26
27/// The files of `notebook` a package holds, as OneNote 2010 packs them: each folder's table
28/// of contents and sections, then its groups', the recycle bin and copies left out. `image`
29/// gives a section the caller holds edits for, by catalog path, as its replica leaves it.
30pub fn notebook_files(
31 notebook: &Notebook,
32 mut image: impl FnMut(&str) -> Option<Vec<u8>>,
33) -> Result<Files> {
34 let mut files = Vec::new();
35 let mut folders = vec![notebook.catalog()];
36 while let Some(folder) = folders.pop() {
37 let Some(toc) = &folder.toc else {
38 continue;
39 };
40 files.push((
41 join(&folder.path, &toc.filename),
42 notebook.read_toc(&folder.path)?,
43 ));
44 for section in folder.sections.iter().filter(|section| !section.copy) {
45 let bytes = match image(&section.path) {
46 Some(bytes) => bytes,
47 None => notebook.read_section(&section.path)?,
48 };
49 files.push((section.path.clone(), bytes));
50 }
51 folders.extend(
52 (folder.groups.iter().rev())
53 .filter(|group| group.path.rsplit('/').next() != Some("OneNote_RecycleBin")),
54 );
55 }
56 Ok(files)
57}
58
59fn join(folder: &str, name: &str) -> String {
60 if folder.is_empty() {
61 name.to_owned()
62 } else {
63 format!("{folder}/{name}")
64 }
65}
66
67/// A package holding `files`, each taken out of its notebook as OneNote writes them.
68pub fn pack(files: Files) -> Result<Vec<u8>> {
69 let mut builder = cab::CabinetBuilder::new();
70 let folder = builder.add_folder(cab::CompressionType::MsZip);
71 for (path, _) in &files {
72 folder.add_file(path.replace('/', "\\"));
73 }
74 let mut writer = builder.build(Cursor::new(Vec::new()))?;
75 let mut files = files.into_iter();
76 while let Some(mut file) = writer.next_file()? {
77 let (_, mut bytes) = files.next().ok_or_else(|| invalid("A file went missing"))?;
78 onestore::place_image(&mut bytes, None)?;
79 file.write_all(&bytes)?;
80 }
81 Ok(writer.finish()?.into_inner())
82}
83
84/// The files the package `bytes` holds, refusing a path that leaves the notebook's folder
85/// and more than `limit` bytes unpacked.
86pub fn read(bytes: &[u8], limit: u64) -> Result<Files> {
87 let mut cabinet = cab::Cabinet::new(Cursor::new(bytes))?;
88 let mut listed = Vec::new();
89 let mut total = 0u64;
90 for folder in cabinet.folder_entries() {
91 for file in folder.file_entries() {
92 total += u64::from(file.uncompressed_size());
93 listed.push(file.name().to_owned());
94 }
95 }
96 if total > limit {
97 return Err(io::Error::from(io::ErrorKind::FileTooLarge).into());
98 }
99 let mut files = Vec::new();
100 for name in listed {
101 let path = name.replace('\\', "/");
102 if !path.split('/').all(component) {
103 return Err(invalid("The package names a file outside its notebook"));
104 }
105 let mut bytes = Vec::new();
106 cabinet.read_file(&name)?.read_to_end(&mut bytes)?;
107 files.push((path, bytes));
108 }
109 Ok(files)
110}
111
112fn component(name: &str) -> bool {
113 !name.is_empty() && !name.contains([':', '\0']) && name != "." && name != ".."
114}
115
116/// Writes package `files` to the new folder `root` as OneNote 2010's Unpack Notebook does:
117/// each table of contents and section takes a new identity, placed under its folder's table
118/// of contents, whose entries follow.
119pub fn unpack(mut files: Files, root: &Path) -> Result<()> {
120 let folder = |path: &str| {
121 path.rsplit_once('/')
122 .map_or("", |(folder, _)| folder)
123 .to_owned()
124 };
125 let toc = |path: &str| path.to_ascii_lowercase().ends_with(".onetoc2");
126 let mut tocs: BTreeMap<String, usize> = BTreeMap::new();
127 for (at, (path, _)) in files.iter().enumerate() {
128 if toc(path) && tocs.insert(folder(path), at).is_some() {
129 return Err(invalid(
130 "A folder of the package holds two tables of contents",
131 ));
132 }
133 }
134 // Parents first: a group's table of contents is placed under its parent's new identity.
135 let mut identities: BTreeMap<String, ([u8; 16], [u8; 16])> = BTreeMap::new();
136 let mut ordered: Vec<(&String, &usize)> = tocs.iter().collect();
137 ordered.sort_by_key(|(folder, _)| folder.split('/').count() * usize::from(!folder.is_empty()));
138 for (path, at) in ordered {
139 let image = &mut files[*at].1;
140 let old = onestore::Store::parse(image)?.header.file_id;
141 let new = onestore::reidentify(image)?;
142 let parent = (!path.is_empty()).then(|| folder(path));
143 let placed = match &parent {
144 Some(parent) => {
145 let (_, ancestor) = identities
146 .get(parent)
147 .ok_or_else(|| invalid("A section group's folder has no parent"))?;
148 Some((*ancestor, path.rsplit('/').next().unwrap_or_default()))
149 }
150 None => None,
151 };
152 onestore::place_image(image, placed)?;
153 identities.insert(path.clone(), (old, new));
154 }
155 // The entries each table of contents lists, by their files' old identities.
156 let mut followed: BTreeMap<String, Vec<TocEdit>> = BTreeMap::new();
157 for (path, image) in &mut files {
158 if toc(path) {
159 continue;
160 }
161 let home = folder(path);
162 let Some((_, ancestor)) = identities.get(&home) else {
163 return Err(invalid("A section's folder has no table of contents"));
164 };
165 let old = onestore::Store::parse(image)?.header.file_id;
166 let new = onestore::reidentify(image)?;
167 onestore::place_image(
168 image,
169 Some((*ancestor, path.rsplit('/').next().unwrap_or_default())),
170 )?;
171 (followed.entry(home).or_default()).push(TocEdit::Reidentify {
172 identity: old,
173 with: new,
174 });
175 }
176 for (path, (old, new)) in &identities {
177 if !path.is_empty() {
178 let edit = TocEdit::Reidentify {
179 identity: *old,
180 with: *new,
181 };
182 followed.entry(folder(path)).or_default().push(edit);
183 }
184 }
185 for (home, edits) in followed {
186 let image = &mut files[tocs[&home]].1;
187 let mut listed = Vec::new();
188 for edit in edits {
189 if let TocEdit::Reidentify { identity, .. } = &edit
190 && lists(image, *identity)?
191 {
192 listed.push(edit);
193 }
194 }
195 if let Some(transaction) = onestore::edit_table_of_contents(image, &listed)? {
196 transaction.apply(image)?;
197 }
198 }
199 fs::create_dir(root)?;
200 for (path, bytes) in &files {
201 let file = root.join(path);
202 if let Some(parent) = file.parent() {
203 fs::create_dir_all(parent)?;
204 }
205 fs::File::create_new(file)?.write_all(bytes)?;
206 }
207 Ok(())
208}
209
210/// The section `image` holds as Save As writes a copy of it: an identity of its own, outside
211/// any notebook.
212pub fn section_copy(mut image: Vec<u8>) -> Result<Vec<u8>> {
213 onestore::reidentify(&mut image)?;
214 onestore::place_image(&mut image, None)?;
215 Ok(image)
216}
217
218/// A new section, named `name` and coloured `color` (COLORREF), holding `page` as Save As
219/// writes a page: the page keeps its identity, title, date and creation time.
220pub fn page_section(page: &Page, name: &str, color: Option<u32>, author: &str) -> Result<Vec<u8>> {
221 let mut image = onestore::create_empty_section(name, color)?;
222 let arena = onestore::Arena::default();
223 let mut section = onestore::Section::open(&arena, image.clone())?;
224 section.apply(
225 author,
226 &Edit {
227 at: crate::now(),
228 ops: vec![moved(page, author)?],
229 },
230 )?;
231 if let Some(transaction) = section.seal()? {
232 transaction.apply(&mut image)?;
233 }
234 onestore::place_image(&mut image, None)?;
235 Ok(image)
236}