1use std::cell::RefCell;
4use std::panic::{AssertUnwindSafe, catch_unwind};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use pptx_parse::PptxPackage;
9use sha2::{Digest, Sha256};
10use yrs::updates::decoder::{Decode, Decoder, DecoderV1};
11use yrs::updates::encoder::Encode;
12use yrs::{
13 ClientID, Doc, IdSet, OffsetKind, Options, ReadTxn, StateVector, StickyIndex, Subscription,
14 Transact, Update, WriteTxn,
15};
16
17mod batch;
18mod comments;
19mod deck;
20mod effects;
21mod inherit;
22mod model;
23mod outline_gradients;
24pub mod paragraph;
25mod peer;
26mod proposal_diff;
27mod proposals;
28mod remote_cleanup;
29mod replay;
30mod save;
31mod search;
32mod source_run_properties;
33mod staging;
34mod story;
35pub mod structured;
36mod table;
37mod target;
38mod undo;
39
40pub use batch::{
41 BackgroundGuard, DocumentVersion, EditApplication, EditFailure, EditFailureCode, EditHistory,
42 EditOutcome, EditPreview, EditReceipt, EditRefusal, EditRequest, EditSource, EditStep,
43 EditTarget, EditValidation, FillGuard, MAX_REQUEST_BYTES, OutlineGuard, RectGuard, SlideTarget,
44 TargetEdge, TextGuard, ValidationOutcome, outcome_json, oversized_request,
45};
46#[doc(hidden)]
47pub use deck::baseline_snapshot;
48pub use model::*;
49pub use ooxml_drawingml::{ColorValue, GradientFill, GradientStop, ShapeFill};
50#[doc(hidden)]
51pub use peer::{PeerError, PeerFont};
52pub use proposal_diff::*;
53pub use proposals::*;
54pub use search::TextSearchMatch;
55#[doc(hidden)]
56pub use story::baseline_story;
57pub use table::{TableColumnWidth, TableEdit, TableInsertPosition, TableReceipt};
58pub use target::{
59 FindMatch, FindOutcome, FindRequest, FindResponse, FindScope, ParagraphText, ReadOutcome,
60 ReadRequest, ReadResponse, ShapeTarget, StoryTarget, StoryText, TextField, TextRange,
61 TextTarget,
62};
63pub use undo::{DeckUndoManager, UndoCaptureMode};
64
65#[cfg(feature = "wasm")]
66pub mod wasm;
67
68pub(crate) const META: &str = "pptx:meta";
69pub(crate) const SLIDE_ORDER: &str = "pptx:slide-order";
70pub(crate) const SLIDES: &str = "pptx:slides";
71pub(crate) const SHAPES: &str = "pptx:shapes";
72pub(crate) const STORIES: &str = "pptx:stories";
73pub(crate) const COMMENTS: &str = "pptx:comments";
74pub(crate) const REMOTE_ORIGIN: &str = "pptx:remote";
75pub(crate) const HYDRATE_ORIGIN: &str = "pptx:hydrate";
76pub(crate) const MIGRATE_ORIGIN: &str = "pptx:migrate";
77pub(crate) const PILCROW_KIND: &str = "pilcrow";
78pub(crate) const KIND: &str = "_kind";
79pub(crate) const PARA_ID: &str = "paraId";
80pub(crate) const BOOTSTRAP_CLIENT_ID: u64 = (1_u64 << 53) - 1;
81pub const MAX_SAFE_CLIENT_ID: u64 = BOOTSTRAP_CLIENT_ID - 1;
82pub const MAX_UPDATE_BYTES: usize = 64 * 1024 * 1024;
83const MAX_STATE_VECTOR_ENTRIES: u32 = 65_536;
84
85pub type UpdateSubscription = Subscription;
86
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct CaretAnchor {
89 story_id: String,
90 position: StickyIndex,
91}
92
93pub struct DeckSession {
94 pub(crate) doc: Doc,
95 client_id: u64,
96 id_counter: AtomicU64,
97 package: Arc<PptxPackage>,
98 undo: RefCell<DeckUndoManager>,
99 table_edits: RefCell<std::collections::HashMap<String, Vec<String>>>,
100 discarded_tables: RefCell<Vec<String>>,
101 collaborative: std::cell::Cell<bool>,
102 proposals: RefCell<proposals::ProposalStore>,
103 epoch: Arc<AtomicU64>,
106 version_nonce: AtomicU64,
108 baseline_epoch: u64,
109 replay_state: RefCell<replay::ReplayState>,
110 _epoch_observer: UpdateSubscription,
111 state_update: RefCell<Option<(u64, Arc<Vec<u8>>)>>,
112 remote_deletions: remote_cleanup::RemoteDeletions,
113}
114
115fn watch_epoch(doc: &Doc) -> EditResult<(Arc<AtomicU64>, UpdateSubscription)> {
116 let epoch = Arc::new(AtomicU64::new(0));
117 let counter = Arc::clone(&epoch);
118 let observer = doc
121 .observe_after_transaction(move |txn| {
122 if !txn.delete_set().is_empty() || txn.after_state() != txn.before_state() {
123 counter.fetch_add(1, Ordering::Relaxed);
124 }
125 })
126 .map_err(|error| EditError::Observer(error.to_string()))?;
127 Ok((epoch, observer))
128}
129
130impl DeckSession {
131 pub fn open(bytes: &[u8], client_id: u64) -> EditResult<Self> {
132 let package =
133 pptx_parse::parse_pptx(bytes).map_err(|error| EditError::Parse(error.to_string()))?;
134 Self::from_package_with_source(package, bytes, client_id)
135 }
136
137 pub fn from_package(package: PptxPackage, client_id: u64) -> EditResult<Self> {
142 let bytes = pptx_parse::write_pptx(&package)
143 .map_err(|error| EditError::Parse(error.to_string()))?;
144 let fingerprint = format!("{:x}", Sha256::digest(bytes));
145 Self::from_package_with_fingerprint(package, fingerprint, client_id)
146 }
147
148 pub fn from_package_with_source(
151 package: PptxPackage,
152 source: &[u8],
153 client_id: u64,
154 ) -> EditResult<Self> {
155 let fingerprint = format!("{:x}", Sha256::digest(source));
156 Self::from_package_with_fingerprint(package, fingerprint, client_id)
157 }
158
159 fn from_package_with_fingerprint(
160 package: PptxPackage,
161 fingerprint: String,
162 client_id: u64,
163 ) -> EditResult<Self> {
164 validate_client_id(client_id)?;
165 let bootstrap = doc_with_client_id(BOOTSTRAP_CLIENT_ID);
166 deck::seed_doc(&bootstrap, &package, &fingerprint)?;
167 let baseline = bootstrap
168 .transact()
169 .encode_state_as_update_v1(&StateVector::default());
170 let doc = session_doc(client_id);
171 hydrate_doc(&doc, &baseline)?;
172 deck::validate_doc(&doc)?;
173 Self::assemble(
174 doc,
175 client_id,
176 0,
177 Arc::new(package),
178 batch::mint_nonce(client_id, 0),
179 )
180 }
181
182 pub fn open_from_update(update: &[u8], client_id: u64) -> EditResult<Self> {
183 validate_client_id(client_id)?;
184 if update.len() > MAX_UPDATE_BYTES {
185 return Err(EditError::InvalidUpdate(format!(
186 "update exceeds {MAX_UPDATE_BYTES} bytes"
187 )));
188 }
189 let doc = session_doc(client_id);
190 hydrate_doc(&doc, update)?;
191 deck::migrate_doc(&doc)?;
192 collect_garbage(&doc, None);
193 let (package, _snapshot) = deck::validate_doc(&doc)?;
194 Self::assemble(
195 doc,
196 client_id,
197 0,
198 Arc::new(package),
199 batch::mint_nonce(client_id, 0),
200 )
201 }
202
203 pub(crate) fn assemble(
204 doc: Doc,
205 client_id: u64,
206 id_counter: u64,
207 package: Arc<PptxPackage>,
208 version_nonce: u64,
209 ) -> EditResult<Self> {
210 let undo = DeckUndoManager::new(&doc, client_id)?;
211 let (epoch, _epoch_observer) = watch_epoch(&doc)?;
212 Ok(Self {
213 doc,
214 client_id,
215 id_counter: AtomicU64::new(id_counter),
216 package,
217 undo: RefCell::new(undo),
218 table_edits: Default::default(),
219 discarded_tables: Default::default(),
220 collaborative: Default::default(),
221 proposals: Default::default(),
222 epoch,
223 version_nonce: AtomicU64::new(version_nonce),
224 baseline_epoch: 0,
225 replay_state: Default::default(),
226 _epoch_observer,
227 state_update: RefCell::new(None),
228 remote_deletions: Default::default(),
229 })
230 }
231
232 pub fn open_from_update_with_source(
236 update: &[u8],
237 source: &[u8],
238 client_id: u64,
239 ) -> EditResult<Self> {
240 let session = Self::open_from_update(update, client_id)?;
241 if !session.seeded_from(source)? {
242 return Err(EditError::Parse(
243 "source bytes do not match the fingerprint recorded in the update".to_owned(),
244 ));
245 }
246 session.attach_source(source)
247 }
248
249 pub(crate) fn seeded_from(&self, source: &[u8]) -> EditResult<bool> {
251 Ok(deck::fingerprint_from_doc(&self.doc)? == format!("{:x}", Sha256::digest(source)))
252 }
253
254 pub(crate) fn attach_source(self, source: &[u8]) -> EditResult<Self> {
256 let mut package = if self.package.models_connectors() {
257 pptx_parse::parse_pptx(source)
258 } else {
259 pptx_parse::parse_pptx_without_connectors(source)
260 }
261 .map_err(|error| EditError::Parse(error.to_string()))?;
262 let mut import = deck::SourceImport::new(self.package().clone(), &package);
263 comments::import_source_comments(&self, &mut import)?;
264 deck::import_source_render_data(&self.doc, &mut import)?;
265 source_run_properties::import_source(
266 &self,
267 &mut import,
268 source_run_properties::SourceProperty::Baseline,
269 )?;
270 deck::import_source_ole_pictures(&self.doc, import.source)?;
271 effects::import_source(&mut import);
272 for property in [
273 source_run_properties::SourceProperty::Spacing,
274 source_run_properties::SourceProperty::Kern,
275 source_run_properties::SourceProperty::Caps,
276 source_run_properties::SourceProperty::Color,
277 ] {
278 source_run_properties::import_source(&self, &mut import, property)?;
279 }
280 story::import_source_numbering_restarts(&self.doc, import.source)?;
281 outline_gradients::import_source(&self, &mut import)?;
282 import.sync_package_json(&self.doc, self.package())?;
283 effects::preserve_theme_styles(&mut package, self.package());
284 collect_garbage(&self.doc, None);
285 Ok(Self {
286 package: Arc::new(package),
287 baseline_epoch: self.epoch(),
288 ..self
289 })
290 }
291
292 pub fn client_id(&self) -> u64 {
293 self.client_id
294 }
295
296 pub fn package(&self) -> &PptxPackage {
297 &self.package
298 }
299
300 pub fn yrs_doc(&self) -> &Doc {
301 &self.doc
302 }
303
304 pub fn encode_state_vector_v1(&self) -> Vec<u8> {
305 self.doc.transact().state_vector().encode_v1()
306 }
307
308 pub fn encode_state_as_update_v1(&self) -> Vec<u8> {
309 (*self.state_update_v1()).clone()
310 }
311
312 pub(crate) fn state_update_v1(&self) -> Arc<Vec<u8>> {
313 let epoch = self.epoch();
314 if let Some((cached_epoch, update)) = &*self.state_update.borrow()
315 && *cached_epoch == epoch
316 {
317 return Arc::clone(update);
318 }
319 let update = Arc::new(
320 self.doc
321 .transact()
322 .encode_state_as_update_v1(&StateVector::default()),
323 );
324 *self.state_update.borrow_mut() = Some((epoch, Arc::clone(&update)));
325 update
326 }
327
328 pub(crate) fn epoch(&self) -> u64 {
329 self.epoch.load(Ordering::Relaxed)
330 }
331
332 pub fn version(&self) -> DocumentVersion {
338 batch::version_token(self.version_nonce.load(Ordering::Relaxed), self.epoch())
339 }
340
341 #[cfg(feature = "wasm")]
343 pub(crate) fn rotate_version(&self, entropy: u64) {
344 self.version_nonce.store(
345 batch::mint_nonce(self.client_id, entropy),
346 Ordering::Relaxed,
347 );
348 }
349
350 pub fn encode_diff_v1(&self, remote_state_vector: &[u8]) -> EditResult<Vec<u8>> {
351 let state_vector =
352 decode_state_vector_v1(remote_state_vector).map_err(EditError::InvalidStateVector)?;
353 Ok(self.doc.transact().encode_diff_v1(&state_vector))
354 }
355
356 pub fn apply_update_v1(&self, bytes: &[u8]) -> EditResult<DeckSnapshot> {
357 if bytes.len() > MAX_UPDATE_BYTES {
358 return Err(EditError::InvalidUpdate(format!(
359 "update exceeds {MAX_UPDATE_BYTES} bytes"
360 )));
361 }
362 let incoming = decode_update_v1(bytes).map_err(EditError::InvalidUpdate)?;
363 let staged = doc_with_client_id(self.client_id);
364 hydrate_doc(&staged, &self.encode_state_as_update_v1())?;
365 let carried = remote_cleanup::apply_staged_update(
366 &mut staged.transact_mut_with(REMOTE_ORIGIN),
367 incoming,
368 )
369 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
370 let (_, snapshot) = deck::validate_doc(&staged)?;
371 let watched: std::collections::BTreeSet<String> = self
372 .undo
373 .borrow()
374 .tracked_tables()
375 .chain(self.table_edits.borrow().keys())
376 .cloned()
377 .collect();
378 let logs: Vec<_> = watched
379 .iter()
380 .map(|id| (id, table::edit_log(&self.doc, id)))
381 .collect();
382
383 let diff = staged
384 .transact()
385 .encode_state_as_update_v1(&self.doc.transact().state_vector());
386 let diff = decode_update_v1(&diff).map_err(EditError::InvalidUpdate)?;
387 let mut integration = self
388 .remote_deletions
389 .integrate(self.doc.transact_mut_with(REMOTE_ORIGIN));
390 integration.carry(&carried);
391 integration
392 .apply_update(diff)
393 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
394 drop(integration);
395 for (id, before) in logs {
396 let after = table::edit_log(&self.doc, id);
397 if after != before {
398 self.undo.borrow_mut().drop_table(&self.doc, id)?;
399 self.settle_table_edits(id, &before, &after);
400 }
401 }
402 if !self.undo.borrow().ever_kept() {
403 collect_garbage(&self.doc, Some(&carried));
404 }
405 Ok(snapshot)
406 }
407
408 pub fn observe_update_v1<F>(&self, callback: F) -> EditResult<Subscription>
411 where
412 F: Fn(UpdateEvent) + 'static,
413 {
414 let remote_deletions = self.remote_deletions.clone();
415 self.doc
416 .observe_update_v1(move |txn, event| {
417 let remote = txn
418 .origin()
419 .is_some_and(|origin| origin.as_ref() == REMOTE_ORIGIN.as_bytes());
420 let origin = if remote {
421 UpdateOrigin::Remote
422 } else {
423 UpdateOrigin::Local
424 };
425 let update = UpdateEvent {
426 update: event.update.clone(),
427 origin,
428 };
429 let _ = catch_unwind(AssertUnwindSafe(|| callback(update)));
430 if !remote {
431 return;
432 }
433 if let Some(update) = remote_deletions.residual_update_v1(txn) {
434 let update = UpdateEvent {
435 update,
436 origin: UpdateOrigin::Local,
437 };
438 let _ = catch_unwind(AssertUnwindSafe(|| callback(update)));
439 }
440 })
441 .map_err(|error| EditError::Observer(error.to_string()))
442 }
443
444 pub fn undo(&self) -> bool {
445 self.undo.borrow_mut().undo()
446 }
447
448 pub fn redo(&self) -> bool {
449 self.undo.borrow_mut().redo()
450 }
451
452 pub fn clear_undo_history(&self) {
454 self.undo.borrow_mut().clear();
455 }
456
457 pub fn set_collaborative(&self) -> EditResult<()> {
460 self.collaborative.set(true);
461 let mut undo = self.undo.borrow_mut();
462 let tables: Vec<_> = undo.tracked_tables().cloned().collect();
463 for id in &tables {
464 undo.drop_table(&self.doc, id)?;
465 }
466 Ok(())
467 }
468
469 pub fn is_collaborative(&self) -> bool {
470 self.collaborative.get()
471 }
472
473 pub fn take_discarded_table_edits(&self) -> Vec<String> {
476 std::mem::take(&mut *self.discarded_tables.borrow_mut())
477 }
478
479 fn settle_table_edits(&self, shape_id: &str, before: &table::EditLog, after: &table::EditLog) {
480 let mut edits = self.table_edits.borrow_mut();
481 let Some(local) = edits.get_mut(shape_id) else {
482 return;
483 };
484 let lineage = after.lineage();
485 let count = local.len();
486 local.retain(|generation| {
487 !before.edits.contains_key(generation) || lineage.contains(generation.as_str())
488 });
489 if local.len() < count {
490 let mut discarded = self.discarded_tables.borrow_mut();
491 if !discarded.iter().any(|id| id == shape_id) {
492 discarded.push(shape_id.to_owned());
493 }
494 }
495 }
496
497 pub fn can_undo(&self) -> bool {
498 self.undo.borrow().can_undo()
499 }
500
501 pub fn can_redo(&self) -> bool {
502 self.undo.borrow().can_redo()
503 }
504
505 pub fn undo_capture_mode(&self) -> UndoCaptureMode {
506 self.undo.borrow().capture_mode()
507 }
508
509 pub fn set_undo_capture_mode(&self, mode: UndoCaptureMode) {
510 self.undo.borrow_mut().set_capture_mode(mode);
511 }
512
513 pub(crate) fn automatic_undo_barrier(&self) {
514 if self.undo_capture_mode() == UndoCaptureMode::Auto {
515 self.add_undo_barrier();
516 }
517 }
518
519 pub fn add_undo_barrier(&self) {
520 self.undo.borrow_mut().add_undo_barrier();
521 }
522
523 pub(crate) fn transact_for(&self, context: &EditCtx) -> yrs::TransactionMut<'_> {
524 match context.origin {
525 EditOrigin::Local => self.doc.transact_mut_with(self.client_id),
526 EditOrigin::Agent => self.doc.transact_mut_with("pptx:agent"),
527 EditOrigin::Remote => self.doc.transact_mut_with(REMOTE_ORIGIN),
528 EditOrigin::System => self.doc.transact_mut_with("pptx:system"),
529 }
530 }
531
532 pub(crate) fn next_id(&self, prefix: &str) -> String {
533 let counter = self.id_counter.fetch_add(1, Ordering::Relaxed);
534 format!("{prefix}:{}:{counter}", self.client_id)
535 }
536}
537
538pub(crate) fn doc_with_client_id(client_id: u64) -> Doc {
539 Doc::with_options(doc_options(client_id))
540}
541
542pub(crate) fn session_doc(client_id: u64) -> Doc {
544 let mut options = doc_options(client_id);
545 options.skip_gc = true;
546 Doc::with_options(options)
547}
548
549fn collect_garbage(doc: &Doc, scope: Option<&IdSet>) {
550 doc.transact_mut_with(HYDRATE_ORIGIN).gc(scope);
551}
552
553fn doc_options(client_id: u64) -> Options {
554 let mut options = Options::with_client_id(ClientID::new(client_id));
555 options.offset_kind = OffsetKind::Utf16;
556 options
557}
558
559fn validate_client_id(client_id: u64) -> EditResult<()> {
560 if client_id == 0 || client_id > MAX_SAFE_CLIENT_ID {
561 return Err(EditError::InvalidClientId(client_id));
562 }
563 Ok(())
564}
565
566fn hydrate_doc(doc: &Doc, bytes: &[u8]) -> EditResult<()> {
567 let update = decode_update_v1(bytes).map_err(EditError::InvalidUpdate)?;
568 let mut txn = doc.transact_mut_with(HYDRATE_ORIGIN);
569 txn.apply_update(update)
570 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
571 txn.get_or_insert_array(SLIDE_ORDER);
574 for root in [META, SLIDES, SHAPES, STORIES, COMMENTS] {
575 txn.get_or_insert_map(root);
576 }
577 Ok(())
578}
579
580fn decode_update_v1(bytes: &[u8]) -> Result<Update, String> {
581 let mut decoder = DecoderV1::from(bytes);
582 let update = Update::decode(&mut decoder).map_err(|error| error.to_string())?;
583 if !decoder
584 .read_to_end()
585 .map_err(|error| error.to_string())?
586 .is_empty()
587 {
588 return Err("update contains trailing bytes".to_owned());
589 }
590 Ok(update)
591}
592
593fn decode_state_vector_v1(bytes: &[u8]) -> Result<StateVector, String> {
594 validate_state_vector_entry_count(bytes)?;
595 let mut decoder = DecoderV1::from(bytes);
596 let state_vector = StateVector::decode(&mut decoder).map_err(|error| error.to_string())?;
597 if !decoder
598 .read_to_end()
599 .map_err(|error| error.to_string())?
600 .is_empty()
601 {
602 return Err("state vector contains trailing bytes".to_owned());
603 }
604 Ok(state_vector)
605}
606
607fn validate_state_vector_entry_count(bytes: &[u8]) -> Result<(), String> {
608 let Some((&first, _)) = bytes.split_first() else {
609 return Err("state vector is empty".to_owned());
610 };
611 let mut value = u32::from(first & 0x7f);
612 let mut shift = 7;
613 let mut used = 1;
614 let mut byte = first;
615 while byte & 0x80 != 0 {
616 if used == 5 || used >= bytes.len() {
617 return Err("invalid state vector entry count".to_owned());
618 }
619 byte = bytes[used];
620 if used == 4 && byte > 0x0f {
621 return Err("invalid state vector entry count".to_owned());
622 }
623 value |= u32::from(byte & 0x7f) << shift;
624 shift += 7;
625 used += 1;
626 }
627 if value > MAX_STATE_VECTOR_ENTRIES {
628 return Err(format!(
629 "state vector contains {value} entries, exceeds the {MAX_STATE_VECTOR_ENTRIES}-entry limit"
630 ));
631 }
632 if value as usize > bytes.len().saturating_sub(used) / 2 {
633 return Err("state vector entry count exceeds its payload".to_owned());
634 }
635 Ok(())
636}