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, OffsetKind, Options, ReadTxn, StateVector, StickyIndex, Subscription, Transact,
14 Update, WriteTxn,
15};
16
17mod comments;
18mod deck;
19mod effects;
20mod model;
21mod outline_gradients;
22mod proposal_diff;
23mod proposals;
24mod save;
25mod source_run_properties;
26mod story;
27mod undo;
28
29pub use model::*;
30pub use proposal_diff::*;
31pub use proposals::*;
32pub use undo::DeckUndoManager;
33
34#[cfg(feature = "wasm")]
35pub mod wasm;
36
37pub(crate) const META: &str = "pptx:meta";
38pub(crate) const SLIDE_ORDER: &str = "pptx:slide-order";
39pub(crate) const SLIDES: &str = "pptx:slides";
40pub(crate) const SHAPES: &str = "pptx:shapes";
41pub(crate) const STORIES: &str = "pptx:stories";
42pub(crate) const COMMENTS: &str = "pptx:comments";
43pub(crate) const REMOTE_ORIGIN: &str = "pptx:remote";
44pub(crate) const HYDRATE_ORIGIN: &str = "pptx:hydrate";
45pub(crate) const MIGRATE_ORIGIN: &str = "pptx:migrate";
46pub(crate) const PILCROW_KIND: &str = "pilcrow";
47pub(crate) const KIND: &str = "_kind";
48pub(crate) const PARA_ID: &str = "paraId";
49pub(crate) const BOOTSTRAP_CLIENT_ID: u64 = (1_u64 << 53) - 1;
50pub const MAX_SAFE_CLIENT_ID: u64 = BOOTSTRAP_CLIENT_ID - 1;
51pub const MAX_UPDATE_BYTES: usize = 64 * 1024 * 1024;
52const MAX_STATE_VECTOR_ENTRIES: u32 = 65_536;
53
54pub type UpdateSubscription = Subscription;
55
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct CaretAnchor {
58 story_id: String,
59 position: StickyIndex,
60}
61
62pub struct DeckSession {
63 pub(crate) doc: Doc,
64 client_id: u64,
65 id_counter: AtomicU64,
66 package: Arc<PptxPackage>,
67 undo: RefCell<DeckUndoManager>,
68 proposals: RefCell<proposals::ProposalStore>,
69}
70
71impl DeckSession {
72 pub fn open(bytes: &[u8], client_id: u64) -> EditResult<Self> {
73 let package =
74 pptx_parse::parse_pptx(bytes).map_err(|error| EditError::Parse(error.to_string()))?;
75 Self::from_package_with_source(package, bytes, client_id)
76 }
77
78 pub fn from_package(package: PptxPackage, client_id: u64) -> EditResult<Self> {
83 let bytes = pptx_parse::write_pptx(&package)
84 .map_err(|error| EditError::Parse(error.to_string()))?;
85 let fingerprint = format!("{:x}", Sha256::digest(bytes));
86 Self::from_package_with_fingerprint(package, fingerprint, client_id)
87 }
88
89 pub fn from_package_with_source(
92 package: PptxPackage,
93 source: &[u8],
94 client_id: u64,
95 ) -> EditResult<Self> {
96 let fingerprint = format!("{:x}", Sha256::digest(source));
97 Self::from_package_with_fingerprint(package, fingerprint, client_id)
98 }
99
100 fn from_package_with_fingerprint(
101 package: PptxPackage,
102 fingerprint: String,
103 client_id: u64,
104 ) -> EditResult<Self> {
105 validate_client_id(client_id)?;
106 let bootstrap = doc_with_client_id(BOOTSTRAP_CLIENT_ID);
107 deck::seed_doc(&bootstrap, &package, &fingerprint)?;
108 let baseline = bootstrap
109 .transact()
110 .encode_state_as_update_v1(&StateVector::default());
111 let doc = doc_with_client_id(client_id);
112 hydrate_doc(&doc, &baseline)?;
113 deck::validate_doc(&doc)?;
114 let undo = DeckUndoManager::new(&doc, client_id)?;
115 Ok(Self {
116 doc,
117 client_id,
118 id_counter: AtomicU64::new(0),
119 package: Arc::new(package),
120 undo: RefCell::new(undo),
121 proposals: Default::default(),
122 })
123 }
124
125 pub fn open_from_update(update: &[u8], client_id: u64) -> EditResult<Self> {
126 validate_client_id(client_id)?;
127 if update.len() > MAX_UPDATE_BYTES {
128 return Err(EditError::InvalidUpdate(format!(
129 "update exceeds {MAX_UPDATE_BYTES} bytes"
130 )));
131 }
132 let doc = doc_with_client_id(client_id);
133 hydrate_doc(&doc, update)?;
134 deck::migrate_doc(&doc)?;
135 deck::validate_doc(&doc)?;
136 let package = deck::package_from_doc(&doc)?;
137 let undo = DeckUndoManager::new(&doc, client_id)?;
138 Ok(Self {
139 doc,
140 client_id,
141 id_counter: AtomicU64::new(0),
142 package: Arc::new(package),
143 undo: RefCell::new(undo),
144 proposals: Default::default(),
145 })
146 }
147
148 pub fn open_from_update_with_source(
152 update: &[u8],
153 source: &[u8],
154 client_id: u64,
155 ) -> EditResult<Self> {
156 let session = Self::open_from_update(update, client_id)?;
157 let recorded = deck::fingerprint_from_doc(&session.doc)?;
158 let actual = format!("{:x}", Sha256::digest(source));
159 if recorded != actual {
160 return Err(EditError::Parse(
161 "source bytes do not match the fingerprint recorded in the update".to_owned(),
162 ));
163 }
164 let package = if session.package.models_connectors() {
165 pptx_parse::parse_pptx(source)
166 } else {
167 pptx_parse::parse_pptx_without_connectors(source)
168 }
169 .map_err(|error| EditError::Parse(error.to_string()))?;
170 comments::import_source_comments(&session, &package)?;
171 deck::import_source_render_data(&session.doc, &package)?;
172 source_run_properties::import_source(
173 &session,
174 &package,
175 source_run_properties::SourceProperty::Baseline,
176 )?;
177 deck::import_source_ole_pictures(&session.doc, &package)?;
178 effects::import_source(&session.doc, &package)?;
179 source_run_properties::import_source(
180 &session,
181 &package,
182 source_run_properties::SourceProperty::Spacing,
183 )?;
184 story::import_source_numbering_restarts(&session.doc, &package)?;
185 outline_gradients::import_source(&session, &package)?;
186 Ok(Self {
187 package: Arc::new(package),
188 ..session
189 })
190 }
191
192 pub fn client_id(&self) -> u64 {
193 self.client_id
194 }
195
196 pub fn package(&self) -> &PptxPackage {
197 &self.package
198 }
199
200 pub fn yrs_doc(&self) -> &Doc {
201 &self.doc
202 }
203
204 pub fn encode_state_vector_v1(&self) -> Vec<u8> {
205 self.doc.transact().state_vector().encode_v1()
206 }
207
208 pub fn encode_state_as_update_v1(&self) -> Vec<u8> {
209 self.doc
210 .transact()
211 .encode_state_as_update_v1(&StateVector::default())
212 }
213
214 pub fn encode_diff_v1(&self, remote_state_vector: &[u8]) -> EditResult<Vec<u8>> {
215 let state_vector =
216 decode_state_vector_v1(remote_state_vector).map_err(EditError::InvalidStateVector)?;
217 Ok(self.doc.transact().encode_diff_v1(&state_vector))
218 }
219
220 pub fn apply_update_v1(&self, bytes: &[u8]) -> EditResult<DeckSnapshot> {
221 if bytes.len() > MAX_UPDATE_BYTES {
222 return Err(EditError::InvalidUpdate(format!(
223 "update exceeds {MAX_UPDATE_BYTES} bytes"
224 )));
225 }
226 let incoming = decode_update_v1(bytes).map_err(EditError::InvalidUpdate)?;
227 let staged = doc_with_client_id(self.client_id);
228 hydrate_doc(&staged, &self.encode_state_as_update_v1())?;
229 staged
230 .transact_mut_with(REMOTE_ORIGIN)
231 .apply_update(incoming)
232 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
233 deck::validate_doc(&staged)?;
234
235 let incoming = decode_update_v1(bytes).map_err(EditError::InvalidUpdate)?;
236 self.doc
237 .transact_mut_with(REMOTE_ORIGIN)
238 .apply_update(incoming)
239 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
240 self.snapshot()
241 }
242
243 pub fn observe_update_v1<F>(&self, callback: F) -> EditResult<Subscription>
244 where
245 F: Fn(UpdateEvent) + 'static,
246 {
247 self.doc
248 .observe_update_v1(move |txn, event| {
249 let origin = if txn
250 .origin()
251 .is_some_and(|origin| origin.as_ref() == REMOTE_ORIGIN.as_bytes())
252 {
253 UpdateOrigin::Remote
254 } else {
255 UpdateOrigin::Local
256 };
257 let update = UpdateEvent {
258 update: event.update.clone(),
259 origin,
260 };
261 let _ = catch_unwind(AssertUnwindSafe(|| callback(update)));
262 })
263 .map_err(|error| EditError::Observer(error.to_string()))
264 }
265
266 pub fn undo(&self) -> bool {
267 self.undo.borrow_mut().undo()
268 }
269
270 pub fn redo(&self) -> bool {
271 self.undo.borrow_mut().redo()
272 }
273
274 pub fn can_undo(&self) -> bool {
275 self.undo.borrow().can_undo()
276 }
277
278 pub fn can_redo(&self) -> bool {
279 self.undo.borrow().can_redo()
280 }
281
282 pub fn add_undo_barrier(&self) {
283 self.undo.borrow_mut().add_undo_barrier();
284 }
285
286 pub(crate) fn transact_for(&self, context: &EditCtx) -> yrs::TransactionMut<'_> {
287 match context.origin {
288 EditOrigin::Local => self.doc.transact_mut_with(self.client_id),
289 EditOrigin::Agent => self.doc.transact_mut_with("pptx:agent"),
290 EditOrigin::Remote => self.doc.transact_mut_with(REMOTE_ORIGIN),
291 EditOrigin::System => self.doc.transact_mut_with("pptx:system"),
292 }
293 }
294
295 pub(crate) fn next_id(&self, prefix: &str) -> String {
296 let counter = self.id_counter.fetch_add(1, Ordering::Relaxed);
297 format!("{prefix}:{}:{counter}", self.client_id)
298 }
299}
300
301pub(crate) fn doc_with_client_id(client_id: u64) -> Doc {
302 let mut options = Options::with_client_id(ClientID::new(client_id));
303 options.offset_kind = OffsetKind::Utf16;
304 Doc::with_options(options)
305}
306
307fn validate_client_id(client_id: u64) -> EditResult<()> {
308 if client_id == 0 || client_id > MAX_SAFE_CLIENT_ID {
309 return Err(EditError::InvalidClientId(client_id));
310 }
311 Ok(())
312}
313
314fn hydrate_doc(doc: &Doc, bytes: &[u8]) -> EditResult<()> {
315 let update = decode_update_v1(bytes).map_err(EditError::InvalidUpdate)?;
316 let mut txn = doc.transact_mut_with(HYDRATE_ORIGIN);
317 txn.apply_update(update)
318 .map_err(|error| EditError::InvalidUpdate(error.to_string()))?;
319 txn.get_or_insert_array(SLIDE_ORDER);
322 for root in [META, SLIDES, SHAPES, STORIES, COMMENTS] {
323 txn.get_or_insert_map(root);
324 }
325 Ok(())
326}
327
328fn decode_update_v1(bytes: &[u8]) -> Result<Update, String> {
329 let mut decoder = DecoderV1::from(bytes);
330 let update = Update::decode(&mut decoder).map_err(|error| error.to_string())?;
331 if !decoder
332 .read_to_end()
333 .map_err(|error| error.to_string())?
334 .is_empty()
335 {
336 return Err("update contains trailing bytes".to_owned());
337 }
338 Ok(update)
339}
340
341fn decode_state_vector_v1(bytes: &[u8]) -> Result<StateVector, String> {
342 validate_state_vector_entry_count(bytes)?;
343 let mut decoder = DecoderV1::from(bytes);
344 let state_vector = StateVector::decode(&mut decoder).map_err(|error| error.to_string())?;
345 if !decoder
346 .read_to_end()
347 .map_err(|error| error.to_string())?
348 .is_empty()
349 {
350 return Err("state vector contains trailing bytes".to_owned());
351 }
352 Ok(state_vector)
353}
354
355fn validate_state_vector_entry_count(bytes: &[u8]) -> Result<(), String> {
356 let Some((&first, _)) = bytes.split_first() else {
357 return Err("state vector is empty".to_owned());
358 };
359 let mut value = u32::from(first & 0x7f);
360 let mut shift = 7;
361 let mut used = 1;
362 let mut byte = first;
363 while byte & 0x80 != 0 {
364 if used == 5 || used >= bytes.len() {
365 return Err("invalid state vector entry count".to_owned());
366 }
367 byte = bytes[used];
368 if used == 4 && byte > 0x0f {
369 return Err("invalid state vector entry count".to_owned());
370 }
371 value |= u32::from(byte & 0x7f) << shift;
372 shift += 7;
373 used += 1;
374 }
375 if value > MAX_STATE_VECTOR_ENTRIES {
376 return Err(format!(
377 "state vector contains {value} entries, exceeds the {MAX_STATE_VECTOR_ENTRIES}-entry limit"
378 ));
379 }
380 if value as usize > bytes.len().saturating_sub(used) / 2 {
381 return Err("state vector entry count exceeds its payload".to_owned());
382 }
383 Ok(())
384}