Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ tui-input = "0.15.0"
[profile.dist]
inherits = "release"
lto = "thin"

[dev-dependencies]
pretty_assertions = "1.4.1"
134 changes: 129 additions & 5 deletions src/database.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::app::App;
use crate::hex::{blocks::ColoredBlock, comment::Comment, hex_view::HexView};

impl App {
pub fn save_database(&self) -> Result<(), Box<dyn Error>> {
let toml_string = toml::to_string_pretty(&self.hex_view)?;
let target_dir: &Path = Path::new(&self.file_info.path)
.parent()
.unwrap_or(Path::new("."));
Expand All @@ -23,6 +26,10 @@ impl App {
return Ok(());
}

// serialize after checking for empty
let db = hex_view_to_db(&self.hex_view);
let toml_string = toml::to_string_pretty(&db)?;

// try target's path or else current directory
fs::write(&target_db, &toml_string).or_else(|_| fs::write(&cwd_db, &toml_string))?;

Expand All @@ -36,10 +43,127 @@ impl App {
let target_db: PathBuf = target_dir.join(&cwd_db);
let data = fs::read_to_string(&cwd_db).or_else(|_| fs::read_to_string(&target_db))?;

// TODO: Although we're only interested in comments and bookmarks, other fields might
// be loaded if they are defined in the TOML file. How to prevent that?
self.hex_view = toml::from_str(&data)?;
self.hex_view.editing_hex = true; // otherwise it defaults to false if a .dz6 file exists for the target
let db = toml::from_str::<Database>(&data)?;
self.hex_view = hex_view_from_db(db);
Ok(())
}
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Database {
// blocks are ByteBlock structs -- ranges with different colors
pub blocks: Vec<DbColoredBlock>,
pub bookmarks: Vec<usize>,

// `comment_name_list` is used to show comments in Names list
// and also on the conversion from selected item on the list
// to file offset passed to goto()
pub comment_name_list: Vec<DbComment>,

// TODO: comments and comment_name_list are redundant, we should only store one of them

@merces merces Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I can't remember why I decided to store both of them. I think my line of thought was that the user could add a name to an offset so this name could be shown when code references it. For example:

jmp 0x401000 could be shown as jmp destination if the user set the name destination to 0x401000, but this only makes sense in a disassembly view, which we don't have yet. Also, even when names and comments are two different vectors, we can still show the elements from both of them in a single Names window, but store them separately in the database and build the comment_name_list from comments and names in the future. Alternatively, perhaps we can get rid of comment_name_list entirely and build this list when the Names window is shown? Just thoughts. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to store just one of them and derive the data of the other one.

I will make a PR the "deprecates" the comment_name_list from the database.

The idea is that we stop reading from it but we still write to it. That way at least for a while the current version can still create DBs for previous versions. then after a few releases we remove the field for good.

(I am probably overly paranoid with this "forward" and "backwards" compatability stuff)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created the MR for it:
#63

I decide to only make it "backwards" compatible, not forward compatible. I explain it in the other MR.

// but to avoid breaking existing .dz6 files, we will keep both for now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for thinking about it! Currently, there's no way an user can create a name, so maybe in the future we can build this list (for the Names window) from comments without breaking anything by ignoring comment_names_list coming from databases created by earlier versions of dz6?

// The `comments` format is more compact and doesn't require a new type
// so maybe later we default to using only it
pub comments: BTreeMap<usize, String>,
Comment thread
merces marked this conversation as resolved.
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct DbComment {
pub offset: usize,
pub comment: String,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct DbColoredBlock {
pub start: usize,
pub end: usize,
pub bg_color: u32,
pub fg_color: u32,
}

fn hex_view_from_db(db: Database) -> HexView {
HexView {
blocks: db.blocks.into_iter().map(colored_block_from_db).collect(),
bookmarks: db.bookmarks,
comment_name_list: db
.comment_name_list
.into_iter()
.map(comment_from_db)
.collect(),
comments: db.comments.into_iter().collect(),
editing_hex: true, // otherwise it defaults to false if a .dz6 file exists for the target
..Default::default()
}
}
fn colored_block_from_db(db_block: DbColoredBlock) -> ColoredBlock {
ColoredBlock {
start: db_block.start,
end: db_block.end,
bg_color: db_block.bg_color,
fg_color: db_block.fg_color,
}
}
fn comment_from_db(db_comment: DbComment) -> Comment {
Comment {
offset: db_comment.offset,
comment: db_comment.comment,
}
}

/// HexView to Database conversion,
/// takes a reference because we don't want to consume the live HexView that is
/// being used in the app.
fn hex_view_to_db(hex_view: &HexView) -> Database {
Database {
blocks: hex_view.blocks.iter().map(colored_block_to_db).collect(),
bookmarks: hex_view.bookmarks.clone(),
comment_name_list: hex_view
.comment_name_list
.iter()
.map(comment_to_db)
.collect(),
comments: hex_view
.comments
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect(),
}
}
fn colored_block_to_db(block: &ColoredBlock) -> DbColoredBlock {
DbColoredBlock {
start: block.start,
end: block.end,
bg_color: block.bg_color,
fg_color: block.fg_color,
}
}
fn comment_to_db(comment: &Comment) -> DbComment {
DbComment {
offset: comment.offset,
comment: comment.comment.clone(),
}
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty neat library for tests that will color the parts that are different when assert_eq fails.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat indeed! Should we use it in other tests too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no big downside to using it. but I usually only use it when a test fails and I have a hard time figuring out why.

In this case the serialization test was failing because the comments map sometimes serialized in different orders. it was hard to see what was different so I added the pretty_assertion.


#[test]
fn test_database_round_trip() {
// db file generated with the hexview serialization
let db_file = "test_data/db.toml";
let db_str = fs::read_to_string(db_file).expect("Failed to read test database file");

let db: Database = toml::from_str(&db_str).expect("Failed to deserialize database");

// tests that we don't drop any data from the original file and don't add any
let serialized = toml::to_string_pretty(&db).expect("Failed to serialize database");
assert_eq!(db_str, serialized);

// tests that we can deserialize the serialized string and get the same data back
let re_deserialized: Database =
toml::from_str(&serialized).expect("Failed to deserialize database");
assert_eq!(db, re_deserialized);
}
}
20 changes: 1 addition & 19 deletions src/hex/hex_view.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::collections::{HashMap, HashSet};

use ratatui::widgets::{ListState, TableState};
use serde::{Deserialize, Serialize};
use tui_input::Input;

use crate::hex::{blocks::ColoredBlock, comment::Comment};
Expand All @@ -13,18 +12,14 @@ pub struct Point {
pub y: usize,
}

#[derive(Default, Serialize, Deserialize)]
#[derive(Default)]
pub struct HexView {
#[serde(skip)]
pub ascii_state: TableState,
// blocks are ByteBlock structs -- ranges with different colors
pub blocks: Vec<ColoredBlock>,
pub bookmarks: Vec<usize>,
#[serde(skip)]
pub changed_bytes: HashMap<usize, String>,
#[serde(skip)]
pub changed_history: Vec<usize>,
#[serde(skip)]
pub comment_input: Input, // the input comment widget (tui-input)

// `comment_name_list` is used to show comments in Names list
Expand All @@ -36,30 +31,17 @@ pub struct HexView {
// to handle that with a hash map
pub comments: HashMap<usize, String>,

#[serde(skip)]
pub cursor: Point,
#[serde(skip)]
pub editing_hex: bool,
#[serde(skip)]
pub highlights: HashSet<u8>, // byte highlight
#[serde(skip)]
pub last_visited_offset: usize,
#[serde(skip)]
pub names_list_state: ListState,
#[serde(skip)]
pub names_regex_input: Input,
#[serde(skip)]
pub names_regex: String,
#[serde(skip)]
pub offset_state: TableState,
#[serde(skip)]
pub offset: usize,
#[serde(skip)]
pub search: crate::hex::search::Search,
#[serde(skip)]
pub selection: crate::hex::selection::Selection,
#[serde(skip)]
pub strings_regex_input: Input,
#[serde(skip)]
pub table_state: TableState,
}
34 changes: 34 additions & 0 deletions test_data/db.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
bookmarks = [
288,
325,
]

[[blocks]]
start = 16
end = 21
bg_color = 446956584
fg_color = 2910089301

[[blocks]]
start = 128
end = 134
bg_color = 3843912424
fg_color = 2232455935

[[blocks]]
start = 182
end = 214
bg_color = 4160309102
fg_color = 4169677532

[[comment_name_list]]
offset = 21
comment = " one comment"

[[comment_name_list]]
offset = 101
comment = "comment"

[comments]
21 = " one comment"
101 = "comment"
Loading