authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 03:36:47-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 04:09:27-07:00
loge860c97095edca027e9a21a94783ad92e2f3305e
tree5eb01ddb6f73bc58e3940505239cc89407820053
parent9ffccbbee0705d77fefa54a2cc1670a25a3dab7a
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix(sync): section copies keep their own TOC entries

A copy of a section opens beside its original with its own replica; renaming, moving, deleting, or reordering it edits only the copy's own TOC entry, as OneNote 2010 lists copies under fresh identities and leaves their files as they are. Assisted-by: claude-opus-5.5

9 files changed, 227 insertions(+), 151 deletions(-)

apps/ios/Snowbound/Lists.swift+1-1
......@@ -233,7 +233,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
233233 systemName: tab.readable ? "rectangle.portrait.fill" : tab.downloading ? "icloud.and.arrow.down" : "lock.fill")
234234 content.imageProperties.tintColor = tab.uiColor
235235 if !tab.readable {
236 content.secondaryText = tab.downloading ? "Downloading…" : tab.copy.map { "Copy of “\($0)”" } ?? tab.problem ?? "Can’t be opened here"
236 content.secondaryText = tab.downloading ? "Downloading…" : tab.problem ?? "Can’t be opened here"
237237 content.textProperties.color = .secondaryLabel
238238 }
239239 case .status(let id):
apps/ios/Snowbound/Notebooks.swift-2
......@@ -128,8 +128,6 @@ struct Tab: Decodable, Equatable {
128128 let readable: Bool
129129 /// Not on this device yet; `Notebook` asks iCloud Drive for it.
130130 let downloading: Bool
131 /// The section this file copies, which opens instead.
132 let copy: String?
133131 /// Why the file could not be read, where retrying or repair may help.
134132 let problem: String?
135133
arc/sync.md+26-3
......@@ -43,9 +43,9 @@ pages. Both desktop and iOS use exactly this surface.
4343
4444## The replica
4545
46Each open section has a replica: a SQLite database in the app's cache,
47identified by the section's logical identity, so the same file reopens the
48same queue after a relaunch. It holds:
46Each open section has a replica: a SQLite database in the app's cache, named by
47where the notebook is and the section's logical identity, so the same file
48reopens the same queue after a relaunch. It holds:
4949
5050- **the base**: the last remote image the queue applies to, stored in chunks so
5151 that acknowledging a publication rewrites only the chunks the transaction
......@@ -59,6 +59,29 @@ replayed and the open batch applied, rebuilt on open. The database runs in WAL
5959mode with full synchronous commits, and every setting is read back to check it
6060took.
6161
62The location matters because OneNote lets a notebook folder be copied whole, a
63Finder duplicate or an iCloud `Name 2` conflict copy, and every section in the
64copy keeps its identity. Keyed by identity alone, the copy would open the
65original's queue and publish its edits into the wrong file. So replicas live in
66`cache/replicas/<hash of location>/<identity>.sqlite` (`notebook::location`),
67where a location is a canonical local path or `smb://server/share/root`, and a
68`location` file in each folder names it. A section renamed or moved inside its
69notebook keeps its replica; a copy of a section inside one notebook (`Cross
702.one` beside `Cross.one`, which OneNote opens as a section of its own) is keyed
71by its own path instead, the original being the one the folder's TOC lists.
72
73When a notebook moves, its queue follows by one of three roads:
74
75- **the app moved it** (iOS Rename): `location::moved` moves the folder's
76 replicas to the new location;
77- **something else moved it** (Finder): on open, a section with no replica takes
78 one from the folder of a local location that no longer exists, if the file
79 stands as that replica's base or has moved on from it (a higher header
80 generation). A replica whose base is newer than the file, or a different state
81 of the same generation, belongs to another copy and stays;
82- **an older install** named replicas by identity alone in the cache's top or
83 its `smb` folder: the first location to open the section takes it.
84
6285An attached file is a payload like a picture: its bytes ride in the edit that
6386inserts it and publish inside that edit's revision. No other file is written,
6487so nothing needs to reach the share before the revision that names it. That
crates/mobile/src/library.rs+1-10
......@@ -290,9 +290,6 @@ pub(crate) struct Tab {
290290 /// Not on this device yet, as iCloud Drive keeps it elsewhere; the host downloads it.
291291 #[serde(default)]
292292 pub(crate) downloading: bool,
293 /// The name of the section this file copies, which opens instead.
294 #[serde(default)]
295 pub(crate) copy: Option<String>,
296293 /// Why the file could not be read, where retrying or repair may help.
297294 #[serde(default)]
298295 pub(crate) problem: Option<String>,
......@@ -325,7 +322,6 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) {
325322 color: rgb(color.unwrap_or(SECTION_COLOR)),
326323 readable,
327324 downloading: false,
328 copy: None,
329325 problem: None,
330326 });
331327 }
......@@ -337,10 +333,6 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) {
337333 color: rgb(SECTION_COLOR),
338334 readable: false,
339335 downloading: entry.reason == Reason::Evicted,
340 copy: match &entry.reason {
341 Reason::Copy { of } => Some(stem(of)),
342 _ => None,
343 },
344336 problem: match entry.reason {
345337 Reason::InUse => Some("Section in use".into()),
346338 Reason::Unreadable => Some("Can’t read this section".into()),
......@@ -520,7 +512,6 @@ impl Library {
520512 color: rgb(SECTION_COLOR),
521513 readable: true,
522514 downloading: false,
523 copy: None,
524515 problem: None,
525516 }]);
526517 }
......@@ -1311,7 +1302,7 @@ pub unsafe extern "C" fn sb_notebook_moved(
13111302}
13121303
13131304/// The notebook's sections read again, as JSON: each with `name`, `path`, `group`,
1314/// `color` as sRGB bytes, `readable`, `downloading` and `copy`, in the notebook's order; null
1305/// `color` as sRGB bytes, `readable`, `downloading` and `problem`, in the notebook's order; null
13151306/// if it cannot be read.
13161307#[unsafe(no_mangle)]
13171308pub extern "C" fn sb_library_sections(library: &Library) -> *mut c_char {
crates/notebook/README.md+10-5
......@@ -53,9 +53,10 @@ thread rereads the queue.
5353the files its folders list otherwise than when last opened (`discover::Cache`, kept under
5454`cache_dir/listings`), and
5555`session::Section::open(file, cache_dir, notify)` opens one section file through
56a replica named by the section's document identity, so the same file reopens the
57same queue after a relaunch. A section is `Send + Sync`: `apply(author, edit)`
58returns at once and reports a refusal as `Event::Rejected`; `events()` drains
56a replica named by the file's location and the section's document identity, so the same
57file reopens the same queue after a relaunch and a copy of it elsewhere has its own
58(`notebook::location`: `folder`, `local`, `smb`, and `moved` for a notebook the app moves).
59A section is `Send + Sync`: `apply(author, edit)` returns at once and reports a refusal as `Event::Rejected`; `events()` drains
5960remote changes (`Changed(spaces)`), publication outcomes, unreachable files and
6061failures; `notify` runs on a background thread whenever an event waits.
6162`conflicts()` lists each page's conflict pages, which `page` reads and `delete_pages`
......@@ -287,8 +288,12 @@ cache as it was.
287288Each file read must be a consistent, bounded snapshot. The result is an
288289observation across multiple files, not an atomic notebook transaction or
289290authorization to publish an edit. Refresh rejects observed topology changes. Of
290several files with one identity (a `Name 2.one` copy), the one its folder's TOC names,
291else the newest, lists; the others list in `unavailable` as `Reason::Copy`. iOS's
291several section files with one identity (a `Name 2.one` copy), each lists, as OneNote opens
292each; all but the one its folder's TOC names (else the one with the shortest path) are
293marked `copy`, have replicas of their own, and are never placed or listed anew by structure
294operations, which touch only a TOC entry OneNote gave the copy's name. Of several groups
295with one TOC identity, the one its parent's TOC names, else the newest, lists; the others
296list in `unavailable` as `Reason::Copy`. iOS's
292297`.Name.one.icloud` placeholders list there under their real names as `Reason::Evicted`.
293298Retain the last accepted catalog if discovery fails; a connection failure does not mean
294299files were deleted.
crates/notebook/src/session.rs+112-46
......@@ -663,7 +663,11 @@ impl Notebook {
663663 /// Renames a section or section group: the file or folder and its TOC entry.
664664 pub fn rename(&mut self, path: &str, name: &str) -> Result<String> {
665665 let (folder, entry) = split(path);
666 let (filename, identity) = self.entry(folder, entry)?;
666 let Entry {
667 filename,
668 identity,
669 copy,
670 } = self.entry(folder, entry)?;
667671 let target = if filename.to_ascii_lowercase().ends_with(".one") {
668672 format!("{name}.one")
669673 } else {
......@@ -675,20 +679,24 @@ impl Notebook {
675679 }
676680 self.storage
677681 .rename(&catalog_path(folder, &filename), &renamed)?;
678 let (_, ancestor) = self.toc(folder)?;
679 let placed = if target.ends_with(".one") {
680 renamed.clone()
681 } else {
682 catalog_path(&renamed, TOC)
683 };
684 self.storage.place(&placed, ancestor, &target)?;
685 self.edit_toc(
686 folder,
687 &[onestore::TocEdit::Rename {
688 identity,
689 filename: target,
690 }],
691 )?;
682 if !copy {
683 let (_, ancestor) = self.toc(folder)?;
684 let placed = if target.ends_with(".one") {
685 renamed.clone()
686 } else {
687 catalog_path(&renamed, TOC)
688 };
689 self.storage.place(&placed, ancestor, &target)?;
690 }
691 if let Some(identity) = identity {
692 self.edit_toc(
693 folder,
694 &[onestore::TocEdit::Rename {
695 identity,
696 filename: target,
697 }],
698 )?;
699 }
692700 self.refresh()?;
693701 Ok(renamed)
694702 }
......@@ -718,7 +726,7 @@ impl Notebook {
718726 if parent != folder {
719727 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
720728 }
721 identities.push(self.entry(folder, entry)?.1);
729 identities.extend(self.entry(folder, entry)?.identity);
722730 }
723731 self.edit_toc(folder, &[onestore::TocEdit::Order(identities)])?;
724732 self.refresh().map(drop)
......@@ -730,12 +738,12 @@ impl Notebook {
730738 /// there the same way, then its folders go. Either way its folder's TOC entry goes.
731739 pub fn delete(&mut self, path: &str) -> Result<()> {
732740 let (folder, entry) = split(path);
733 let (filename, identity) = self.entry(folder, entry)?;
741 let deleted = self.entry(folder, entry)?;
734742 if path == RECYCLE_BIN {
735743 return Err(io::Error::from(io::ErrorKind::InvalidInput).into());
736744 }
737 if filename.to_ascii_lowercase().ends_with(".one") {
738 self.bin_section(path, &filename, identity)?;
745 if deleted.filename.to_ascii_lowercase().ends_with(".one") {
746 self.bin_section(path, &deleted)?;
739747 } else {
740748 // Sections first, then each folder once it is empty, deepest first.
741749 let mut sections = Vec::new();
......@@ -754,7 +762,7 @@ impl Notebook {
754762 group
755763 .sections
756764 .iter()
757 .map(|section| (section.path.clone(), section.file_id)),
765 .map(|section| (section.path.clone(), Entry::of(group, section))),
758766 );
759767 folders.push((
760768 group.path.clone(),
......@@ -762,9 +770,8 @@ impl Notebook {
762770 ));
763771 pending.extend(&group.groups);
764772 }
765 for (section, identity) in sections {
766 let (_, name) = split(&section);
767 self.bin_section(&section, name, identity)?;
773 for (section, entry) in sections {
774 self.bin_section(&section, &entry)?;
768775 }
769776 for (group, toc) in folders.into_iter().rev() {
770777 if let Some(toc) = toc {
......@@ -773,7 +780,9 @@ impl Notebook {
773780 self.storage.delete(&group)?;
774781 }
775782 }
776 self.edit_toc(folder, &[onestore::TocEdit::Remove { identity }])?;
783 if let Some(identity) = deleted.identity {
784 self.edit_toc(folder, &[onestore::TocEdit::Remove { identity }])?;
785 }
777786 self.refresh().map(drop)
778787 }
779788
......@@ -824,9 +833,10 @@ impl Notebook {
824833 Ok((bin_toc, identity))
825834 }
826835
827 /// Moves the section file at `path` into the recycle bin, under a name no binned
828 /// section has, and lists it there; its own folder's TOC is the caller's.
829 fn bin_section(&self, path: &str, filename: &str, identity: [u8; 16]) -> Result<()> {
836 /// Moves the section file at `path`, its folder's `entry`, into the recycle bin, under a
837 /// name no binned section has, and lists it there; its own folder's TOC is the caller's.
838 fn bin_section(&self, path: &str, entry: &Entry) -> Result<()> {
839 let filename = &entry.filename;
830840 let (bin_toc, bin_identity) = self.bin()?;
831841 let mut target = filename.to_owned();
832842 let mut attempt = 1;
......@@ -837,6 +847,9 @@ impl Notebook {
837847 }
838848 let binned = catalog_path(RECYCLE_BIN, &target);
839849 self.storage.rename(path, &binned)?;
850 let (false, Some(identity)) = (entry.copy, entry.identity) else {
851 return Ok(());
852 };
840853 self.storage.place(&binned, bin_identity, &target)?;
841854 self.commit_toc(
842855 &bin_toc,
......@@ -900,7 +913,11 @@ impl Notebook {
900913 /// entry follows it. Returns its new catalog path.
901914 pub fn move_entry(&mut self, path: &str, folder: &str) -> Result<String> {
902915 let (from, entry) = split(path);
903 let (filename, identity) = self.entry(from, entry)?;
916 let Entry {
917 filename,
918 identity,
919 copy,
920 } = self.entry(from, entry)?;
904921 self.folder(folder)?;
905922 let group = !filename.to_ascii_lowercase().ends_with(".one");
906923 let target = catalog_path(folder, &filename);
......@@ -913,34 +930,40 @@ impl Notebook {
913930 }
914931 let (_, ancestor) = self.toc(folder)?;
915932 self.storage.rename(path, &target)?;
916 let placed = if group {
917 catalog_path(&target, TOC)
918 } else {
919 target.clone()
920 };
921 self.storage.place(&placed, ancestor, &filename)?;
922 self.edit_toc(
923 folder,
924 &[onestore::TocEdit::Add {
925 filename,
926 identity,
927 group,
928 }],
929 )?;
930 self.edit_toc(from, &[onestore::TocEdit::Remove { identity }])?;
933 if !copy {
934 let placed = if group {
935 catalog_path(&target, TOC)
936 } else {
937 target.clone()
938 };
939 self.storage.place(&placed, ancestor, &filename)?;
940 if let Some(identity) = identity {
941 self.edit_toc(
942 folder,
943 &[onestore::TocEdit::Add {
944 filename,
945 identity,
946 group,
947 }],
948 )?;
949 }
950 }
951 if let Some(identity) = identity {
952 self.edit_toc(from, &[onestore::TocEdit::Remove { identity }])?;
953 }
931954 self.refresh()?;
932955 Ok(target)
933956 }
934957
935958 /// The stored filename and TOC identity of a folder's section or group.
936 fn entry(&self, folder: &str, name: &str) -> Result<(String, [u8; 16])> {
959 fn entry(&self, folder: &str, name: &str) -> Result<Entry> {
937960 let parent = self.folder(folder)?;
938961 if let Some(section) = parent
939962 .sections
940963 .iter()
941964 .find(|section| split(&section.path).1 == name)
942965 {
943 return Ok((name.to_owned(), section.file_id));
966 return Ok(Entry::of(parent, section));
944967 }
945968 if let Some(group) = parent
946969 .groups
......@@ -951,7 +974,11 @@ impl Notebook {
951974 .toc
952975 .as_ref()
953976 .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
954 return Ok((name.to_owned(), toc.file_id));
977 return Ok(Entry {
978 filename: name.to_owned(),
979 identity: Some(toc.file_id),
980 copy: false,
981 });
955982 }
956983 Err(io::Error::from(io::ErrorKind::NotFound).into())
957984 }
......@@ -1179,6 +1206,45 @@ impl Notebook {
11791206 }
11801207}
11811208
1209/// A section or group as its folder holds it, for the structure operations.
1210struct Entry {
1211 filename: String,
1212 /// What its folder's TOC lists it under, if anything.
1213 identity: Option<[u8; 16]>,
1214 /// A copy of another section of the notebook (`discover::Section::copy`). Its header names
1215 /// the other's file, so it is never placed or listed anew: OneNote lists a copy it finds
1216 /// under an identity of its own, leaving the file as it is.
1217 copy: bool,
1218}
1219
1220impl Entry {
1221 /// `section` of the folder `parent`: a copy has only the entry its folder's TOC lists
1222 /// under its name.
1223 fn of(parent: &discover::Folder, section: &discover::Section) -> Self {
1224 let filename = split(&section.path).1.to_owned();
1225 let identity = if section.copy {
1226 parent
1227 .toc
1228 .iter()
1229 .flat_map(|toc| &toc.unresolved)
1230 .find(|entry| {
1231 entry
1232 .filename
1233 .as_deref()
1234 .is_some_and(|name| name.eq_ignore_ascii_case(&filename))
1235 })
1236 .map(|entry| entry.file)
1237 } else {
1238 Some(section.file_id)
1239 };
1240 Self {
1241 filename,
1242 identity,
1243 copy: section.copy,
1244 }
1245 }
1246}
1247
11821248/// A page as a section file stores it.
11831249pub struct StoredPage {
11841250 pub space: ExGuid,
crates/notebook/tests/discovery.rs+25-28
......@@ -83,41 +83,39 @@ fn copy_of(unavailable: &notebook::discover::Unavailable) -> Option<&str> {
8383 }
8484}
8585
86/// OneNote 2010 lists a copied section file beside its original
87/// (`Cross 2.one`, 2026-09-29 lab run); one replica cannot hold both, so the copy the TOC
88/// does not name lists as unavailable.
86/// Which of the folder's sections are copies of another, by path.
87fn copies(folder: &notebook::discover::Folder) -> Vec<(&str, bool)> {
88 folder
89 .sections
90 .iter()
91 .map(|section| (section.path.as_str(), section.copy))
92 .collect()
93}
94
95/// OneNote 2010 opens a copied section file beside its original as a section of its own
96/// (`Cross 2.one`, 2026-09-29 lab run); the one the TOC names is the original, however
97/// short the copy's path.
8998#[test]
90#[ignore = "expects copies set aside; duplicates now open as sections"]
91fn a_copied_section_lists_as_a_copy_of_the_one_the_toc_names() {
99fn a_copied_section_lists_beside_the_one_the_toc_names() {
92100 let root = tempfile::tempdir().unwrap();
93101 fixture(root.path());
94102 fs::create_dir(root.path().join("group")).unwrap();
95 for copy in ["one 2.one", "group/other.one"] {
103 for copy in ["o.one", "group/other.one"] {
96104 fs::copy(root.path().join("one.one"), root.path().join(copy)).unwrap();
97105 touch(&root.path().join(copy), 2_000_000_000);
98106 }
99107 let catalog = discover(&mut Local::open(root.path()).unwrap(), limits()).unwrap();
100 let paths = |folder: &notebook::discover::Folder| {
101 folder
102 .sections
103 .iter()
104 .map(|section| section.path.clone())
105 .collect::<Vec<_>>()
106 };
107 assert_eq!(paths(&catalog), ["one.one"]);
108 assert_eq!(catalog.unavailable.len(), 1);
109 assert_eq!(catalog.unavailable[0].path, "one 2.one");
110 assert_eq!(copy_of(&catalog.unavailable[0]), Some("one.one"));
111 assert!(catalog.unavailable[0].error.contains("\u{201c}one\u{201d}"));
112 let group = &catalog.groups[0];
113 assert!(paths(group).is_empty());
114 assert_eq!(group.unavailable[0].path, "group/other.one");
115 assert_eq!(copy_of(&group.unavailable[0]), Some("one.one"));
108 assert!(catalog.unavailable.is_empty());
109 let mut listed = copies(&catalog);
110 listed.sort();
111 assert_eq!(listed, [("o.one", true), ("one.one", false)]);
112 assert_eq!(copies(&catalog.groups[0]), [("group/other.one", true)]);
116113}
117114
115/// Without a TOC, the original is the copy with the shortest path, however new the others:
116/// a choice that stands while the files keep their names.
118117#[test]
119#[ignore = "expects copies set aside; duplicates now open as sections"]
120fn without_a_toc_the_newest_copy_lists() {
118fn without_a_toc_the_shortest_path_is_the_original() {
121119 let root = tempfile::tempdir().unwrap();
122120 fixture(root.path());
123121 fs::remove_file(root.path().join("Open Notebook.onetoc2")).unwrap();
......@@ -125,10 +123,9 @@ fn without_a_toc_the_newest_copy_lists() {
125123 touch(&root.path().join("one.one"), 1_000_000_000);
126124 touch(&root.path().join("one 2.one"), 2_000_000_000);
127125 let catalog = discover(&mut Local::open(root.path()).unwrap(), limits()).unwrap();
128 assert_eq!(catalog.sections.len(), 1);
129 assert_eq!(catalog.sections[0].path, "one 2.one");
130 assert_eq!(catalog.unavailable[0].path, "one.one");
131 assert_eq!(copy_of(&catalog.unavailable[0]), Some("one 2.one"));
126 let mut listed = copies(&catalog);
127 listed.sort();
128 assert_eq!(listed, [("one 2.one", true), ("one.one", false)]);
132129}
133130
134131#[test]
crates/notebook/tests/location.rs+52-8
......@@ -226,28 +226,72 @@ fn a_replica_named_by_identity_alone_moves_to_the_first_location_that_opens_it()
226226}
227227
228228#[test]
229#[ignore = "expects copies set aside; duplicates now open as sections"]
230fn a_section_copy_beside_its_original_opens_only_as_a_lone_file_with_its_own_replica() {
229fn a_section_copy_beside_its_original_opens_with_its_own_replica() {
231230 let directory = tempfile::tempdir().unwrap();
232231 let cache = directory.path().join("cache");
233232 let root = notebook(directory.path(), "Notebook");
234233 std::fs::copy(root.join("First.one"), root.join("First 2.one")).unwrap();
235234 let mut notebook = Notebook::open(&root, &cache).unwrap();
236 // Discovery lists the copy as unavailable, so one replica serves the section.
237 assert!(notebook.section("First 2.one", || {}).is_err());
235 assert_ne!(
236 notebook.replica_path("First.one").unwrap(),
237 notebook.replica_path("First 2.one").unwrap()
238 );
238239 assert_eq!(
239240 notebook
240241 .replicas()
241242 .iter()
242243 .filter(|known| known.path.starts_with("First"))
243244 .count(),
244 1
245 2
245246 );
246 let original = Section::open(root.join("First.one"), &cache, || {}).unwrap();
247 let copy = Section::open(root.join("First 2.one"), &cache, || {}).unwrap();
247 let original = notebook.section("First.one", || {}).unwrap();
248 let copy = notebook.section("First 2.one", || {}).unwrap();
248249 original.set_offline(true);
249 typed(&original, "Mine ");
250 let space = typed(&original, "Mine ");
250251 assert!(copy.pending().unwrap().is_empty());
252 original.set_offline(false);
253 until("the original published", || {
254 stored(&root.join("First.one"), space).starts_with("Mine ")
255 });
256 assert_eq!(stored(&root.join("First 2.one"), space), "First");
251257 original.close().unwrap();
252258 copy.close().unwrap();
253259}
260
261/// Whether the catalog marks the section at `path` a copy.
262fn copy(notebook: &Notebook, path: &str) -> bool {
263 notebook
264 .catalog()
265 .sections
266 .iter()
267 .find(|section| section.path == path)
268 .unwrap()
269 .copy
270}
271
272#[test]
273fn renaming_reordering_or_deleting_a_copy_leaves_the_originals_toc_entry() {
274 let directory = tempfile::tempdir().unwrap();
275 let root = notebook(directory.path(), "Notebook");
276 // `F.one` would be the original by its shorter path were the TOC not to list `First.one`.
277 for copy in ["F.one", "First 2.one"] {
278 std::fs::copy(root.join("First.one"), root.join(copy)).unwrap();
279 }
280 let original = std::fs::read(root.join("First.one")).unwrap();
281 let identity = onestore::Store::parse(&original).unwrap().header.file_id;
282 std::fs::write(
283 root.join("Open Notebook.onetoc2"),
284 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("First.one", identity)])
285 .unwrap(),
286 )
287 .unwrap();
288 let mut notebook = Notebook::open(&root, directory.path().join("cache")).unwrap();
289 assert!(!copy(&notebook, "First.one") && copy(&notebook, "F.one"));
290 notebook.delete("First 2.one").unwrap();
291 assert_eq!(notebook.rename("F.one", "G").unwrap(), "G.one");
292 notebook.reorder("", &["G.one", "First.one"]).unwrap();
293 assert!(!copy(&notebook, "First.one") && copy(&notebook, "G.one"));
294 // A copy keeps its header as it was, as OneNote leaves one.
295 assert_eq!(std::fs::read(root.join("G.one")).unwrap(), original);
296 assert_eq!(std::fs::read(root.join("First.one")).unwrap(), original);
297}
crates/onestore/examples/scratch_pressure.rs deleted-48
......@@ -1,48 +0,0 @@
1use onestore::{RevisionIndex, Store, document::{Document, Kind}};
2fn mb(bytes: &[u8]) -> Vec<i64> {
3 let mut out = Vec::new(); let mut v = 0u64; let mut s = 0;
4 for &b in bytes { v |= u64::from(b & 0x7f) << s; s += 7; if b & 0x80 == 0 { out.push(v as i64); v = 0; s = 0; } }
5 let count = (out[0] >> 1) as usize;
6 out[1..].iter().take(count).map(|r| if r & 1 == 1 { -(r >> 1) } else { r >> 1 }).collect()
7}
8fn guid(b: &[u8]) -> String {
9 let d1 = u32::from_le_bytes(b[0..4].try_into().unwrap());
10 let d2 = u16::from_le_bytes(b[4..6].try_into().unwrap());
11 let d3 = u16::from_le_bytes(b[6..8].try_into().unwrap());
12 format!("{d1:08x}-{d2:04x}-{d3:04x}-{}", b[8..].iter().map(|x| format!("{x:02x}")).collect::<String>())
13}
14fn main() {
15 let path = std::env::args().nth(1).unwrap();
16 let bytes = std::fs::read(path).unwrap();
17 let store = Store::parse(&bytes).unwrap();
18 let index = RevisionIndex::parse(&store).unwrap();
19 let document = Document::parse(&index).unwrap();
20 for (space, _) in document.pages().unwrap() {
21 let rev = document.active(space).unwrap();
22 for (id, node) in &rev.nodes {
23 match &node.kind {
24 Kind::InkStyle { dimensions, .. } => {
25 println!("STYLE {id:?} {:?}", node.kind);
26 for d in dimensions.chunks_exact(32) {
27 let lo = i32::from_le_bytes(d[16..20].try_into().unwrap());
28 let hi = i32::from_le_bytes(d[20..24].try_into().unwrap());
29 let units = u32::from_le_bytes(d[24..28].try_into().unwrap());
30 let res = f32::from_le_bytes(d[28..32].try_into().unwrap());
31 println!(" dim {} [{lo}, {hi}] units {units} res {res}", guid(&d[..16]));
32 }
33 for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } }
34 }
35 Kind::InkStroke { path, style, bias, index, .. } => {
36 let v = mb(path);
37 println!("STROKE {id:?} style {style:?} bias {bias:?} index {index:?} n={} values {:?}", v.len(), v);
38 for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } }
39 }
40 Kind::Ink { .. } | Kind::InkData { .. } => {
41 println!("{id:?} {:?} layout {:?}", node.kind, node.layout);
42 for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } }
43 }
44 _ => {}
45 }
46 }
47 }
48}