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