Skip to main content

pptx_edit/
lib.rs

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