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                    t!("errors.create_comments", e = 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(NotificationLevel::Error, t!("errors.write_comments", e = e));
122                return;
123            }
124            self.log(NotificationLevel::Info, t!("app.messages.comments_saved"));
125            self.comments.reset_dirty();
126        }
127    }
128
129    /// If comments_path is None, it will use the default path calculated by get_comments_path.
130    pub(super) fn load_comments(&mut self, comments_path: Option<String>) {
131        let comments_path = comments_path.unwrap_or(self.get_comments_path());
132        match self.filesystem.read(&comments_path) {
133            Ok(comments_data) => match serde_json::from_slice::<Comments>(&comments_data) {
134                Ok(comments) => {
135                    self.comments = comments;
136                    self.comments
137                        .check_max_address(self.data.bytes().len() as u64);
138                    self.log(NotificationLevel::Info, t!("app.messages.comments_loaded"));
139                }
140                Err(e) => {
141                    self.log(NotificationLevel::Error, t!("errors.parse_comments", e = e));
142                }
143            },
144            Err(e) => {
145                // This is in debug because the file may not exist.
146                self.log(NotificationLevel::Debug, t!("errors.read_comments", e = e));
147                self.comments = Comments::new();
148            }
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::{app::plugins::plugin::Plugin, get_app_context};
156
157    use super::*;
158
159    #[test]
160    fn insert_and_remove() {
161        let mut comments = Comments::new();
162        comments.insert(0x1000, "comment_1".to_string());
163        assert!(comments.is_dirty());
164        comments.insert(0x2000, "comment_2".to_string());
165        comments.insert(0x3000, "comment_3".to_string());
166        comments.reset_dirty();
167        assert_eq!(comments.len(), 3);
168        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
169        assert_eq!(comments.get(&0x2000), Some(&"comment_2".to_string()));
170        assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
171        assert!(!comments.is_dirty());
172        comments.remove(&0x2000);
173        assert!(comments.is_dirty());
174        assert_eq!(comments.len(), 2);
175        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
176        assert_eq!(comments.get(&0x2000), None);
177        assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
178        comments.check_max_address(0x2000);
179        assert_eq!(comments.len(), 1);
180        assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
181        assert_eq!(comments.get(&0x2000), None);
182        assert_eq!(comments.get(&0x3000), None);
183    }
184
185    #[test]
186    fn save_and_load() {
187        let mut app = App::mockup(vec![0x90; 0x100]);
188        let tmp_comments = tempfile::NamedTempFile::new().unwrap();
189        let comments_path = tmp_comments.path().to_str().unwrap().to_string();
190        app.comments.insert(0x10, "comment_1".to_string());
191        app.comments.insert(0x20, "comment_2".to_string());
192        app.comments.insert(0x30, "comment_3".to_string());
193        app.save_comments(Some(comments_path.clone()));
194        assert!(!app.comments.is_dirty());
195        assert!(app.logger.get_notification_level() < NotificationLevel::Warning);
196        app.comments = Comments::new();
197        app.load_comments(Some(comments_path));
198        assert_eq!(app.comments.len(), 3);
199        assert_eq!(app.comments.get(&0x10), Some(&"comment_1".to_string()));
200        assert_eq!(app.comments.get(&0x20), Some(&"comment_2".to_string()));
201        assert_eq!(app.comments.get(&0x30), Some(&"comment_3".to_string()));
202    }
203
204    #[test]
205    fn test_plugin() {
206        let source = "
207        function init(context)
208            context.set_comment(0x10, 'comment_1')
209            context.set_comment(0x20, 'comment_2')
210            context.set_comment(0x30, 'comment_3')
211            c1 = context.get_comment(0x10)
212            c2 = context.get_comment(0x20)
213            c3 = context.get_comment(0x30)
214            should_be_nil = context.get_comment(0x40)
215            assert(c1 == 'comment_1', 'c1')
216            assert(c2 == 'comment_2', 'c2')
217            assert(c3 == 'comment_3', 'c3')
218            assert(should_be_nil == nil, 'should_be_nil')
219            comments = context.get_comments()
220            assert(comments[0x10] == 'comment_1', 'table c1')
221            assert(comments[0x20] == 'comment_2', 'table c2')
222            assert(comments[0x30] == 'comment_3', 'table c3')
223            context.set_comment(0x10, '')
224            assert(context.get_comment(0x10) == nil, 'remove c1')
225            context.set_comment(0x20, nil)
226            assert(context.get_comment(0x20) == nil, 'remove c2')
227        end";
228
229        let mut app = App::mockup(vec![0; 0x100]);
230        let mut app_context = get_app_context!(app);
231        Plugin::new_from_source(source, &mut app_context).unwrap();
232    }
233}