1#[path = "../src/flush.rs"]
2mod flush;
3#[path = "support/typing.rs"]
4mod typing;
5
6use onestore::{
7 ExGuid, RevisionIndex, Store,
8 document::{Document, Kind},
9};
10use std::{
11 collections::BTreeSet,
12 env, fs,
13 io::Write,
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17fn main() -> Result<(), Box<dyn std::error::Error>> {
18 let args: Vec<_> = env::args().skip(1).collect();
19 if !(3..=4).contains(&args.len()) {
20 return Err("Usage: random_edit FILE OUTPUT|--in-place SEED [PAGE_ID]".into());
21 }
22 let started = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
23 let seed: u64 = args[2].parse()?;
24 let mut state = seed;
25 let mut next = || {
26 state = state.wrapping_add(0x9e3779b97f4a7c15);
27 let mut value = state;
28 value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
29 value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb);
30 value ^ (value >> 31)
31 };
32 let source = onestore::read_file(&args[0])?;
33 let store = Store::parse(&source)?;
34 let index = RevisionIndex::parse(&store)?;
35 index.validate_current()?;
36 let document = Document::parse(&index)?;
37 let mut candidates = Vec::new();
38 for (sid, page) in document.pages()? {
39 if args.get(3).is_some_and(|id| *id != page.to_string()) {
40 continue;
41 }
42 let space = &document.spaces[&sid];
43 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
44 let mut pending = vec![page];
45 let mut seen = BTreeSet::new();
46 while let Some(oid) = pending.pop() {
47 if !seen.insert(oid) {
48 continue;
49 }
50 let node = &revision.nodes[&oid];
51 pending.extend(
52 node.children
53 .iter()
54 .chain(&node.content)
55 .chain(&node.structure)
56 .copied(),
57 );
58 if let Kind::RichText {
59 runs,
60 boilerplate: false,
61 ..
62 } = &node.kind
63 {
64 for (run, resolved) in runs.iter().zip(revision.text_runs(oid)?) {
65 if [
66 resolved.format.hidden,
67 resolved.format.hyperlink,
68 resolved.format.math,
69 resolved.format.embedded_object,
70 ]
71 .contains(&Some(true))
72 {
73 continue;
74 }
75 candidates.push((sid, page, oid, run.start, resolved.text));
76 }
77 }
78 }
79 }
80 for i in (1..candidates.len()).rev() {
81 candidates.swap(i, (next() % (i as u64 + 1)) as usize);
82 }
83 let mut rejected = Vec::new();
84 for (sid, page, oid, offset, text) in candidates {
85 let mut boundaries = vec![0];
86 for character in text.chars() {
87 boundaries.push(boundaries.last().unwrap() + character.len_utf16() as u32);
88 }
89 let first = (next() % boundaries.len() as u64) as usize;
90 let second = (next() % boundaries.len() as u64) as usize;
91 let mode = next() % 3;
92 let start = if mode == 0 { first } else { first.min(second) };
93 let end = if mode == 0 { first } else { first.max(second) };
94 let insertion = match next() % 4 {
95 0 => " revised ",
96 1 => " café ",
97 2 => " 東京 🦀 ",
98 _ => " e\u{301} ",
99 };
100 let replacement = if mode == 1 && start != end {
101 ""
102 } else {
103 insertion
104 };
105 let end = if text
106 .chars()
107 .skip(start)
108 .take(end - start)
109 .eq(replacement.chars())
110 {
111 start
112 } else {
113 end
114 };
115 let range = offset + boundaries[start]..offset + boundaries[end];
116 let edit = typing::text(sid, oid, range.clone(), replacement);
117 let transaction = match typing::sealed(&source, "Random editor", &edit) {
118 Ok(Some(transaction)) => transaction,
119 Ok(None) => {
120 rejected.push("The edit stores nothing".into());
121 continue;
122 }
123 Err(error) => {
124 rejected.push(error.to_string());
125 continue;
126 }
127 };
128 let mut written = source.clone();
129 transaction.apply(&mut written)?;
130 let space = &document.spaces[&sid];
131 let revision = &space.revisions[&space.contexts[&ExGuid::default()]];
132 let Kind::RichText { runs, .. } = &revision.nodes[&oid].kind else {
133 unreachable!()
134 };
135 let selected = runs
136 .iter()
137 .rposition(|run| run.start <= range.start && range.end <= run.end)
138 .unwrap();
139 let actual = revision.text_runs(oid)?;
140 let mut record = serde_json::json!({"seed": seed, "source": args[0], "page": page, "space": sid,
141 "object": oid, "range": [range.start, range.end], "replacement": replacement,
142 "run_before": actual[selected].text, "run_start": runs[selected].start, "source_md5": format!("{:x}", md5::compute(&source)),
143 "started_ms": started, "rejected_candidates": rejected});
144 let failed = if args[1] == "--in-place" {
145 match transaction.commit_file(&args[0]) {
146 Ok(()) => {
147 record["state"] = "Committed".into();
148 false
149 }
150 Err(error) => {
151 record["state"] = format!("{:?}", error.state).into();
152 record["error"] = error.error.to_string().into();
153 true
154 }
155 }
156 } else {
157 let mut file = fs::OpenOptions::new()
158 .write(true)
159 .create_new(true)
160 .open(&args[1])?;
161 file.write_all(&written)?;
162 flush::flush(&file)?;
163 record["state"] = "Created".into();
164 record["output"] = args[1].clone().into();
165 false
166 };
167 record["finished_ms"] =
168 u64::try_from(SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis())?.into();
169 println!("{}", serde_json::to_string(&record)?);
170 if failed {
171 std::process::exit(2);
172 }
173 return Ok(());
174 }
175 Err(format!(
176 "No supported text edit on the selected page: {}",
177 rejected.join("; ")
178 )
179 .into())
180}