| 1 | #[path = "../src/flush.rs"] |
| 2 | mod flush; |
| 3 | #[path = "support/typing.rs"] |
| 4 | mod typing; |
| 5 | |
| 6 | use onestore::{ |
| 7 | ExGuid, RevisionIndex, Store, |
| 8 | document::{Document, Kind}, |
| 9 | }; |
| 10 | use std::{ |
| 11 | collections::BTreeSet, |
| 12 | env, fs, |
| 13 | io::Write, |
| 14 | time::{SystemTime, UNIX_EPOCH}, |
| 15 | }; |
| 16 | |
| 17 | fn 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 | } |