From e860c97095edca027e9a21a94783ad92e2f3305e Mon Sep 17 00:00:00 2001 From: clover caruso Date: Wed, 30 Sep 2026 03:36:47 -0700 Subject: [PATCH] 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 --- apps/ios/Snowbound/Lists.swift | 2 +- apps/ios/Snowbound/Notebooks.swift | 2 - arc/sync.md | 29 +++- crates/mobile/src/library.rs | 11 +- crates/notebook/README.md | 15 +- crates/notebook/src/session.rs | 158 +++++++++++++------ crates/notebook/tests/discovery.rs | 53 +++---- crates/notebook/tests/location.rs | 60 ++++++- crates/onestore/examples/scratch_pressure.rs | 48 ------ 9 files changed, 227 insertions(+), 151 deletions(-) delete mode 100644 crates/onestore/examples/scratch_pressure.rs diff --git a/apps/ios/Snowbound/Lists.swift b/apps/ios/Snowbound/Lists.swift index 1cc494ef877c504d61a24d0ce15068e8697d76dc..6bb640f9ed14d97dd02f808d818bf92bc4ccfc61 100644 --- a/apps/ios/Snowbound/Lists.swift +++ b/apps/ios/Snowbound/Lists.swift @@ -233,7 +233,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke systemName: tab.readable ? "rectangle.portrait.fill" : tab.downloading ? "icloud.and.arrow.down" : "lock.fill") content.imageProperties.tintColor = tab.uiColor if !tab.readable { - content.secondaryText = tab.downloading ? "Downloading…" : tab.copy.map { "Copy of “\($0)”" } ?? tab.problem ?? "Can’t be opened here" + content.secondaryText = tab.downloading ? "Downloading…" : tab.problem ?? "Can’t be opened here" content.textProperties.color = .secondaryLabel } case .status(let id): diff --git a/apps/ios/Snowbound/Notebooks.swift b/apps/ios/Snowbound/Notebooks.swift index dd3ffcc6b8dd515facf23b2db79ade74ca685020..b90708d6335467d83cdf9042cfdf667272046b6f 100644 --- a/apps/ios/Snowbound/Notebooks.swift +++ b/apps/ios/Snowbound/Notebooks.swift @@ -128,8 +128,6 @@ struct Tab: Decodable, Equatable { let readable: Bool /// Not on this device yet; `Notebook` asks iCloud Drive for it. let downloading: Bool - /// The section this file copies, which opens instead. - let copy: String? /// Why the file could not be read, where retrying or repair may help. let problem: String? diff --git a/arc/sync.md b/arc/sync.md index 930a6a91e0de20580f30a54ce0b8e2c3e0dbe5de..68e367b55c7311f0afcc1ee5d3250bdbde4cb65b 100644 --- a/arc/sync.md +++ b/arc/sync.md @@ -43,9 +43,9 @@ pages. Both desktop and iOS use exactly this surface. ## The replica -Each open section has a replica: a SQLite database in the app's cache, -identified by the section's logical identity, so the same file reopens the -same queue after a relaunch. It holds: +Each open section has a replica: a SQLite database in the app's cache, named by +where the notebook is and the section's logical identity, so the same file +reopens the same queue after a relaunch. It holds: - **the base**: the last remote image the queue applies to, stored in chunks so 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 mode with full synchronous commits, and every setting is read back to check it took. +The location matters because OneNote lets a notebook folder be copied whole, a +Finder duplicate or an iCloud `Name 2` conflict copy, and every section in the +copy keeps its identity. Keyed by identity alone, the copy would open the +original's queue and publish its edits into the wrong file. So replicas live in +`cache/replicas//.sqlite` (`notebook::location`), +where a location is a canonical local path or `smb://server/share/root`, and a +`location` file in each folder names it. A section renamed or moved inside its +notebook keeps its replica; a copy of a section inside one notebook (`Cross +2.one` beside `Cross.one`, which OneNote opens as a section of its own) is keyed +by its own path instead, the original being the one the folder's TOC lists. + +When a notebook moves, its queue follows by one of three roads: + +- **the app moved it** (iOS Rename): `location::moved` moves the folder's + replicas to the new location; +- **something else moved it** (Finder): on open, a section with no replica takes + one from the folder of a local location that no longer exists, if the file + stands as that replica's base or has moved on from it (a higher header + generation). A replica whose base is newer than the file, or a different state + of the same generation, belongs to another copy and stays; +- **an older install** named replicas by identity alone in the cache's top or + its `smb` folder: the first location to open the section takes it. + An attached file is a payload like a picture: its bytes ride in the edit that inserts it and publish inside that edit's revision. No other file is written, so nothing needs to reach the share before the revision that names it. That diff --git a/crates/mobile/src/library.rs b/crates/mobile/src/library.rs index d1a556f5ca2bb65245c55b06e071a5ea48236a15..94fc77f69707a886ab50b28eebfe183df763e65d 100644 --- a/crates/mobile/src/library.rs +++ b/crates/mobile/src/library.rs @@ -290,9 +290,6 @@ pub(crate) struct Tab { /// Not on this device yet, as iCloud Drive keeps it elsewhere; the host downloads it. #[serde(default)] pub(crate) downloading: bool, - /// The name of the section this file copies, which opens instead. - #[serde(default)] - pub(crate) copy: Option, /// Why the file could not be read, where retrying or repair may help. #[serde(default)] pub(crate) problem: Option, @@ -325,7 +322,6 @@ fn tabs(folder: &Folder, tabs: &mut Vec) { color: rgb(color.unwrap_or(SECTION_COLOR)), readable, downloading: false, - copy: None, problem: None, }); } @@ -337,10 +333,6 @@ fn tabs(folder: &Folder, tabs: &mut Vec) { color: rgb(SECTION_COLOR), readable: false, downloading: entry.reason == Reason::Evicted, - copy: match &entry.reason { - Reason::Copy { of } => Some(stem(of)), - _ => None, - }, problem: match entry.reason { Reason::InUse => Some("Section in use".into()), Reason::Unreadable => Some("Can’t read this section".into()), @@ -520,7 +512,6 @@ impl Library { color: rgb(SECTION_COLOR), readable: true, downloading: false, - copy: None, problem: None, }]); } @@ -1311,7 +1302,7 @@ pub unsafe extern "C" fn sb_notebook_moved( } /// The notebook's sections read again, as JSON: each with `name`, `path`, `group`, -/// `color` as sRGB bytes, `readable`, `downloading` and `copy`, in the notebook's order; null +/// `color` as sRGB bytes, `readable`, `downloading` and `problem`, in the notebook's order; null /// if it cannot be read. #[unsafe(no_mangle)] pub extern "C" fn sb_library_sections(library: &Library) -> *mut c_char { diff --git a/crates/notebook/README.md b/crates/notebook/README.md index 36badc0f89383ffc5ee76954de3e35740f331c9c..e77633ff9235501fbdfd52a9d823a27eee470750 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -53,9 +53,10 @@ thread rereads the queue. the files its folders list otherwise than when last opened (`discover::Cache`, kept under `cache_dir/listings`), and `session::Section::open(file, cache_dir, notify)` opens one section file through -a replica named by the section's document identity, so the same file reopens the -same queue after a relaunch. A section is `Send + Sync`: `apply(author, edit)` -returns at once and reports a refusal as `Event::Rejected`; `events()` drains +a replica named by the file's location and the section's document identity, so the same +file reopens the same queue after a relaunch and a copy of it elsewhere has its own +(`notebook::location`: `folder`, `local`, `smb`, and `moved` for a notebook the app moves). +A section is `Send + Sync`: `apply(author, edit)` returns at once and reports a refusal as `Event::Rejected`; `events()` drains remote changes (`Changed(spaces)`), publication outcomes, unreachable files and failures; `notify` runs on a background thread whenever an event waits. `conflicts()` lists each page's conflict pages, which `page` reads and `delete_pages` @@ -287,8 +288,12 @@ cache as it was. Each file read must be a consistent, bounded snapshot. The result is an observation across multiple files, not an atomic notebook transaction or authorization to publish an edit. Refresh rejects observed topology changes. Of -several files with one identity (a `Name 2.one` copy), the one its folder's TOC names, -else the newest, lists; the others list in `unavailable` as `Reason::Copy`. iOS's +several section files with one identity (a `Name 2.one` copy), each lists, as OneNote opens +each; all but the one its folder's TOC names (else the one with the shortest path) are +marked `copy`, have replicas of their own, and are never placed or listed anew by structure +operations, which touch only a TOC entry OneNote gave the copy's name. Of several groups +with one TOC identity, the one its parent's TOC names, else the newest, lists; the others +list in `unavailable` as `Reason::Copy`. iOS's `.Name.one.icloud` placeholders list there under their real names as `Reason::Evicted`. Retain the last accepted catalog if discovery fails; a connection failure does not mean files were deleted. diff --git a/crates/notebook/src/session.rs b/crates/notebook/src/session.rs index c86bf78d725d5d7e76ee7b3ab515d285e0948280..c043efed2bc82cbbfd15cd6c8d836e38860e5b6b 100644 --- a/crates/notebook/src/session.rs +++ b/crates/notebook/src/session.rs @@ -663,7 +663,11 @@ impl Notebook { /// Renames a section or section group: the file or folder and its TOC entry. pub fn rename(&mut self, path: &str, name: &str) -> Result { let (folder, entry) = split(path); - let (filename, identity) = self.entry(folder, entry)?; + let Entry { + filename, + identity, + copy, + } = self.entry(folder, entry)?; let target = if filename.to_ascii_lowercase().ends_with(".one") { format!("{name}.one") } else { @@ -675,20 +679,24 @@ impl Notebook { } self.storage .rename(&catalog_path(folder, &filename), &renamed)?; - let (_, ancestor) = self.toc(folder)?; - let placed = if target.ends_with(".one") { - renamed.clone() - } else { - catalog_path(&renamed, TOC) - }; - self.storage.place(&placed, ancestor, &target)?; - self.edit_toc( - folder, - &[onestore::TocEdit::Rename { - identity, - filename: target, - }], - )?; + if !copy { + let (_, ancestor) = self.toc(folder)?; + let placed = if target.ends_with(".one") { + renamed.clone() + } else { + catalog_path(&renamed, TOC) + }; + self.storage.place(&placed, ancestor, &target)?; + } + if let Some(identity) = identity { + self.edit_toc( + folder, + &[onestore::TocEdit::Rename { + identity, + filename: target, + }], + )?; + } self.refresh()?; Ok(renamed) } @@ -718,7 +726,7 @@ impl Notebook { if parent != folder { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); } - identities.push(self.entry(folder, entry)?.1); + identities.extend(self.entry(folder, entry)?.identity); } self.edit_toc(folder, &[onestore::TocEdit::Order(identities)])?; self.refresh().map(drop) @@ -730,12 +738,12 @@ impl Notebook { /// there the same way, then its folders go. Either way its folder's TOC entry goes. pub fn delete(&mut self, path: &str) -> Result<()> { let (folder, entry) = split(path); - let (filename, identity) = self.entry(folder, entry)?; + let deleted = self.entry(folder, entry)?; if path == RECYCLE_BIN { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); } - if filename.to_ascii_lowercase().ends_with(".one") { - self.bin_section(path, &filename, identity)?; + if deleted.filename.to_ascii_lowercase().ends_with(".one") { + self.bin_section(path, &deleted)?; } else { // Sections first, then each folder once it is empty, deepest first. let mut sections = Vec::new(); @@ -754,7 +762,7 @@ impl Notebook { group .sections .iter() - .map(|section| (section.path.clone(), section.file_id)), + .map(|section| (section.path.clone(), Entry::of(group, section))), ); folders.push(( group.path.clone(), @@ -762,9 +770,8 @@ impl Notebook { )); pending.extend(&group.groups); } - for (section, identity) in sections { - let (_, name) = split(§ion); - self.bin_section(§ion, name, identity)?; + for (section, entry) in sections { + self.bin_section(§ion, &entry)?; } for (group, toc) in folders.into_iter().rev() { if let Some(toc) = toc { @@ -773,7 +780,9 @@ impl Notebook { self.storage.delete(&group)?; } } - self.edit_toc(folder, &[onestore::TocEdit::Remove { identity }])?; + if let Some(identity) = deleted.identity { + self.edit_toc(folder, &[onestore::TocEdit::Remove { identity }])?; + } self.refresh().map(drop) } @@ -824,9 +833,10 @@ impl Notebook { Ok((bin_toc, identity)) } - /// Moves the section file at `path` into the recycle bin, under a name no binned - /// section has, and lists it there; its own folder's TOC is the caller's. - fn bin_section(&self, path: &str, filename: &str, identity: [u8; 16]) -> Result<()> { + /// Moves the section file at `path`, its folder's `entry`, into the recycle bin, under a + /// name no binned section has, and lists it there; its own folder's TOC is the caller's. + fn bin_section(&self, path: &str, entry: &Entry) -> Result<()> { + let filename = &entry.filename; let (bin_toc, bin_identity) = self.bin()?; let mut target = filename.to_owned(); let mut attempt = 1; @@ -837,6 +847,9 @@ impl Notebook { } let binned = catalog_path(RECYCLE_BIN, &target); self.storage.rename(path, &binned)?; + let (false, Some(identity)) = (entry.copy, entry.identity) else { + return Ok(()); + }; self.storage.place(&binned, bin_identity, &target)?; self.commit_toc( &bin_toc, @@ -900,7 +913,11 @@ impl Notebook { /// entry follows it. Returns its new catalog path. pub fn move_entry(&mut self, path: &str, folder: &str) -> Result { let (from, entry) = split(path); - let (filename, identity) = self.entry(from, entry)?; + let Entry { + filename, + identity, + copy, + } = self.entry(from, entry)?; self.folder(folder)?; let group = !filename.to_ascii_lowercase().ends_with(".one"); let target = catalog_path(folder, &filename); @@ -913,34 +930,40 @@ impl Notebook { } let (_, ancestor) = self.toc(folder)?; self.storage.rename(path, &target)?; - let placed = if group { - catalog_path(&target, TOC) - } else { - target.clone() - }; - self.storage.place(&placed, ancestor, &filename)?; - self.edit_toc( - folder, - &[onestore::TocEdit::Add { - filename, - identity, - group, - }], - )?; - self.edit_toc(from, &[onestore::TocEdit::Remove { identity }])?; + if !copy { + let placed = if group { + catalog_path(&target, TOC) + } else { + target.clone() + }; + self.storage.place(&placed, ancestor, &filename)?; + if let Some(identity) = identity { + self.edit_toc( + folder, + &[onestore::TocEdit::Add { + filename, + identity, + group, + }], + )?; + } + } + if let Some(identity) = identity { + self.edit_toc(from, &[onestore::TocEdit::Remove { identity }])?; + } self.refresh()?; Ok(target) } /// The stored filename and TOC identity of a folder's section or group. - fn entry(&self, folder: &str, name: &str) -> Result<(String, [u8; 16])> { + fn entry(&self, folder: &str, name: &str) -> Result { let parent = self.folder(folder)?; if let Some(section) = parent .sections .iter() .find(|section| split(§ion.path).1 == name) { - return Ok((name.to_owned(), section.file_id)); + return Ok(Entry::of(parent, section)); } if let Some(group) = parent .groups @@ -951,7 +974,11 @@ impl Notebook { .toc .as_ref() .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?; - return Ok((name.to_owned(), toc.file_id)); + return Ok(Entry { + filename: name.to_owned(), + identity: Some(toc.file_id), + copy: false, + }); } Err(io::Error::from(io::ErrorKind::NotFound).into()) } @@ -1179,6 +1206,45 @@ impl Notebook { } } +/// A section or group as its folder holds it, for the structure operations. +struct Entry { + filename: String, + /// What its folder's TOC lists it under, if anything. + identity: Option<[u8; 16]>, + /// A copy of another section of the notebook (`discover::Section::copy`). Its header names + /// the other's file, so it is never placed or listed anew: OneNote lists a copy it finds + /// under an identity of its own, leaving the file as it is. + copy: bool, +} + +impl Entry { + /// `section` of the folder `parent`: a copy has only the entry its folder's TOC lists + /// under its name. + fn of(parent: &discover::Folder, section: &discover::Section) -> Self { + let filename = split(§ion.path).1.to_owned(); + let identity = if section.copy { + parent + .toc + .iter() + .flat_map(|toc| &toc.unresolved) + .find(|entry| { + entry + .filename + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case(&filename)) + }) + .map(|entry| entry.file) + } else { + Some(section.file_id) + }; + Self { + filename, + identity, + copy: section.copy, + } + } +} + /// A page as a section file stores it. pub struct StoredPage { pub space: ExGuid, diff --git a/crates/notebook/tests/discovery.rs b/crates/notebook/tests/discovery.rs index 35fb0e523cb3ea9027c036030f100d2a69c0b8e8..4961768984d55c1a9f529af4dcc514f11937a684 100644 --- a/crates/notebook/tests/discovery.rs +++ b/crates/notebook/tests/discovery.rs @@ -83,41 +83,39 @@ fn copy_of(unavailable: ¬ebook::discover::Unavailable) -> Option<&str> { } } -/// OneNote 2010 lists a copied section file beside its original -/// (`Cross 2.one`, 2026-09-29 lab run); one replica cannot hold both, so the copy the TOC -/// does not name lists as unavailable. +/// Which of the folder's sections are copies of another, by path. +fn copies(folder: ¬ebook::discover::Folder) -> Vec<(&str, bool)> { + folder + .sections + .iter() + .map(|section| (section.path.as_str(), section.copy)) + .collect() +} + +/// OneNote 2010 opens a copied section file beside its original as a section of its own +/// (`Cross 2.one`, 2026-09-29 lab run); the one the TOC names is the original, however +/// short the copy's path. #[test] -#[ignore = "expects copies set aside; duplicates now open as sections"] -fn a_copied_section_lists_as_a_copy_of_the_one_the_toc_names() { +fn a_copied_section_lists_beside_the_one_the_toc_names() { let root = tempfile::tempdir().unwrap(); fixture(root.path()); fs::create_dir(root.path().join("group")).unwrap(); - for copy in ["one 2.one", "group/other.one"] { + for copy in ["o.one", "group/other.one"] { fs::copy(root.path().join("one.one"), root.path().join(copy)).unwrap(); touch(&root.path().join(copy), 2_000_000_000); } let catalog = discover(&mut Local::open(root.path()).unwrap(), limits()).unwrap(); - let paths = |folder: ¬ebook::discover::Folder| { - folder - .sections - .iter() - .map(|section| section.path.clone()) - .collect::>() - }; - assert_eq!(paths(&catalog), ["one.one"]); - assert_eq!(catalog.unavailable.len(), 1); - assert_eq!(catalog.unavailable[0].path, "one 2.one"); - assert_eq!(copy_of(&catalog.unavailable[0]), Some("one.one")); - assert!(catalog.unavailable[0].error.contains("\u{201c}one\u{201d}")); - let group = &catalog.groups[0]; - assert!(paths(group).is_empty()); - assert_eq!(group.unavailable[0].path, "group/other.one"); - assert_eq!(copy_of(&group.unavailable[0]), Some("one.one")); + assert!(catalog.unavailable.is_empty()); + let mut listed = copies(&catalog); + listed.sort(); + assert_eq!(listed, [("o.one", true), ("one.one", false)]); + assert_eq!(copies(&catalog.groups[0]), [("group/other.one", true)]); } +/// Without a TOC, the original is the copy with the shortest path, however new the others: +/// a choice that stands while the files keep their names. #[test] -#[ignore = "expects copies set aside; duplicates now open as sections"] -fn without_a_toc_the_newest_copy_lists() { +fn without_a_toc_the_shortest_path_is_the_original() { let root = tempfile::tempdir().unwrap(); fixture(root.path()); fs::remove_file(root.path().join("Open Notebook.onetoc2")).unwrap(); @@ -125,10 +123,9 @@ fn without_a_toc_the_newest_copy_lists() { touch(&root.path().join("one.one"), 1_000_000_000); touch(&root.path().join("one 2.one"), 2_000_000_000); let catalog = discover(&mut Local::open(root.path()).unwrap(), limits()).unwrap(); - assert_eq!(catalog.sections.len(), 1); - assert_eq!(catalog.sections[0].path, "one 2.one"); - assert_eq!(catalog.unavailable[0].path, "one.one"); - assert_eq!(copy_of(&catalog.unavailable[0]), Some("one 2.one")); + let mut listed = copies(&catalog); + listed.sort(); + assert_eq!(listed, [("one 2.one", true), ("one.one", false)]); } #[test] diff --git a/crates/notebook/tests/location.rs b/crates/notebook/tests/location.rs index 1c757d5d9de09d00a46d0c0ed4d475b8b2c1fe5d..76d0e2e1c0aeb984e171155e8d5b150241f78ca5 100644 --- a/crates/notebook/tests/location.rs +++ b/crates/notebook/tests/location.rs @@ -226,28 +226,72 @@ fn a_replica_named_by_identity_alone_moves_to_the_first_location_that_opens_it() } #[test] -#[ignore = "expects copies set aside; duplicates now open as sections"] -fn a_section_copy_beside_its_original_opens_only_as_a_lone_file_with_its_own_replica() { +fn a_section_copy_beside_its_original_opens_with_its_own_replica() { let directory = tempfile::tempdir().unwrap(); let cache = directory.path().join("cache"); let root = notebook(directory.path(), "Notebook"); std::fs::copy(root.join("First.one"), root.join("First 2.one")).unwrap(); let mut notebook = Notebook::open(&root, &cache).unwrap(); - // Discovery lists the copy as unavailable, so one replica serves the section. - assert!(notebook.section("First 2.one", || {}).is_err()); + assert_ne!( + notebook.replica_path("First.one").unwrap(), + notebook.replica_path("First 2.one").unwrap() + ); assert_eq!( notebook .replicas() .iter() .filter(|known| known.path.starts_with("First")) .count(), - 1 + 2 ); - let original = Section::open(root.join("First.one"), &cache, || {}).unwrap(); - let copy = Section::open(root.join("First 2.one"), &cache, || {}).unwrap(); + let original = notebook.section("First.one", || {}).unwrap(); + let copy = notebook.section("First 2.one", || {}).unwrap(); original.set_offline(true); - typed(&original, "Mine "); + let space = typed(&original, "Mine "); assert!(copy.pending().unwrap().is_empty()); + original.set_offline(false); + until("the original published", || { + stored(&root.join("First.one"), space).starts_with("Mine ") + }); + assert_eq!(stored(&root.join("First 2.one"), space), "First"); original.close().unwrap(); copy.close().unwrap(); } + +/// Whether the catalog marks the section at `path` a copy. +fn copy(notebook: &Notebook, path: &str) -> bool { + notebook + .catalog() + .sections + .iter() + .find(|section| section.path == path) + .unwrap() + .copy +} + +#[test] +fn renaming_reordering_or_deleting_a_copy_leaves_the_originals_toc_entry() { + let directory = tempfile::tempdir().unwrap(); + let root = notebook(directory.path(), "Notebook"); + // `F.one` would be the original by its shorter path were the TOC not to list `First.one`. + for copy in ["F.one", "First 2.one"] { + std::fs::copy(root.join("First.one"), root.join(copy)).unwrap(); + } + let original = std::fs::read(root.join("First.one")).unwrap(); + let identity = onestore::Store::parse(&original).unwrap().header.file_id; + std::fs::write( + root.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents("Open Notebook.onetoc2", &[("First.one", identity)]) + .unwrap(), + ) + .unwrap(); + let mut notebook = Notebook::open(&root, directory.path().join("cache")).unwrap(); + assert!(!copy(¬ebook, "First.one") && copy(¬ebook, "F.one")); + notebook.delete("First 2.one").unwrap(); + assert_eq!(notebook.rename("F.one", "G").unwrap(), "G.one"); + notebook.reorder("", &["G.one", "First.one"]).unwrap(); + assert!(!copy(¬ebook, "First.one") && copy(¬ebook, "G.one")); + // A copy keeps its header as it was, as OneNote leaves one. + assert_eq!(std::fs::read(root.join("G.one")).unwrap(), original); + assert_eq!(std::fs::read(root.join("First.one")).unwrap(), original); +} diff --git a/crates/onestore/examples/scratch_pressure.rs b/crates/onestore/examples/scratch_pressure.rs deleted file mode 100644 index fc283a652f93aa1297c185035b9c4ba7b08d79a0..0000000000000000000000000000000000000000 --- a/crates/onestore/examples/scratch_pressure.rs +++ /dev/null @@ -1,48 +0,0 @@ -use onestore::{RevisionIndex, Store, document::{Document, Kind}}; -fn mb(bytes: &[u8]) -> Vec { - let mut out = Vec::new(); let mut v = 0u64; let mut s = 0; - 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; } } - let count = (out[0] >> 1) as usize; - out[1..].iter().take(count).map(|r| if r & 1 == 1 { -(r >> 1) } else { r >> 1 }).collect() -} -fn guid(b: &[u8]) -> String { - let d1 = u32::from_le_bytes(b[0..4].try_into().unwrap()); - let d2 = u16::from_le_bytes(b[4..6].try_into().unwrap()); - let d3 = u16::from_le_bytes(b[6..8].try_into().unwrap()); - format!("{d1:08x}-{d2:04x}-{d3:04x}-{}", b[8..].iter().map(|x| format!("{x:02x}")).collect::()) -} -fn main() { - let path = std::env::args().nth(1).unwrap(); - let bytes = std::fs::read(path).unwrap(); - let store = Store::parse(&bytes).unwrap(); - let index = RevisionIndex::parse(&store).unwrap(); - let document = Document::parse(&index).unwrap(); - for (space, _) in document.pages().unwrap() { - let rev = document.active(space).unwrap(); - for (id, node) in &rev.nodes { - match &node.kind { - Kind::InkStyle { dimensions, .. } => { - println!("STYLE {id:?} {:?}", node.kind); - for d in dimensions.chunks_exact(32) { - let lo = i32::from_le_bytes(d[16..20].try_into().unwrap()); - let hi = i32::from_le_bytes(d[20..24].try_into().unwrap()); - let units = u32::from_le_bytes(d[24..28].try_into().unwrap()); - let res = f32::from_le_bytes(d[28..32].try_into().unwrap()); - println!(" dim {} [{lo}, {hi}] units {units} res {res}", guid(&d[..16])); - } - for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } } - } - Kind::InkStroke { path, style, bias, index, .. } => { - let v = mb(path); - println!("STROKE {id:?} style {style:?} bias {bias:?} index {index:?} n={} values {:?}", v.len(), v); - for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } } - } - Kind::Ink { .. } | Kind::InkData { .. } => { - println!("{id:?} {:?} layout {:?}", node.kind, node.layout); - for set in &node.extra { for f in set { println!(" extra {:#x} {:?}", f.id, f.value); } } - } - _ => {} - } - } - } -} -- 2.54.0