diffedit3/
types.rs

1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
6#[serde(tag = "type", content = "value")]
7pub enum FileEntry {
8    Missing,
9    // TODO: Track executable bit, other metadata perhaps
10    Text(String),
11    Unsupported(String),
12}
13
14/// TODO: Clean this up to make things more readable
15const OUTPUT_INDEX: usize = 2;
16#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
17//struct EntriesToCompare<P, const N: usize>(std::collections::BTreeMap<P,
18// [Option<String>; N]>);
19pub struct EntriesToCompare(pub std::collections::BTreeMap<PathBuf, [FileEntry; 3]>);
20
21#[derive(Error, Debug)]
22pub enum DataSaveError {
23    // TODO: Collect the list of what files couldn't be saved
24    #[error("IO Error while saving {0}: {1}")]
25    IOError(PathBuf, std::io::Error),
26    #[error("Cannot save the demo fake data")]
27    CannotSaveFakeData,
28    #[error("Failed to retrieve valid paths for saving: {0}")]
29    ValidationIOError(#[from] DataReadError),
30    #[error(
31        "Security error: got request to save to a file that wasn't one of the files being merged: \
32         '{0}'\nPerhaps this client is now connected to a different server than the one it was \
33         started from?"
34    )]
35    ValidationFailError(String),
36}
37
38#[derive(Error, Debug)]
39pub enum DataReadError {
40    #[error("IO Error while reading: {0}")]
41    IOError(#[from] std::io::Error),
42}
43
44impl serde::Serialize for DataSaveError {
45    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46    where
47        S: serde::Serializer,
48    {
49        serializer.serialize_str(self.to_string().as_ref())
50    }
51}
52
53impl serde::Serialize for DataReadError {
54    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
55    where
56        S: serde::Serializer,
57    {
58        serializer.serialize_str(self.to_string().as_ref())
59    }
60}
61
62// TODO: What does 'static mean here? Can it be loosened?
63pub trait DataInterface: Send + Sync + 'static {
64    /// Return the content of either the original files to merge or the
65    /// last-saved version.
66    ///
67    /// A `scan()` after a successful `save()` should return the saved results.
68    fn scan(&self) -> Result<EntriesToCompare, DataReadError>;
69    // TODO: Make `save` more generic than IndexMap
70    /// Do not use this method directly, as it does not check whether the
71    /// requested paths are safe to save to.
72    fn save_unchecked(
73        &mut self,
74        result: indexmap::IndexMap<String, String>,
75    ) -> Result<(), DataSaveError>;
76
77    /// Get a list of all the files we were originally asked to merge.
78    ///
79    /// The default implementation may be very inefficient.
80    fn get_valid_entries(&mut self) -> Result<std::collections::HashSet<PathBuf>, DataReadError> {
81        let entries = self.scan()?;
82        Ok(entries.0.keys().cloned().collect())
83    }
84
85    /// Save the result. First, check that each file being saved was one of the
86    /// files we were comparing originally.
87    ///
88    /// This check helps with two potential problems when running a local
89    /// server:
90    /// - The frontend webapp could be connected to a different server than it
91    ///   was started with.
92    /// - A malicious frontend could try making the diff editor save to
93    ///   `../../../../home/$USER/.bashrc`.
94    fn save(&mut self, result: indexmap::IndexMap<String, String>) -> Result<(), DataSaveError> {
95        let valid_entries = self.get_valid_entries()?;
96        if let Some(unsafe_path) = result
97            .keys()
98            .find(|x| !valid_entries.contains::<PathBuf>(&x.into()))
99        {
100            // TODO: Have the server print some debug info, e.g. the list of
101            // valid file names, to the terminal. It should not be returned to
102            // the HTTP request, though.
103            return Err(DataSaveError::ValidationFailError(unsafe_path.to_string()));
104        }
105        self.save_unchecked(result)
106    }
107}
108
109// Dummy implementation for in-memory storage
110// TODO: Make FakeData use this
111// TODO: Create a separate type for the DataInterface, allow for callbacks
112// and/or output to a Writer on scan or save.
113impl DataInterface for EntriesToCompare {
114    fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
115        Ok(self.clone())
116    }
117
118    fn save_unchecked(
119        &mut self,
120        result: indexmap::IndexMap<String, String>,
121    ) -> Result<(), DataSaveError> {
122        for (path, new_value) in result.into_iter() {
123            self.0
124                .get_mut(&PathBuf::from(path))
125                .expect("At this point, `save()` should have verified that the path is valid")
126                [OUTPUT_INDEX] = FileEntry::Text(new_value);
127        }
128        Ok(())
129    }
130}
131
132pub struct FakeData;
133
134impl DataInterface for FakeData {
135    fn scan(&self) -> Result<EntriesToCompare, DataReadError> {
136        // let mut two_sides_map = btreemap! {
137        //     "edited_file" => [
138        //           Some("First\nThird\nFourth\nFourthAndAHalf\n\nFifth\nSixth\n----\
139        // none two"),           Some("First\nSecond\nThird\nFifth\nSixth\n----\
140        // none\n")     ],
141        //     "deleted_file" => [Some("deleted"), None],
142        //     "added file" => [None, Some("added")]
143        // };
144        #[rustfmt::skip]
145        let two_sides_map = vec![
146            (
147                "edited_file",
148                [
149                    FileEntry::Text(
150                        "Long line, a long line, a quite long line. Long line, a long line, a \
151                         quite long line. Long line, a long line, a quite long \
152                         line.\nFirst\nThird\nFourth\nFourthAndAHalf\nSame\nSame\nSame\nSame\
153                         \nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
154                         \nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none two"
155                            .to_string(),
156                    ),
157                    FileEntry::Text(
158                        "Long line, a long line, a quite long line. Long line, a long line, a \
159                         quite long line. Something new. Long line, a long line, a quite long \
160                         line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
161                         \nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\
162                         \nSame\nSame\nFifth\nSixth\n----\none\n"
163                            .to_string(),
164                    ),
165                ],
166            ),
167            (
168                "deleted_file",
169                [FileEntry::Text("deleted".to_string()), FileEntry::Missing],
170            ),
171            (
172                "added file",
173                [FileEntry::Missing, FileEntry::Text("added".to_string())],
174            ),
175            (
176                "unsupported-left",
177                [
178                    FileEntry::Unsupported("demo of an unsupported file".to_string()),
179                    FileEntry::Text("text".to_string()),
180                ],
181            ),
182            (
183                "unsupported-right",
184                [
185                    FileEntry::Text("text".to_string()),
186                    FileEntry::Unsupported("demo of an unsupported file".to_string()),
187                ],
188            ),
189        ];
190        Ok(EntriesToCompare(
191            two_sides_map
192                .into_iter()
193                .map(|(key, [left, right])| (PathBuf::from(key), [left, right.clone(), right]))
194                .collect(),
195        ))
196    }
197
198    fn save_unchecked(
199        &mut self,
200        result: indexmap::IndexMap<String, String>,
201    ) -> Result<(), DataSaveError> {
202        eprintln!("Can't save fake demo data. Here it is as TOML");
203        eprintln!();
204        eprintln!(
205            "{}",
206            toml::to_string(&result).unwrap_or_else(|err| format!("Failed to parse TOML: {err}"))
207        );
208        Err(DataSaveError::CannotSaveFakeData)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn fake_data() {
218        insta::assert_yaml_snapshot!(FakeData.scan().unwrap(), 
219        @r###"
220        ---
221        added file:
222          - type: Missing
223          - type: Text
224            value: added
225          - type: Text
226            value: added
227        deleted_file:
228          - type: Text
229            value: deleted
230          - type: Missing
231          - type: Missing
232        edited_file:
233          - type: Text
234            value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Long line, a long line, a quite long line.\nFirst\nThird\nFourth\nFourthAndAHalf\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none two"
235          - type: Text
236            value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Something new. Long line, a long line, a quite long line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none\n"
237          - type: Text
238            value: "Long line, a long line, a quite long line. Long line, a long line, a quite long line. Something new. Long line, a long line, a quite long line.\nFirst\nSecond\nThird\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nSame\nFifth\nSixth\n----\none\n"
239        unsupported-left:
240          - type: Unsupported
241            value: demo of an unsupported file
242          - type: Text
243            value: text
244          - type: Text
245            value: text
246        unsupported-right:
247          - type: Text
248            value: text
249          - type: Unsupported
250            value: demo of an unsupported file
251          - type: Unsupported
252            value: demo of an unsupported file
253        "###);
254    }
255}