Skip to main content

strop_core/buffer/
seed.rs

1//! Deterministic buffer seeds (R11 forensic replay): whole text, revision,
2//! undo history and the disk baseline — a replayed buffer keeps its exact
3//! live identity without reading the filesystem. `disk_stamp` is seed DATA
4//! for overwrite protection, never a request to stat anything.
5use serde::{Deserialize, Serialize};
6
7use super::Buffer;
8use crate::history::History;
9use crate::id::BufferRevision;
10
11/// A pure, serializable image of a buffer at the startup seed boundary.
12/// The buffer's diagnostic trace identity is deliberately absent: it names
13/// a process-local incarnation, not document semantics.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BufferSeed {
16    pub text: String,
17    pub revision: BufferRevision,
18    pub history: History,
19    #[serde(with = "crate::path_serde::option")]
20    pub path: Option<std::path::PathBuf>,
21    pub name: Option<String>,
22    pub dirty: bool,
23    pub readonly: bool,
24    pub disk_stamp: Option<std::time::SystemTime>,
25    #[serde(with = "crate::path_serde::option")]
26    pub file_identity: Option<std::path::PathBuf>,
27}
28
29impl Buffer {
30    pub fn seed(&self) -> BufferSeed {
31        BufferSeed {
32            text: self.rope.to_string(),
33            revision: self.revision(),
34            history: self.history.clone(),
35            path: self.path.clone(),
36            name: self.name.clone(),
37            dirty: self.dirty,
38            readonly: self.readonly,
39            disk_stamp: self.disk_stamp,
40            file_identity: self.file_identity.clone(),
41        }
42    }
43}
44
45impl BufferSeed {
46    /// Rebuild the buffer. Restored history is validated against the
47    /// seeded text; a seed that disagrees is rejected, not coerced.
48    pub fn into_buffer(self) -> Result<Buffer, crate::history::HistoryError> {
49        let mut buffer = Buffer::from_text(&self.text);
50        buffer.restore_history(self.history)?;
51        buffer.epoch = self.revision.get();
52        buffer.path = self.path;
53        buffer.name = self.name;
54        buffer.dirty = self.dirty;
55        buffer.readonly = self.readonly;
56        buffer.disk_stamp = self.disk_stamp;
57        buffer.file_identity = self.file_identity;
58        Ok(buffer)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn seed_round_trips_content_history_and_disk_baseline() {
68        let mut buffer = Buffer::from_text("line\nline two\n");
69        buffer.begin_undo_group();
70        buffer
71            .edit()
72            .replace(crate::Range::charwise(5, 8), "XY")
73            .unwrap();
74        buffer.commit_undo_group();
75        buffer.dirty = true;
76        buffer.disk_stamp = Some(std::time::UNIX_EPOCH);
77        let seed = buffer.seed();
78        let mut restored = seed.into_buffer().unwrap();
79        assert_eq!(restored.text().to_string(), "line\nXYe two\n");
80        // The undo that survived the seed still runs on the rebuilt buffer.
81        restored.undo().unwrap();
82        assert_eq!(restored.text().to_string(), "line\nline two\n");
83    }
84
85    #[test]
86    fn history_that_disagrees_with_text_is_rejected() {
87        let mut buffer = Buffer::from_text("abc\n");
88        buffer.begin_undo_group();
89        buffer
90            .edit()
91            .replace(crate::Range::charwise(0, 1), "z")
92            .unwrap();
93        buffer.commit_undo_group();
94        let mut seed = buffer.seed();
95        seed.text = "different bytes\n".into();
96        assert!(seed.into_buffer().is_err());
97    }
98}