| 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 | |
| 9 | use crate::{ |
| 10 | Error, Result, fs, |
| 11 | session::{Notebook, lists, moved}, |
| 12 | }; |
| 13 | use onestore::{TocEdit, op::Edit, page::Page}; |
| 14 | use 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. |
| 21 | pub type Files = Vec<(String, Vec<u8>)>; |
| 22 | |
| 23 | fn 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. |
| 30 | pub 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 | |
| 59 | fn 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. |
| 68 | pub 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. |
| 86 | pub 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 | |
| 112 | fn 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. |
| 119 | pub 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. |
| 212 | pub 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. |
| 220 | pub 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 | } |