Skip to main content

justpdf_core/
journal.rs

1//! Journal / undo-redo system for tracking document modifications (Phase 7, §7.10).
2//!
3//! This module provides an application-level undo/redo journal that records
4//! [`Operation`]s performed on a PDF document.  The caller is responsible for
5//! actually applying the inverse operations; the journal merely tracks state.
6
7use crate::object::{PdfDict, PdfObject};
8
9// ---------------------------------------------------------------------------
10// Operation
11// ---------------------------------------------------------------------------
12
13/// A single recorded operation that can be undone.
14#[derive(Debug, Clone)]
15pub enum Operation {
16    /// An object was added (stores the obj_num for undo = delete).
17    AddObject { obj_num: u32 },
18    /// An object was modified (stores old value for undo).
19    ModifyObject { obj_num: u32, old_value: PdfObject },
20    /// An object was deleted (stores old value for undo = re-add).
21    DeleteObject { obj_num: u32, old_value: PdfObject },
22    /// A batch of operations grouped as one undoable action.
23    Batch {
24        ops: Vec<Operation>,
25        description: String,
26    },
27}
28
29// ---------------------------------------------------------------------------
30// Journal
31// ---------------------------------------------------------------------------
32
33/// A journal that records operations for undo/redo support.
34#[derive(Debug)]
35pub struct Journal {
36    /// Stack of past operations (most recent last).
37    undo_stack: Vec<Operation>,
38    /// Stack of undone operations (for redo).
39    redo_stack: Vec<Operation>,
40    /// Whether recording is enabled.
41    recording: bool,
42}
43
44impl Journal {
45    /// Create a new empty journal with recording enabled.
46    pub fn new() -> Self {
47        Self {
48            undo_stack: Vec::new(),
49            redo_stack: Vec::new(),
50            recording: true,
51        }
52    }
53
54    /// Start recording operations.
55    pub fn start_recording(&mut self) {
56        self.recording = true;
57    }
58
59    /// Stop recording operations.
60    pub fn stop_recording(&mut self) {
61        self.recording = false;
62    }
63
64    /// Whether recording is active.
65    pub fn is_recording(&self) -> bool {
66        self.recording
67    }
68
69    /// Record an operation. Clears the redo stack.
70    pub fn record(&mut self, op: Operation) {
71        if !self.recording {
72            return;
73        }
74        self.redo_stack.clear();
75        self.undo_stack.push(op);
76    }
77
78    /// Whether undo is available.
79    pub fn can_undo(&self) -> bool {
80        !self.undo_stack.is_empty()
81    }
82
83    /// Whether redo is available.
84    pub fn can_redo(&self) -> bool {
85        !self.redo_stack.is_empty()
86    }
87
88    /// Pop the last operation for undoing. Returns the operation to undo.
89    /// The caller is responsible for applying the inverse.
90    pub fn undo(&mut self) -> Option<Operation> {
91        let op = self.undo_stack.pop()?;
92        self.redo_stack.push(op.clone());
93        Some(op)
94    }
95
96    /// Pop the last undone operation for redoing.
97    pub fn redo(&mut self) -> Option<Operation> {
98        let op = self.redo_stack.pop()?;
99        self.undo_stack.push(op.clone());
100        Some(op)
101    }
102
103    /// Get the inverse of an operation (what to do to undo it).
104    pub fn inverse(op: &Operation) -> Operation {
105        match op {
106            Operation::AddObject { obj_num } => Operation::DeleteObject {
107                obj_num: *obj_num,
108                old_value: PdfObject::Null,
109            },
110            Operation::ModifyObject { obj_num, old_value } => Operation::ModifyObject {
111                obj_num: *obj_num,
112                old_value: old_value.clone(),
113            },
114            Operation::DeleteObject { obj_num, old_value: _ } => Operation::AddObject {
115                obj_num: *obj_num,
116            },
117            Operation::Batch { ops, description } => Operation::Batch {
118                ops: ops.iter().rev().map(Journal::inverse).collect(),
119                description: format!("Undo: {}", description),
120            },
121        }
122    }
123
124    /// Number of undo steps available.
125    pub fn undo_count(&self) -> usize {
126        self.undo_stack.len()
127    }
128
129    /// Number of redo steps available.
130    pub fn redo_count(&self) -> usize {
131        self.redo_stack.len()
132    }
133
134    /// Clear all history.
135    pub fn clear(&mut self) {
136        self.undo_stack.clear();
137        self.redo_stack.clear();
138    }
139
140    /// Begin a batch operation. Returns a [`BatchBuilder`].
141    pub fn begin_batch(&mut self, description: &str) -> BatchBuilder<'_> {
142        BatchBuilder {
143            journal: self,
144            ops: Vec::new(),
145            description: description.to_string(),
146        }
147    }
148
149    /// Serialize journal to bytes (for persistence).
150    ///
151    /// Binary format:
152    /// - 4 bytes: magic `JRNL`
153    /// - 4 bytes: version (1) as little-endian u32
154    /// - 4 bytes: number of undo entries (LE u32)
155    /// - For each entry: serialized [`Operation`]
156    /// - 4 bytes: number of redo entries (LE u32)
157    /// - For each entry: serialized [`Operation`]
158    pub fn to_bytes(&self) -> Vec<u8> {
159        let mut buf = Vec::new();
160        // Magic
161        buf.extend_from_slice(b"JRNL");
162        // Version
163        buf.extend_from_slice(&1u32.to_le_bytes());
164        // Undo stack
165        buf.extend_from_slice(&(self.undo_stack.len() as u32).to_le_bytes());
166        for op in &self.undo_stack {
167            serialize_operation(&mut buf, op);
168        }
169        // Redo stack
170        buf.extend_from_slice(&(self.redo_stack.len() as u32).to_le_bytes());
171        for op in &self.redo_stack {
172            serialize_operation(&mut buf, op);
173        }
174        buf
175    }
176
177    /// Deserialize journal from bytes.
178    pub fn from_bytes(data: &[u8]) -> Option<Self> {
179        let mut cursor = Cursor::new(data);
180        // Magic
181        let magic = cursor.read_bytes(4)?;
182        if magic != b"JRNL" {
183            return None;
184        }
185        // Version
186        let version = cursor.read_u32()?;
187        if version != 1 {
188            return None;
189        }
190        // Undo stack
191        let undo_count = cursor.read_u32()? as usize;
192        let mut undo_stack = Vec::with_capacity(undo_count);
193        for _ in 0..undo_count {
194            undo_stack.push(deserialize_operation(&mut cursor)?);
195        }
196        // Redo stack
197        let redo_count = cursor.read_u32()? as usize;
198        let mut redo_stack = Vec::with_capacity(redo_count);
199        for _ in 0..redo_count {
200            redo_stack.push(deserialize_operation(&mut cursor)?);
201        }
202        Some(Self {
203            undo_stack,
204            redo_stack,
205            recording: true,
206        })
207    }
208}
209
210impl Default for Journal {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216// ---------------------------------------------------------------------------
217// BatchBuilder
218// ---------------------------------------------------------------------------
219
220/// Builder for batch operations.
221pub struct BatchBuilder<'a> {
222    journal: &'a mut Journal,
223    ops: Vec<Operation>,
224    description: String,
225}
226
227impl<'a> BatchBuilder<'a> {
228    /// Record an operation into this batch.
229    pub fn record(&mut self, op: Operation) {
230        self.ops.push(op);
231    }
232
233    /// Commit the batch to the journal.
234    pub fn commit(self) {
235        if self.ops.is_empty() {
236            return;
237        }
238        let batch = Operation::Batch {
239            ops: self.ops,
240            description: self.description,
241        };
242        self.journal.record(batch);
243    }
244
245    /// Cancel the batch (discard all recorded ops).
246    pub fn cancel(self) {
247        // Simply drop self without committing.
248    }
249}
250
251// ---------------------------------------------------------------------------
252// Binary serialization helpers
253// ---------------------------------------------------------------------------
254
255/// Tag bytes for [`Operation`] variants.
256const TAG_ADD: u8 = 1;
257const TAG_MODIFY: u8 = 2;
258const TAG_DELETE: u8 = 3;
259const TAG_BATCH: u8 = 4;
260
261/// Tag bytes for [`PdfObject`] variants.
262const OBJ_NULL: u8 = 0;
263const OBJ_BOOL: u8 = 1;
264const OBJ_INTEGER: u8 = 2;
265const OBJ_REAL: u8 = 3;
266const OBJ_NAME: u8 = 4;
267const OBJ_STRING: u8 = 5;
268const OBJ_ARRAY: u8 = 6;
269const OBJ_DICT: u8 = 7;
270const OBJ_STREAM: u8 = 8;
271const OBJ_REFERENCE: u8 = 9;
272
273fn serialize_operation(buf: &mut Vec<u8>, op: &Operation) {
274    match op {
275        Operation::AddObject { obj_num } => {
276            buf.push(TAG_ADD);
277            buf.extend_from_slice(&obj_num.to_le_bytes());
278        }
279        Operation::ModifyObject { obj_num, old_value } => {
280            buf.push(TAG_MODIFY);
281            buf.extend_from_slice(&obj_num.to_le_bytes());
282            serialize_pdf_object(buf, old_value);
283        }
284        Operation::DeleteObject { obj_num, old_value } => {
285            buf.push(TAG_DELETE);
286            buf.extend_from_slice(&obj_num.to_le_bytes());
287            serialize_pdf_object(buf, old_value);
288        }
289        Operation::Batch { ops, description } => {
290            buf.push(TAG_BATCH);
291            let desc_bytes = description.as_bytes();
292            buf.extend_from_slice(&(desc_bytes.len() as u32).to_le_bytes());
293            buf.extend_from_slice(desc_bytes);
294            buf.extend_from_slice(&(ops.len() as u32).to_le_bytes());
295            for op in ops {
296                serialize_operation(buf, op);
297            }
298        }
299    }
300}
301
302fn serialize_pdf_object(buf: &mut Vec<u8>, obj: &PdfObject) {
303    match obj {
304        PdfObject::Null => buf.push(OBJ_NULL),
305        PdfObject::Bool(v) => {
306            buf.push(OBJ_BOOL);
307            buf.push(if *v { 1 } else { 0 });
308        }
309        PdfObject::Integer(v) => {
310            buf.push(OBJ_INTEGER);
311            buf.extend_from_slice(&v.to_le_bytes());
312        }
313        PdfObject::Real(v) => {
314            buf.push(OBJ_REAL);
315            buf.extend_from_slice(&v.to_le_bytes());
316        }
317        PdfObject::Name(v) => {
318            buf.push(OBJ_NAME);
319            buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
320            buf.extend_from_slice(v);
321        }
322        PdfObject::String(v) => {
323            buf.push(OBJ_STRING);
324            buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
325            buf.extend_from_slice(v);
326        }
327        PdfObject::Array(items) => {
328            buf.push(OBJ_ARRAY);
329            buf.extend_from_slice(&(items.len() as u32).to_le_bytes());
330            for item in items {
331                serialize_pdf_object(buf, item);
332            }
333        }
334        PdfObject::Dict(dict) => {
335            buf.push(OBJ_DICT);
336            serialize_pdf_dict(buf, dict);
337        }
338        PdfObject::Stream { dict, data } => {
339            buf.push(OBJ_STREAM);
340            serialize_pdf_dict(buf, dict);
341            buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
342            buf.extend_from_slice(data);
343        }
344        PdfObject::Reference(r) => {
345            buf.push(OBJ_REFERENCE);
346            buf.extend_from_slice(&r.obj_num.to_le_bytes());
347            buf.extend_from_slice(&r.gen_num.to_le_bytes());
348        }
349    }
350}
351
352fn serialize_pdf_dict(buf: &mut Vec<u8>, dict: &PdfDict) {
353    let entries: Vec<_> = dict.iter().collect();
354    buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
355    for (key, value) in entries {
356        buf.extend_from_slice(&(key.len() as u32).to_le_bytes());
357        buf.extend_from_slice(key);
358        serialize_pdf_object(buf, value);
359    }
360}
361
362// ---------------------------------------------------------------------------
363// Deserialization
364// ---------------------------------------------------------------------------
365
366/// A simple cursor over a byte slice for deserialization.
367struct Cursor<'a> {
368    data: &'a [u8],
369    pos: usize,
370}
371
372impl<'a> Cursor<'a> {
373    fn new(data: &'a [u8]) -> Self {
374        Self { data, pos: 0 }
375    }
376
377    fn read_bytes(&mut self, n: usize) -> Option<&'a [u8]> {
378        if self.pos + n > self.data.len() {
379            return None;
380        }
381        let slice = &self.data[self.pos..self.pos + n];
382        self.pos += n;
383        Some(slice)
384    }
385
386    fn read_u8(&mut self) -> Option<u8> {
387        let b = self.read_bytes(1)?;
388        Some(b[0])
389    }
390
391    fn read_u16(&mut self) -> Option<u16> {
392        let b = self.read_bytes(2)?;
393        Some(u16::from_le_bytes([b[0], b[1]]))
394    }
395
396    fn read_u32(&mut self) -> Option<u32> {
397        let b = self.read_bytes(4)?;
398        Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
399    }
400
401    fn read_i64(&mut self) -> Option<i64> {
402        let b = self.read_bytes(8)?;
403        Some(i64::from_le_bytes([
404            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
405        ]))
406    }
407
408    fn read_f64(&mut self) -> Option<f64> {
409        let b = self.read_bytes(8)?;
410        Some(f64::from_le_bytes([
411            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
412        ]))
413    }
414}
415
416fn deserialize_operation(cursor: &mut Cursor<'_>) -> Option<Operation> {
417    let tag = cursor.read_u8()?;
418    match tag {
419        TAG_ADD => {
420            let obj_num = cursor.read_u32()?;
421            Some(Operation::AddObject { obj_num })
422        }
423        TAG_MODIFY => {
424            let obj_num = cursor.read_u32()?;
425            let old_value = deserialize_pdf_object(cursor)?;
426            Some(Operation::ModifyObject { obj_num, old_value })
427        }
428        TAG_DELETE => {
429            let obj_num = cursor.read_u32()?;
430            let old_value = deserialize_pdf_object(cursor)?;
431            Some(Operation::DeleteObject { obj_num, old_value })
432        }
433        TAG_BATCH => {
434            let desc_len = cursor.read_u32()? as usize;
435            let desc_bytes = cursor.read_bytes(desc_len)?;
436            let description = std::str::from_utf8(desc_bytes).ok()?.to_string();
437            let count = cursor.read_u32()? as usize;
438            let mut ops = Vec::with_capacity(count);
439            for _ in 0..count {
440                ops.push(deserialize_operation(cursor)?);
441            }
442            Some(Operation::Batch { ops, description })
443        }
444        _ => None,
445    }
446}
447
448fn deserialize_pdf_object(cursor: &mut Cursor<'_>) -> Option<PdfObject> {
449    let tag = cursor.read_u8()?;
450    match tag {
451        OBJ_NULL => Some(PdfObject::Null),
452        OBJ_BOOL => {
453            let v = cursor.read_u8()?;
454            Some(PdfObject::Bool(v != 0))
455        }
456        OBJ_INTEGER => {
457            let v = cursor.read_i64()?;
458            Some(PdfObject::Integer(v))
459        }
460        OBJ_REAL => {
461            let v = cursor.read_f64()?;
462            Some(PdfObject::Real(v))
463        }
464        OBJ_NAME => {
465            let len = cursor.read_u32()? as usize;
466            let bytes = cursor.read_bytes(len)?;
467            Some(PdfObject::Name(bytes.to_vec()))
468        }
469        OBJ_STRING => {
470            let len = cursor.read_u32()? as usize;
471            let bytes = cursor.read_bytes(len)?;
472            Some(PdfObject::String(bytes.to_vec()))
473        }
474        OBJ_ARRAY => {
475            let count = cursor.read_u32()? as usize;
476            let mut items = Vec::with_capacity(count);
477            for _ in 0..count {
478                items.push(deserialize_pdf_object(cursor)?);
479            }
480            Some(PdfObject::Array(items))
481        }
482        OBJ_DICT => {
483            let dict = deserialize_pdf_dict(cursor)?;
484            Some(PdfObject::Dict(dict))
485        }
486        OBJ_STREAM => {
487            let dict = deserialize_pdf_dict(cursor)?;
488            let data_len = cursor.read_u32()? as usize;
489            let data = cursor.read_bytes(data_len)?.to_vec();
490            Some(PdfObject::Stream { dict, data })
491        }
492        OBJ_REFERENCE => {
493            let obj_num = cursor.read_u32()?;
494            let gen_num = cursor.read_u16()?;
495            Some(PdfObject::Reference(crate::object::IndirectRef {
496                obj_num,
497                gen_num,
498            }))
499        }
500        _ => None,
501    }
502}
503
504fn deserialize_pdf_dict(cursor: &mut Cursor<'_>) -> Option<PdfDict> {
505    let count = cursor.read_u32()? as usize;
506    let mut dict = PdfDict::new();
507    for _ in 0..count {
508        let key_len = cursor.read_u32()? as usize;
509        let key = cursor.read_bytes(key_len)?.to_vec();
510        let value = deserialize_pdf_object(cursor)?;
511        dict.insert(key, value);
512    }
513    Some(dict)
514}
515
516// ---------------------------------------------------------------------------
517// Tests
518// ---------------------------------------------------------------------------
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use crate::object::{IndirectRef, PdfDict, PdfObject};
524
525    fn sample_object() -> PdfObject {
526        PdfObject::Integer(42)
527    }
528
529    #[test]
530    fn record_and_undo_single() {
531        let mut j = Journal::new();
532        j.record(Operation::AddObject { obj_num: 1 });
533        assert!(j.can_undo());
534        assert_eq!(j.undo_count(), 1);
535
536        let op = j.undo().unwrap();
537        assert!(!j.can_undo());
538        assert!(j.can_redo());
539        match op {
540            Operation::AddObject { obj_num } => assert_eq!(obj_num, 1),
541            _ => panic!("expected AddObject"),
542        }
543    }
544
545    #[test]
546    fn record_and_redo_after_undo() {
547        let mut j = Journal::new();
548        j.record(Operation::ModifyObject {
549            obj_num: 5,
550            old_value: sample_object(),
551        });
552        j.undo();
553        assert!(j.can_redo());
554        assert_eq!(j.redo_count(), 1);
555
556        let op = j.redo().unwrap();
557        assert!(j.can_undo());
558        assert!(!j.can_redo());
559        match op {
560            Operation::ModifyObject { obj_num, .. } => assert_eq!(obj_num, 5),
561            _ => panic!("expected ModifyObject"),
562        }
563    }
564
565    #[test]
566    fn recording_disabled_no_ops() {
567        let mut j = Journal::new();
568        j.stop_recording();
569        assert!(!j.is_recording());
570        j.record(Operation::AddObject { obj_num: 1 });
571        assert!(!j.can_undo());
572        assert_eq!(j.undo_count(), 0);
573    }
574
575    #[test]
576    fn redo_cleared_on_new_record() {
577        let mut j = Journal::new();
578        j.record(Operation::AddObject { obj_num: 1 });
579        j.record(Operation::AddObject { obj_num: 2 });
580        j.undo(); // redo has obj_num=2
581        assert!(j.can_redo());
582
583        // Recording a new op clears redo
584        j.record(Operation::AddObject { obj_num: 3 });
585        assert!(!j.can_redo());
586        assert_eq!(j.redo_count(), 0);
587    }
588
589    #[test]
590    fn batch_operations() {
591        let mut j = Journal::new();
592        {
593            let mut batch = j.begin_batch("add two objects");
594            batch.record(Operation::AddObject { obj_num: 10 });
595            batch.record(Operation::AddObject { obj_num: 11 });
596            batch.commit();
597        }
598        assert_eq!(j.undo_count(), 1);
599        let op = j.undo().unwrap();
600        match op {
601            Operation::Batch { ops, description } => {
602                assert_eq!(ops.len(), 2);
603                assert_eq!(description, "add two objects");
604            }
605            _ => panic!("expected Batch"),
606        }
607    }
608
609    #[test]
610    fn batch_cancel() {
611        let mut j = Journal::new();
612        {
613            let mut batch = j.begin_batch("will cancel");
614            batch.record(Operation::AddObject { obj_num: 99 });
615            batch.cancel();
616        }
617        assert!(!j.can_undo());
618        assert_eq!(j.undo_count(), 0);
619    }
620
621    #[test]
622    fn inverse_add() {
623        let op = Operation::AddObject { obj_num: 7 };
624        let inv = Journal::inverse(&op);
625        match inv {
626            Operation::DeleteObject { obj_num, .. } => assert_eq!(obj_num, 7),
627            _ => panic!("expected DeleteObject"),
628        }
629    }
630
631    #[test]
632    fn inverse_modify() {
633        let op = Operation::ModifyObject {
634            obj_num: 3,
635            old_value: PdfObject::Bool(true),
636        };
637        let inv = Journal::inverse(&op);
638        match inv {
639            Operation::ModifyObject { obj_num, old_value } => {
640                assert_eq!(obj_num, 3);
641                assert_eq!(old_value, PdfObject::Bool(true));
642            }
643            _ => panic!("expected ModifyObject"),
644        }
645    }
646
647    #[test]
648    fn inverse_delete() {
649        let op = Operation::DeleteObject {
650            obj_num: 4,
651            old_value: PdfObject::Integer(100),
652        };
653        let inv = Journal::inverse(&op);
654        match inv {
655            Operation::AddObject { obj_num } => assert_eq!(obj_num, 4),
656            _ => panic!("expected AddObject"),
657        }
658    }
659
660    #[test]
661    fn inverse_batch() {
662        let op = Operation::Batch {
663            ops: vec![
664                Operation::AddObject { obj_num: 1 },
665                Operation::DeleteObject {
666                    obj_num: 2,
667                    old_value: PdfObject::Null,
668                },
669            ],
670            description: "test batch".to_string(),
671        };
672        let inv = Journal::inverse(&op);
673        match inv {
674            Operation::Batch { ops, description } => {
675                assert_eq!(description, "Undo: test batch");
676                // Should be reversed order
677                assert_eq!(ops.len(), 2);
678                match &ops[0] {
679                    Operation::AddObject { obj_num } => assert_eq!(*obj_num, 2),
680                    _ => panic!("expected AddObject as inverse of DeleteObject"),
681                }
682                match &ops[1] {
683                    Operation::DeleteObject { obj_num, .. } => assert_eq!(*obj_num, 1),
684                    _ => panic!("expected DeleteObject as inverse of AddObject"),
685                }
686            }
687            _ => panic!("expected Batch"),
688        }
689    }
690
691    #[test]
692    fn multiple_undo_redo_cycles() {
693        let mut j = Journal::new();
694        j.record(Operation::AddObject { obj_num: 1 });
695        j.record(Operation::AddObject { obj_num: 2 });
696        j.record(Operation::AddObject { obj_num: 3 });
697        assert_eq!(j.undo_count(), 3);
698
699        // Undo all
700        j.undo();
701        j.undo();
702        j.undo();
703        assert_eq!(j.undo_count(), 0);
704        assert_eq!(j.redo_count(), 3);
705
706        // Redo all
707        j.redo();
708        j.redo();
709        j.redo();
710        assert_eq!(j.undo_count(), 3);
711        assert_eq!(j.redo_count(), 0);
712
713        // Undo two, redo one
714        j.undo();
715        j.undo();
716        assert_eq!(j.undo_count(), 1);
717        assert_eq!(j.redo_count(), 2);
718        j.redo();
719        assert_eq!(j.undo_count(), 2);
720        assert_eq!(j.redo_count(), 1);
721    }
722
723    #[test]
724    fn serialization_roundtrip() {
725        let mut j = Journal::new();
726        j.record(Operation::AddObject { obj_num: 1 });
727        j.record(Operation::ModifyObject {
728            obj_num: 2,
729            old_value: PdfObject::Name(b"Type".to_vec()),
730        });
731        j.record(Operation::DeleteObject {
732            obj_num: 3,
733            old_value: PdfObject::Array(vec![
734                PdfObject::Integer(10),
735                PdfObject::Real(3.14),
736                PdfObject::String(b"hello".to_vec()),
737            ]),
738        });
739        // Create a batch
740        {
741            let mut batch = j.begin_batch("batch op");
742            batch.record(Operation::AddObject { obj_num: 100 });
743            batch.commit();
744        }
745        // Undo one to populate redo stack
746        j.undo();
747
748        let bytes = j.to_bytes();
749        let j2 = Journal::from_bytes(&bytes).expect("deserialization failed");
750
751        assert_eq!(j2.undo_count(), j.undo_count());
752        assert_eq!(j2.redo_count(), j.redo_count());
753
754        // Re-serialize and check bytes are identical
755        let bytes2 = j2.to_bytes();
756        assert_eq!(bytes, bytes2);
757    }
758
759    #[test]
760    fn serialization_roundtrip_complex_objects() {
761        let mut dict = PdfDict::new();
762        dict.insert(b"Key".to_vec(), PdfObject::Bool(false));
763        dict.insert(b"Other".to_vec(), PdfObject::Null);
764
765        let mut j = Journal::new();
766        j.record(Operation::ModifyObject {
767            obj_num: 50,
768            old_value: PdfObject::Dict(dict),
769        });
770        j.record(Operation::DeleteObject {
771            obj_num: 51,
772            old_value: PdfObject::Stream {
773                dict: PdfDict::new(),
774                data: vec![0xDE, 0xAD, 0xBE, 0xEF],
775            },
776        });
777        j.record(Operation::ModifyObject {
778            obj_num: 52,
779            old_value: PdfObject::Reference(IndirectRef {
780                obj_num: 99,
781                gen_num: 2,
782            }),
783        });
784
785        let bytes = j.to_bytes();
786        let j2 = Journal::from_bytes(&bytes).expect("deserialization failed");
787        assert_eq!(j2.undo_count(), 3);
788        assert_eq!(j2.to_bytes(), bytes);
789    }
790
791    #[test]
792    fn empty_journal_serialization() {
793        let j = Journal::new();
794        let bytes = j.to_bytes();
795        // Magic(4) + version(4) + undo_count(4) + redo_count(4) = 16 bytes
796        assert_eq!(bytes.len(), 16);
797        assert_eq!(&bytes[0..4], b"JRNL");
798
799        let j2 = Journal::from_bytes(&bytes).expect("deserialization failed");
800        assert_eq!(j2.undo_count(), 0);
801        assert_eq!(j2.redo_count(), 0);
802        assert!(j2.is_recording());
803    }
804
805    #[test]
806    fn can_undo_can_redo_states() {
807        let mut j = Journal::new();
808        assert!(!j.can_undo());
809        assert!(!j.can_redo());
810
811        j.record(Operation::AddObject { obj_num: 1 });
812        assert!(j.can_undo());
813        assert!(!j.can_redo());
814
815        j.undo();
816        assert!(!j.can_undo());
817        assert!(j.can_redo());
818
819        j.redo();
820        assert!(j.can_undo());
821        assert!(!j.can_redo());
822    }
823
824    #[test]
825    fn clear_history() {
826        let mut j = Journal::new();
827        j.record(Operation::AddObject { obj_num: 1 });
828        j.record(Operation::AddObject { obj_num: 2 });
829        j.undo();
830        assert!(j.can_undo());
831        assert!(j.can_redo());
832
833        j.clear();
834        assert!(!j.can_undo());
835        assert!(!j.can_redo());
836        assert_eq!(j.undo_count(), 0);
837        assert_eq!(j.redo_count(), 0);
838    }
839
840    #[test]
841    fn undo_on_empty_returns_none() {
842        let mut j = Journal::new();
843        assert!(j.undo().is_none());
844    }
845
846    #[test]
847    fn redo_on_empty_returns_none() {
848        let mut j = Journal::new();
849        assert!(j.redo().is_none());
850    }
851
852    #[test]
853    fn from_bytes_invalid_magic() {
854        assert!(Journal::from_bytes(b"XXXX").is_none());
855    }
856
857    #[test]
858    fn from_bytes_truncated() {
859        assert!(Journal::from_bytes(b"JRN").is_none());
860        assert!(Journal::from_bytes(b"").is_none());
861    }
862}