| 1 | //! Native editor CRDT operations. In production this crate runs ONLY inside |
| 2 | //! a fuel/memory-limited WebAssembly interpreter, never as a native decoder. |
| 3 | //! The server retains its separate ABI; both modules share the reviewed yrs deletion backports. |
| 4 | mod projection; |
| 5 | mod operations; |
| 6 | mod structure; |
| 7 | mod display; |
| 8 | mod paste; |
| 9 | mod lists; |
| 10 | mod input_rules; |
| 11 | use serde_json::{json, Value}; |
| 12 | use std::{cell::RefCell, collections::HashSet, sync::Arc}; |
| 13 | use yrs::{Doc, OffsetKind, Options, ReadTxn, StateVector, Transact, UndoManager, Update}; |
| 14 | use yrs::updates::decoder::{Decode, Decoder, DecoderV1}; |
| 15 | use yrs::updates::encoder::Encode; |
| 16 | |
| 17 | pub struct Editor { |
| 18 | doc: Doc, |
| 19 | undo: UndoManager, |
| 20 | } |
| 21 | impl Editor { |
| 22 | pub fn new(guid: &str, client_id: u64) -> Self { |
| 23 | let mut options = Options::with_guid_and_client_id(guid.into(), client_id); |
| 24 | options.offset_kind = OffsetKind::Utf16; |
| 25 | let doc = Doc::with_options(options); |
| 26 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 27 | let undo = UndoManager::with_scope_and_options(&doc, &root, yrs::undo::Options { |
| 28 | capture_timeout_millis: 0, |
| 29 | tracked_origins: HashSet::from(["native-edit".into()]), |
| 30 | capture_transaction: None, |
| 31 | timestamp: Arc::new(|| 1), |
| 32 | }); |
| 33 | Self {doc, undo} |
| 34 | } |
| 35 | pub fn apply(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, false) } |
| 36 | pub fn apply_local(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, true) } |
| 37 | fn apply_origin(&self, bytes: &[u8], local: bool) -> Result<(), &'static str> { |
| 38 | let updates = decode_updates(bytes)?; |
| 39 | let mut txn = self.doc.transact_mut_with(if local { "native-edit" } else { "remote" }); |
| 40 | for update in updates { txn.apply_update(update).map_err(|_| "invalid_update")?; } |
| 41 | Ok(()) |
| 42 | } |
| 43 | pub fn snapshot(&self) -> Vec<u8> { |
| 44 | self.doc.transact().encode_state_as_update_v1(&StateVector::default()) |
| 45 | } |
| 46 | pub fn projection(&self) -> Value { |
| 47 | let root = self.doc.get_or_insert_xml_fragment("prosemirror"); |
| 48 | let txn = self.doc.transact(); |
| 49 | json!({"children": projection::children(&root, &txn), "canUndo": !self.undo.undo_stack().is_empty(), "canRedo": !self.undo.redo_stack().is_empty()}) |
| 50 | } |
| 51 | pub fn anchor(&self, at: u32) -> Result<Value, &'static str> { display::anchor(&self.doc, at) } |
| 52 | pub fn resolve(&self, anchor: &Value) -> Result<u32, &'static str> { display::resolve(&self.doc, anchor) } |
| 53 | pub fn command(&mut self, request: &Value) -> Result<Vec<u8>, &'static str> { |
| 54 | let before = self.doc.transact().state_vector(); |
| 55 | match request.get("op").and_then(Value::as_str).ok_or("invalid_command")? { |
| 56 | "batch" => { |
| 57 | let commands = request["commands"].as_array().filter(|v| v.len() <= 1000).ok_or("invalid_command")?; |
| 58 | if commands.iter().any(|c| ["batch","undo","redo"].contains(&c["op"].as_str().unwrap_or(""))) { return Err("invalid_command"); } |
| 59 | let mut affected = HashSet::new(); |
| 60 | for command in commands { |
| 61 | let target = lists::normalization_target(&self.doc, command); |
| 62 | self.command(command)?; |
| 63 | if let Some(id) = target.and_then(|t| lists::normalized_identity(&self.doc, t)) { affected.insert(id); } |
| 64 | } |
| 65 | lists::normalize(&self.doc, &mut affected); |
| 66 | }, |
| 67 | "undo" => { self.undo.undo_blocking(); }, |
| 68 | "redo" => { self.undo.redo_blocking(); }, |
| 69 | "display_autoformat" | "display_revert_rule" | "display_exit_list" => input_rules::execute(&self.doc, request)?, |
| 70 | "list_indent" => lists::execute(&self.doc, request)?, |
| 71 | "display_paste" => paste::execute(&self.doc, request)?, |
| 72 | "display_edit" => display::execute(&self.doc, request)?, |
| 73 | "block" if ["bullet_list","ordered_list"].contains(&request["type"].as_str().unwrap_or("")) && lists::contains(&self.doc, request["node"].as_str().unwrap_or("")) => lists::toggle(&self.doc, request)?, |
| 74 | "block" | "split" | "hard_break" | "join_range" => structure::execute(&self.doc, request)?, |
| 75 | _ => operations::execute(&self.doc, request)?, |
| 76 | } |
| 77 | Ok(self.doc.transact().encode_state_as_update_v1(&before)) |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | thread_local! { |
| 82 | static EDITOR: RefCell<Option<Editor>> = const { RefCell::new(None) }; |
| 83 | static OUTPUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) }; |
| 84 | } |
| 85 | |
| 86 | // All pointers are private WASM linear-memory offsets passed by the interpreter. |
| 87 | // No API in this module may be called with native pointers by an Apple process. |
| 88 | #[cfg(target_arch = "wasm32")] |
| 89 | #[no_mangle] |
| 90 | pub extern "C" fn docs_editor_alloc(len: u32) -> u32 { |
| 91 | if len > 8 * 1024 * 1024 + 1 { return 0; } |
| 92 | Box::into_raw(vec![0u8; len as usize].into_boxed_slice()) as *mut u8 as u32 |
| 93 | } |
| 94 | #[cfg(target_arch = "wasm32")] |
| 95 | #[no_mangle] |
| 96 | pub unsafe extern "C" fn docs_editor_free(ptr: u32, len: u32) { |
| 97 | if ptr != 0 { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr as *mut u8, len as usize))); } |
| 98 | } |
| 99 | #[cfg(target_arch = "wasm32")] |
| 100 | #[no_mangle] |
| 101 | pub unsafe extern "C" fn docs_editor_call(ptr: u32, len: u32) -> i32 { |
| 102 | let end = (ptr as u64) + (len as u64); |
| 103 | if len > 8 * 1024 * 1024 + 1 || end > (core::arch::wasm32::memory_size(0) * 65536) as u64 { return -1; } |
| 104 | let bytes = std::slice::from_raw_parts(ptr as *const u8, len as usize); |
| 105 | let result = dispatch(bytes); |
| 106 | match result { |
| 107 | Ok(bytes) => { OUTPUT.with(|out| *out.borrow_mut() = bytes); 0 }, |
| 108 | Err(code) => { OUTPUT.with(|out| *out.borrow_mut() = code.as_bytes().to_vec()); -1 }, |
| 109 | } |
| 110 | } |
| 111 | #[cfg(target_arch = "wasm32")] |
| 112 | #[no_mangle] |
| 113 | pub extern "C" fn docs_editor_output_ptr() -> u32 { OUTPUT.with(|out| out.borrow().as_ptr() as u32) } |
| 114 | #[cfg(target_arch = "wasm32")] |
| 115 | #[no_mangle] |
| 116 | pub extern "C" fn docs_editor_output_len() -> u32 { OUTPUT.with(|out| out.borrow().len() as u32) } |
| 117 | |
| 118 | fn decode_updates(mut remaining: &[u8]) -> Result<Vec<Update>, &'static str> { |
| 119 | let mut updates = Vec::new(); |
| 120 | while !remaining.is_empty() { |
| 121 | let mut decoder = DecoderV1::from(remaining); |
| 122 | updates.push(Update::decode(&mut decoder).map_err(|_| "invalid_update")?); |
| 123 | let trailing = decoder.read_to_end().map_err(|_| "invalid_update")?.len(); |
| 124 | if trailing >= remaining.len() { return Err("invalid_update"); } |
| 125 | remaining = &remaining[remaining.len() - trailing..]; |
| 126 | } |
| 127 | Ok(updates) |
| 128 | } |
| 129 | |
| 130 | pub fn dispatch(bytes: &[u8]) -> Result<Vec<u8>, &'static str> { |
| 131 | // Merge pending writes without constructing a document: pending structs may |
| 132 | // depend on state already held by the server. Snapshotting an empty document |
| 133 | // would silently lose those structs and deletes. |
| 134 | if bytes.first() == Some(&253) { |
| 135 | let updates = decode_updates(&bytes[1..])?; |
| 136 | if updates.is_empty() { return Err("invalid_update"); } |
| 137 | return Ok(Update::merge_updates(updates).encode_v1()); |
| 138 | } |
| 139 | // Binary envelope avoids expanding an 8MiB log into JSON byte numbers. |
| 140 | if matches!(bytes.first(), Some(254 | 255)) { |
| 141 | return EDITOR.with(|cell| { |
| 142 | cell.borrow().as_ref().ok_or("not_open")?.apply_origin(&bytes[1..], bytes[0] == 254)?; |
| 143 | Ok(Vec::new()) |
| 144 | }); |
| 145 | } |
| 146 | let request: Value = serde_json::from_slice(bytes).map_err(|_| "invalid_command")?; |
| 147 | EDITOR.with(|cell| { |
| 148 | let mut state = cell.borrow_mut(); |
| 149 | if request["op"] == "open" { |
| 150 | let guid = request["guid"].as_str().ok_or("invalid_identity")?; |
| 151 | let client = request["client"].as_u64().filter(|v| *v > 0 && *v <= u32::MAX as u64).ok_or("invalid_identity")?; |
| 152 | *state = Some(Editor::new(guid, client)); |
| 153 | return Ok(Vec::new()); |
| 154 | } |
| 155 | let editor = state.as_mut().ok_or("not_open")?; |
| 156 | match request["op"].as_str() { |
| 157 | Some("apply") => { |
| 158 | let bytes = request["bytes"].as_array().ok_or("invalid_update")?.iter() |
| 159 | .map(|v| v.as_u64().filter(|n| *n <= 255).map(|n| n as u8).ok_or("invalid_update")) |
| 160 | .collect::<Result<Vec<_>, _>>()?; |
| 161 | editor.apply(&bytes)?; Ok(Vec::new()) |
| 162 | }, |
| 163 | Some("snapshot") => Ok(editor.snapshot()), |
| 164 | Some("anchor") => serde_json::to_vec(&editor.anchor(request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?)?).map_err(|_| "invalid_anchor"), |
| 165 | Some("resolve") => serde_json::to_vec(&editor.resolve(&request["anchor"])?).map_err(|_| "invalid_anchor"), |
| 166 | Some("clients") => serde_json::to_vec(&editor.doc.transact().state_vector().iter().map(|(id, _)| *id).collect::<Vec<_>>()).map_err(|_| "invalid_projection"), |
| 167 | Some("project") => serde_json::to_vec(&editor.projection()).map_err(|_| "invalid_projection"), |
| 168 | _ => editor.command(&request), |
| 169 | } |
| 170 | }) |
| 171 | } |
| 172 | |
| 173 | #[cfg(target_arch = "wasm32")] |
| 174 | #[no_mangle] |
| 175 | extern "Rust" fn __getrandom_v03_custom(_dest: *mut u8, _len: usize) -> Result<(), getrandom::Error> { |
| 176 | Err(getrandom::Error::UNSUPPORTED) |
| 177 | } |
| 178 | |