1use notebook::fs;
2use onestore::{
3 Arena, ExGuid, Section, TextAttribute,
4 document::{Format, Layout},
5 op::{Op, PageOp},
6 page::{
7 Outline, PageObject, PageParagraph, Paragraph, ParagraphContent, TextObject, text::new_id,
8 },
9};
10use serde::Deserialize;
11use serde_json::{Value, json};
12use std::{
13 env,
14 io::{self, Write},
15};
16
17#[derive(Deserialize)]
18#[serde(deny_unknown_fields)]
19struct Edit {
20 space: ExGuid,
21 object: ExGuid,
22 action: Action,
23}
24
25#[derive(Deserialize)]
26#[serde(tag = "type", deny_unknown_fields)]
27enum Action {
28 Text {
29 start: u32,
30 end: u32,
31 replacement: String,
32 },
33 Format {
34 start: u32,
35 end: u32,
36 attributes: Vec<TextAttribute>,
37 },
38 Paragraph {
39 before: Option<ExGuid>,
40 text: String,
41 author: String,
42 },
43 Outline {
44 x: f32,
45 y: f32,
46 text: String,
47 author: String,
48 },
49}
50
51/// A new paragraph holding `text` in OneNote's default text style.
52fn paragraph(text: String) -> Result<PageParagraph, Box<dyn std::error::Error>> {
53 let format = Format {
54 font: Some("Calibri".into()),
55 font_size: Some(11.0),
56 language: Some(0x409),
57 ..Default::default()
58 };
59 Ok(PageParagraph {
60 id: new_id()?,
61 parent: None,
62 level: 1,
63 style: None,
64 format: Format::default(),
65 content: ParagraphContent::Text(TextObject {
66 id: new_id()?,
67 date_field: None,
68 text: Paragraph::new(text, format),
69 tags: Vec::new(),
70 }),
71 lists: Vec::new(),
72 tags: Vec::new(),
73 media: Default::default(),
74 collapsed: false,
75 })
76}
77
78fn run(args: &[std::ffi::OsString]) -> Result<Value, Box<dyn std::error::Error>> {
79 if let [mode, root] = args
80 && mode == "catalog"
81 {
82 let catalog = notebook::discover::discover(
83 &mut notebook::discover::Local::open(root)?,
84 notebook::discover::Limits {
85 entries: 100_000,
86 bytes_per_file: notebook::MAX_FILE_BYTES,
87 depth: 64,
88 },
89 )?;
90 return Ok(json!({"ok": true, "catalog": catalog}));
91 }
92 if args.len() != 3
93 || !["snapshot", "check", "commit"]
94 .iter()
95 .any(|mode| args[0] == *mode)
96 {
97 return Err("Usage: onestore-diagnostic catalog ROOT | snapshot FILE NEW_SNAPSHOT | check SNAPSHOT - | commit FILE SNAPSHOT; edits arrive as JSON on stdin".into());
98 }
99 if args[0] == "snapshot" {
100 let bytes = fs::read_file(&args[1])?;
101 let mut file = fs::OpenOptions::new()
102 .write(true)
103 .create_new(true)
104 .open(&args[2])?;
105 file.write_all(&bytes)?;
106 file.sync_all()?;
107 return Ok(json!({"ok": true, "bytes": bytes.len()}));
108 }
109 let check = args[0] == "check";
110 if check && args[2] != "-" {
111 return Err("The check command requires '-' as its final argument".into());
112 }
113 let bytes = fs::read(if check { &args[1] } else { &args[2] })?;
114 let edit: Edit = serde_json::from_reader(io::stdin().lock())?;
115 let sid = edit.space;
116 let oid = edit.object;
117 let (op, author) = match edit.action {
118 Action::Text {
119 start,
120 end,
121 replacement,
122 } => (
123 PageOp::Text {
124 text: oid,
125 range: start..end,
126 with: replacement,
127 },
128 String::new(),
129 ),
130 Action::Format {
131 start,
132 end,
133 attributes,
134 } => (
135 PageOp::Format {
136 text: oid,
137 range: start..end,
138 set: attributes,
139 clear: Vec::new(),
140 },
141 String::new(),
142 ),
143 Action::Paragraph {
144 before,
145 text,
146 author,
147 } => (
148 PageOp::Insert {
149 container: oid,
150 before,
151 paragraphs: vec![paragraph(text)?],
152 },
153 author,
154 ),
155 Action::Outline { x, y, text, author } => {
156 let store = onestore::Store::parse(&bytes)?;
157 let index = onestore::RevisionIndex::parse(&store)?;
158 if !onestore::document::Document::parse(&index)?
159 .pages()?
160 .contains(&(sid, oid))
161 {
162 return Err("Choose the page to hold the outline".into());
163 }
164 (
165 PageOp::Add {
166 object: PageObject::Outline(Outline {
167 id: new_id()?,
168 title: false,
169 min_width: None,
170 layout: Layout {
171 x: Some(x),
172 y: Some(y),
173 ..Default::default()
174 },
175 indents: Vec::new(),
176 paragraphs: vec![paragraph(text)?],
177 unsupported: Vec::new(),
178 }),
179 before: None,
180 },
181 author,
182 )
183 }
184 };
185 let arena = Arena::default();
186 let mut section = Section::open(&arena, bytes)?;
187 let unix = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?;
188 let at = (unix.as_secs() + 11_644_473_600) * 10_000_000 + u64::from(unix.subsec_nanos() / 100);
189 section.apply(
190 &author,
191 &onestore::op::Edit {
192 at,
193 ops: vec![Op::Page { space: sid, op }],
194 },
195 )?;
196 let transaction = section.seal()?;
197 if check {
198 return Ok(json!({"ok": true}));
199 }
200 let Some(transaction) = transaction else {
201 return Ok(json!({"ok": true, "state": "Unchanged"}));
202 };
203 Ok(match fs::commit_file(&transaction, &args[1]) {
204 Ok(()) => json!({"ok": true, "state": "Committed"}),
205 Err(error) => json!({"ok": false, "state": format!("{:?}", error.state),
206 "kind": format!("{:?}", error.error.kind()), "error": error.error.to_string()}),
207 })
208}
209
210fn main() {
211 let args: Vec<_> = env::args_os().skip(1).collect();
212 let result = run(&args).unwrap_or_else(|error| {
213 let mut result = json!({"ok": false, "kind": "Input", "error": error.to_string()});
214 if args.first().is_some_and(|mode| mode == "commit") {
215 result["state"] = json!("NotCommitted");
216 }
217 result
218 });
219 println!("{result}");
220}