//! Native editor CRDT operations. In production this crate runs ONLY inside //! a fuel/memory-limited WebAssembly interpreter, never as a native decoder. //! The server retains its separate ABI; both modules share the reviewed yrs deletion backports. mod projection; mod operations; mod structure; mod display; mod paste; mod lists; mod input_rules; use serde_json::{json, Value}; use std::{cell::RefCell, collections::HashSet, sync::Arc}; use yrs::{Doc, OffsetKind, Options, ReadTxn, StateVector, Transact, UndoManager, Update}; use yrs::updates::decoder::{Decode, Decoder, DecoderV1}; use yrs::updates::encoder::Encode; pub struct Editor { doc: Doc, undo: UndoManager, } impl Editor { pub fn new(guid: &str, client_id: u64) -> Self { let mut options = Options::with_guid_and_client_id(guid.into(), client_id); options.offset_kind = OffsetKind::Utf16; let doc = Doc::with_options(options); let root = doc.get_or_insert_xml_fragment("prosemirror"); let undo = UndoManager::with_scope_and_options(&doc, &root, yrs::undo::Options { capture_timeout_millis: 0, tracked_origins: HashSet::from(["native-edit".into()]), capture_transaction: None, timestamp: Arc::new(|| 1), }); Self {doc, undo} } pub fn apply(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, false) } pub fn apply_local(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, true) } fn apply_origin(&self, bytes: &[u8], local: bool) -> Result<(), &'static str> { let updates = decode_updates(bytes)?; let mut txn = self.doc.transact_mut_with(if local { "native-edit" } else { "remote" }); for update in updates { txn.apply_update(update).map_err(|_| "invalid_update")?; } Ok(()) } pub fn snapshot(&self) -> Vec { self.doc.transact().encode_state_as_update_v1(&StateVector::default()) } pub fn projection(&self) -> Value { let root = self.doc.get_or_insert_xml_fragment("prosemirror"); let txn = self.doc.transact(); json!({"children": projection::children(&root, &txn), "canUndo": !self.undo.undo_stack().is_empty(), "canRedo": !self.undo.redo_stack().is_empty()}) } pub fn anchor(&self, at: u32) -> Result { display::anchor(&self.doc, at) } pub fn resolve(&self, anchor: &Value) -> Result { display::resolve(&self.doc, anchor) } pub fn command(&mut self, request: &Value) -> Result, &'static str> { let before = self.doc.transact().state_vector(); match request.get("op").and_then(Value::as_str).ok_or("invalid_command")? { "batch" => { let commands = request["commands"].as_array().filter(|v| v.len() <= 1000).ok_or("invalid_command")?; if commands.iter().any(|c| ["batch","undo","redo"].contains(&c["op"].as_str().unwrap_or(""))) { return Err("invalid_command"); } let mut affected = HashSet::new(); for command in commands { let target = lists::normalization_target(&self.doc, command); self.command(command)?; if let Some(id) = target.and_then(|t| lists::normalized_identity(&self.doc, t)) { affected.insert(id); } } lists::normalize(&self.doc, &mut affected); }, "undo" => { self.undo.undo_blocking(); }, "redo" => { self.undo.redo_blocking(); }, "display_autoformat" | "display_revert_rule" | "display_exit_list" => input_rules::execute(&self.doc, request)?, "list_indent" => lists::execute(&self.doc, request)?, "display_paste" => paste::execute(&self.doc, request)?, "display_edit" => display::execute(&self.doc, request)?, "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)?, "block" | "split" | "hard_break" | "join_range" => structure::execute(&self.doc, request)?, _ => operations::execute(&self.doc, request)?, } Ok(self.doc.transact().encode_state_as_update_v1(&before)) } } thread_local! { static EDITOR: RefCell> = const { RefCell::new(None) }; static OUTPUT: RefCell> = const { RefCell::new(Vec::new()) }; } // All pointers are private WASM linear-memory offsets passed by the interpreter. // No API in this module may be called with native pointers by an Apple process. #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn docs_editor_alloc(len: u32) -> u32 { if len > 8 * 1024 * 1024 + 1 { return 0; } Box::into_raw(vec![0u8; len as usize].into_boxed_slice()) as *mut u8 as u32 } #[cfg(target_arch = "wasm32")] #[no_mangle] pub unsafe extern "C" fn docs_editor_free(ptr: u32, len: u32) { if ptr != 0 { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr as *mut u8, len as usize))); } } #[cfg(target_arch = "wasm32")] #[no_mangle] pub unsafe extern "C" fn docs_editor_call(ptr: u32, len: u32) -> i32 { let end = (ptr as u64) + (len as u64); if len > 8 * 1024 * 1024 + 1 || end > (core::arch::wasm32::memory_size(0) * 65536) as u64 { return -1; } let bytes = std::slice::from_raw_parts(ptr as *const u8, len as usize); let result = dispatch(bytes); match result { Ok(bytes) => { OUTPUT.with(|out| *out.borrow_mut() = bytes); 0 }, Err(code) => { OUTPUT.with(|out| *out.borrow_mut() = code.as_bytes().to_vec()); -1 }, } } #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn docs_editor_output_ptr() -> u32 { OUTPUT.with(|out| out.borrow().as_ptr() as u32) } #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn docs_editor_output_len() -> u32 { OUTPUT.with(|out| out.borrow().len() as u32) } fn decode_updates(mut remaining: &[u8]) -> Result, &'static str> { let mut updates = Vec::new(); while !remaining.is_empty() { let mut decoder = DecoderV1::from(remaining); updates.push(Update::decode(&mut decoder).map_err(|_| "invalid_update")?); let trailing = decoder.read_to_end().map_err(|_| "invalid_update")?.len(); if trailing >= remaining.len() { return Err("invalid_update"); } remaining = &remaining[remaining.len() - trailing..]; } Ok(updates) } pub fn dispatch(bytes: &[u8]) -> Result, &'static str> { // Merge pending writes without constructing a document: pending structs may // depend on state already held by the server. Snapshotting an empty document // would silently lose those structs and deletes. if bytes.first() == Some(&253) { let updates = decode_updates(&bytes[1..])?; if updates.is_empty() { return Err("invalid_update"); } return Ok(Update::merge_updates(updates).encode_v1()); } // Binary envelope avoids expanding an 8MiB log into JSON byte numbers. if matches!(bytes.first(), Some(254 | 255)) { return EDITOR.with(|cell| { cell.borrow().as_ref().ok_or("not_open")?.apply_origin(&bytes[1..], bytes[0] == 254)?; Ok(Vec::new()) }); } let request: Value = serde_json::from_slice(bytes).map_err(|_| "invalid_command")?; EDITOR.with(|cell| { let mut state = cell.borrow_mut(); if request["op"] == "open" { let guid = request["guid"].as_str().ok_or("invalid_identity")?; let client = request["client"].as_u64().filter(|v| *v > 0 && *v <= u32::MAX as u64).ok_or("invalid_identity")?; *state = Some(Editor::new(guid, client)); return Ok(Vec::new()); } let editor = state.as_mut().ok_or("not_open")?; match request["op"].as_str() { Some("apply") => { let bytes = request["bytes"].as_array().ok_or("invalid_update")?.iter() .map(|v| v.as_u64().filter(|n| *n <= 255).map(|n| n as u8).ok_or("invalid_update")) .collect::, _>>()?; editor.apply(&bytes)?; Ok(Vec::new()) }, Some("snapshot") => Ok(editor.snapshot()), 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"), Some("resolve") => serde_json::to_vec(&editor.resolve(&request["anchor"])?).map_err(|_| "invalid_anchor"), Some("clients") => serde_json::to_vec(&editor.doc.transact().state_vector().iter().map(|(id, _)| *id).collect::>()).map_err(|_| "invalid_projection"), Some("project") => serde_json::to_vec(&editor.projection()).map_err(|_| "invalid_projection"), _ => editor.command(&request), } }) } #[cfg(target_arch = "wasm32")] #[no_mangle] extern "Rust" fn __getrandom_v03_custom(_dest: *mut u8, _len: usize) -> Result<(), getrandom::Error> { Err(getrandom::Error::UNSUPPORTED) }