hex_patch/app/
comments.rs

1use std::collections::{hash_map::Iter, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use super::{log::NotificationLevel, App};
6
7#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Comments {
9    comments: HashMap<u64, String>,
10    #[serde(skip)]
11    dirty: bool,
12}
13
14impl Comments {
15    pub fn new() -> Self {
16        Self {
17            comments: HashMap::new(),
18            dirty: false,
19        }
20    }
21
22    pub fn remove(&mut self, address: &u64) {
23        self.comments.remove(address);
24        self.dirty = true;
25    }
26
27    pub fn insert(&mut self, address: u64, comment: String) {
28        self.comments.insert(address, comment);
29        self.dirty = true;
30    }
31
32    pub fn iter(&self) -> Iter<'_, u64, String> {
33        self.comments.iter()
34    }
35
36    pub fn get(&self, address: &u64) -> Option<&String> {
37        self.comments.get(address)
38    }
39
40    pub fn len(&self) -> usize {
41        self.comments.len()
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.comments.is_empty()
46    }
47
48    pub fn is_dirty(&self) -> bool {
49        self.dirty
50    }
51
52    pub fn reset_dirty(&mut self) {
53        self.dirty = false;
54    }
55
56    pub fn to_vec(&self) -> Vec<(u64, String)> {
57        self.comments.iter().map(|(a, s)| (*a, s.clone())).collect()
58    }
59
60    pub fn check_max_address(&mut self, max_address: u64) {
61        let mut comments_removed = false;
62        self.comments.retain(|address, _| {
63            if *address > max_address {
64                comments_removed = true;
65                false
66            } else {
67                true
68            }
69        });
70        if comments_removed {
71            self.dirty = true;
72        }
73    }
74}
75
76impl App {
77    pub(super) fn edit_comment(&mut self, comment: &str) {
78        let address = self.get_cursor_position().global_byte_index as u64;
79        if comment.is_empty() {
80            self.comments.remove(&address);
81        } else {
82            self.comments.insert(address, comment.to_string());
83        }
84    }
85
86    pub(super) fn find_comments(&self, filter: &str) -> Vec<(u64, String)> {
87        if filter.is_empty() {
88            return Vec::new();
89        }
90        let mut comments: Vec<(u64, String)> = self
91            .comments
92            .iter()
93            .filter(|(_, symbol)| symbol.contains(filter))
94            .map(|(address, symbol)| (*address, symbol.clone()))
95            .collect();
96        comments.sort_by_key(|(_, symbol)| symbol.len());
97        comments
98    }
99
100    pub(super) fn get_comments_path(&self) -> String {
101        let path = self.filesystem.pwd();
102        path.to_string() + ".hp-data.json"
103    }
104
105    /// If comments_path is None, it will use the default path calculated by get_comments_path.
106    pub(super) fn save_comments(&mut self, comments_path: Option<String>) {
107        if self.comments.is_dirty() {
108            let comments_str = serde_json::to_string_pretty(&self.comments).unwrap();
109            let comments_path = comments_path.unwrap_or(self.get_comments_path());
110            if let Err(e) = self.filesystem.create(&comments_path) {
111                self.log(
112                    NotificationLevel::Error,
113                    &format!("Failed to create comments: {}", e),
114                );
115                return;
116            }
117            if let Err(e) = self
118                .filesystem
119                .write(&comments_path, comments_str.as_bytes())
120            {
121                self.log(
122                    NotificationLevel::Error,
123                    &format!("Failed to write comments: {}", e),
124                );
125                return;
126            }
127            self.log(NotificationLevel::Info, "Comments saved.");
128            self.comments.reset_dirty();
129        }
130    }
131
132    /// If comments_path is None, it will use the default path calculated by get_comments_path.
133    pub(super) fn load_comments(&mut self, comments_path: Option<String>) {
134        let comments_path = comments_path.unwrap_or(self.get_comments_path());
135        match self.filesystem.read(&comments_path) {
136            Ok(comments_data) => match serde_json::from_slice::<Comments>(&comments_data) {
137                Ok(comments) => {
138                    self.comments = comments;
139                    self.comments
140                        .check_max_address(self.data.bytes().len() as u64);
141                    self.log(NotificationLevel::Info, "Comments loaded.");
142                }
143                Err(e) => {
144                    self.log(
145                        NotificationLevel::Error,
146                        &format!("Failed to parse comments: {}", e),
147                    );
148                }
149            },
150            Err(e) => {
151                // This is in debug because the file may not exist.
152                self.log(
153                    NotificationLevel::Debug,
154                    &format!("Failed to read comments: {}", e),
155                );
156                self.comments = Comments::new();
157            }
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use crate::{app::plugins::plugin::Plugin, get_app_context};
165
166    use super::*;
167
168    #[test]
169    fn insert_and_remove() {
170        let mut comments = Comments::new();
171        comments.insert(0x1000, "comment_1".to_string());
172        assert!(comments.is_dirty());
173        comments.insert(0x2000, "comment_2".to_string());
174        comments.insert(0x3000, "comment_3".to_string());
175        comments.reset_dirty();
176        assert_eq!(comments.len(), 3);
177        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
178        assert_eq!(comments.get(&0x2000), Some(&"comment_2".to_string()));
179        assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
180        assert!(!comments.is_dirty());
181        comments.remove(&0x2000);
182        assert!(comments.is_dirty());
183        assert_eq!(comments.len(), 2);
184        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
185        assert_eq!(comments.get(&0x2000), None);
186        assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
187        comments.check_max_address(0x2000);
188        assert_eq!(comments.len(), 1);
189        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
190        assert_eq!(comments.get(&0x2000), None);
191        assert_eq!(comments.get(&0x3000), None);
192    }
193
194    #[test]
195    fn save_and_load() {
196        let mut app = App::mockup(vec![0x90; 0x100]);
197        let tmp_comments = tempfile::NamedTempFile::new().unwrap();
198        let comments_path = tmp_comments.path().to_str().unwrap().to_string();
199        app.comments.insert(0x10, "comment_1".to_string());
200        app.comments.insert(0x20, "comment_2".to_string());
201        app.comments.insert(0x30, "comment_3".to_string());
202        app.save_comments(Some(comments_path.clone()));
203        assert!(!app.comments.is_dirty());
204        assert!(app.logger.get_notification_level() < NotificationLevel::Warning);
205        app.comments = Comments::new();
206        app.load_comments(Some(comments_path));
207        assert_eq!(app.comments.len(), 3);
208        assert_eq!(app.comments.get(&0x10), Some(&"comment_1".to_string()));
209        assert_eq!(app.comments.get(&0x20), Some(&"comment_2".to_string()));
210        assert_eq!(app.comments.get(&0x30), Some(&"comment_3".to_string()));
211    }
212
213    #[test]
214    fn test_plugin() {
215        let source = "
216        function init(context)
217            context.set_comment(0x10, 'comment_1')
218            context.set_comment(0x20, 'comment_2')
219            context.set_comment(0x30, 'comment_3')
220            c1 = context.get_comment(0x10)
221            c2 = context.get_comment(0x20)
222            c3 = context.get_comment(0x30)
223            should_be_nil = context.get_comment(0x40)
224            assert(c1 == 'comment_1', 'c1')
225            assert(c2 == 'comment_2', 'c2')
226            assert(c3 == 'comment_3', 'c3')
227            assert(should_be_nil == nil, 'should_be_nil')
228            comments = context.get_comments()
229            assert(comments[0x10] == 'comment_1', 'table c1')
230            assert(comments[0x20] == 'comment_2', 'table c2')
231            assert(comments[0x30] == 'comment_3', 'table c3')
232            context.set_comment(0x10, '')
233            assert(context.get_comment(0x10) == nil, 'remove c1')
234            context.set_comment(0x20, nil)
235            assert(context.get_comment(0x20) == nil, 'remove c2')
236        end";
237
238        let mut app = App::mockup(vec![0; 0x100]);
239        let mut app_context = get_app_context!(app);
240        Plugin::new_from_source(source, &mut app_context).unwrap();
241    }
242}