diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b6306..7d74831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## dz6 v0.8.0 - Header view: new view to inspect ELF and PE executables. -- Added a command to change the view (same as pressing `Tab`). Example: `:set view hex` (possible values are `text`, `hex`, and `header`). It can be used in `~/.dz6init` to set the default view when dz6 is loaded. -- Added support for comments / lines starting with `#` in `~/.dz6init`. +- Header view: added a command to change the view (same as pressing `Tab`). Example: `:set view hex` (possible values are `text`, `hex`, and `header`). It can be used in `~/.dz6init` to set the default view when dz6 starts. +- initfile: Added support for comments / lines starting with `#` in `~/.dz6init`. +- Search: add basic hex pattern search. `??` matches any byte. Space characters are ignored. +- Search: new configuration options with impact to text search: `:set ignorecase` or `:set ic` enables case-insensitive search. If smart case is also enabled with `:set smartcase` or `:set scs`, case-insensitive search is disabled when the search term contains a capital letter. +- Search: `ws` is an alias for `wrapscan`. ## Breaking changes: + - `Tab` cycles through available views now (`Enter` no longer does that). - To search over the string list, press `/` (it was `f` before). diff --git a/Cargo.lock b/Cargo.lock index d8aaabf..9f39b0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "shlex", @@ -556,13 +556,12 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encoding_rs" -version = "0.8.41" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", "core_detect", - "multiversion", "multiversion_no_op", "rustversion", "scopeguard", @@ -629,9 +628,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "finl_unicode" @@ -832,9 +831,9 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" dependencies = [ "darling", "indoc", @@ -866,9 +865,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -883,7 +882,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -894,15 +893,15 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libm" @@ -912,9 +911,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] @@ -957,21 +956,21 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" dependencies = [ "hashbrown 0.17.1", ] [[package]] name = "mac_address" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" dependencies = [ - "nix", - "winapi", + "nix 0.30.1", + "windows-sys 0.61.2", ] [[package]] @@ -1012,9 +1011,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "log", @@ -1037,27 +1036,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "multiversion" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" -dependencies = [ - "multiversion-macros", -] - -[[package]] -name = "multiversion-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" -dependencies = [ - "proc-macro2", - "quote", - "rustversion", - "syn 3.0.6", -] - [[package]] name = "multiversion_no_op" version = "1.0.0" @@ -1069,6 +1047,18 @@ name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.13.2", "cfg-if", @@ -1280,9 +1270,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" dependencies = [ "memchr", "ucd-trie", @@ -1290,9 +1280,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" dependencies = [ "pest", "pest_generator", @@ -1300,9 +1290,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" dependencies = [ "pest", "pest_meta", @@ -1313,9 +1303,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" dependencies = [ "pest", ] @@ -1500,7 +1490,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.20", + "thiserror 2.0.21", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1785,9 +1775,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -1797,9 +1787,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "static_assertions" @@ -1919,7 +1909,7 @@ dependencies = [ "libc", "log", "memmem", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "ordered-float", @@ -1954,11 +1944,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.21", ] [[package]] @@ -1974,9 +1964,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", @@ -2045,9 +2035,9 @@ checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tui-input" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb7c7ddaef6dcc58fae905d0f89a3df49ef77e93822df7447a5bb0babc9ece3" +checksum = "a4fa0cb7933d19f91eb3109781bf7297d5e4cba57e857d97a29b9465d3383f56" dependencies = [ "ratatui", "unicode-segmentation", @@ -2103,9 +2093,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.1" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "atomic", "getrandom 0.4.3", @@ -2145,9 +2135,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -2158,9 +2148,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2168,9 +2158,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", @@ -2181,9 +2171,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index a15a3d6..feb73f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ encoding_rs = "0.8.35" evalexpr = "13.1.*" goblin = "0.10.5" hex = "0.4.3" -memchr = "2.8.0" +memchr = "2.8.3" mmap-io = "1.0.0" rand = "0.10.1" ratatui = "0.30.0" @@ -37,3 +37,4 @@ lto = "thin" [dev-dependencies] pretty_assertions = "1.4.1" + diff --git a/README.md b/README.md index ee77896..aad1379 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Fast Vim-inspired hex editor for the terminal - Edit in hex or ASCII - String list with regex filtering - Multiple smart ways to navigate through a file -- Find strings and hex bytes +- Find strings and hex patterns - Add comments and bookmarks - Mark blocks with colors - PE/ELF headers parsing (WIP -- not released yet) @@ -100,25 +100,29 @@ Once you load a file in **dz6**, you can use the commands below. #### Commands -| Command | Action | Parameters | Tips/Examples | -| ---------------- | ---------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------- | -| `` | Go to offset | | hex default; `t` suffix = decimal; `+` prefix = incremental jump; `-` prefix = decremental jump | -| `cmt` | Add `` to `` | `` `` | `cmt 1000 "my comment"` (comment at offset 0x1000; offset obeys the same rules above) | -| `sel` | Select `` bytes from `` | `` `` | `sel 40 10t` (select 10 bytes from offset 0x40) | -| `set byteline` | Set the number of bytes per line | `` or `auto` | `set byteline 8` (default is 16; `auto` enables automatic setting based on screen width) | -| `set ctrlchar` | Set the character shown in the ASCII dump for non-graphic values | `` | `set ctrlchar " "` would set a blankspace (default: `.`) | -| `set db` | Turn on database file saving/loading (default) | | A database file with a `.dz6` extension will be used to store bookmarks and comments for the file | -| `set nodb` | Turn off database file saving/loading | | | -| `set dimzero` | Dim (gray out) null bytes only (default) | | | -| `set dimctrl` | Dim all control characters | | All non-graphic characters will be dimmed | -| `set nodim` | Turn off byte dimming | | | -| `set theme` | Set the theme | `dark` or `light` | `set theme light` (default: `dark`) | -| `set view` | Changes the current view | `text`, `hex` or `header` | `set view header` (default: `hex`) | -| `set wrapscan` | Enable search results wrap | | | -| `set nowrapscan` | Disable search results wrap | | | -| `w` | Write changes to file | | | -| `wq` or `x` | Write changes to file and quit | | | -| `q` | Quit without saving changes | | In replace mode, `T` (truncate) is an exception because it modifies the file immediately. | +| Command | Action | Parameters | Tips/Examples | +| ------------------ | ---------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `` | Go to offset | | hex default; `t` suffix = decimal; `+` prefix = incremental jump; `-` prefix = decremental jump | +| `cmt` | Add `` to `` | `` `` | `cmt 1000 "my comment"` (comment at offset 0x1000; offset obeys the same rules above) | +| `sel` | Select `` bytes from `` | `` `` | `sel 40 10t` (select 10 bytes from offset 0x40) | +| `set byteline` | Set the number of bytes per line | `` or `auto` | `set byteline 8` (default is 16; `auto` enables automatic setting based on screen width) | +| `set ctrlchar` | Set the character shown in the ASCII dump for non-graphic values | `` | `set ctrlchar " "` would set a blankspace (default: `.`) | +| `set db` | Turn on database file saving/loading (default) | | A database file with a `.dz6` extension will be used to store bookmarks and comments for the file | +| `set nodb` | Turn off database file saving/loading | | | +| `set dimzero` | Dim (gray out) null bytes only (default) | | | +| `set dimctrl` | Dim all control characters | | All non-graphic characters will be dimmed | +| `set nodim` | Turn off byte dimming | | | +| `set theme` | Set the theme | `dark` or `light` | `set theme light` (default: `dark`) | +| `set view` | Changes the current view | `text`, `hex` or `header` | `set view header` (default: `hex`) | +| `set wrapscan` | Enable search results wrap | | `set ws` also works | +| `set nowrapscan` | Disable search results wrap | | `set nows` also works | +| `set ignorecase` | Enable case-insensitive search (ASCII only) | | `set ic` also works | +| `set noignorecase` | Disable case-insensitive search | | `set noic` also works | +| `set smartcase` | Enable smart case search | | if the search term contains a capital letter and `ignorecase` is enabled, disable it. `set scs` also works. | +| `set nosmartcase` | Disable smart case search | | `set noscs` also works | +| `w` | Write changes to file | | | +| `wq` or `x` | Write changes to file and quit | | | +| `q` | Quit without saving changes | | In replace mode, `T` (truncate) is an exception because it modifies the file immediately. | > If you need permanent settings, create a `$HOME/.dz6init` file containing any of the commands above, one per line. dz6 will load that at startup. Lines starting with `#` are ignored. @@ -144,9 +148,9 @@ This is the main view you would expect from a hex editor. It displays file offse | `Ctrl+x` | Decrement byte under the cursor | | | `v` | Enter [select mode](#hex-selection-mode) | | | `u` | Undo the last change made to the buffer | Use it _before_ writing to the file (`:w`) | -| `/` | Search (forward) | Search the entire file. `Tab` cycles between ASCII and hex search | +| `/` | Enter [search mode](#hex-search-mode) (forward) | Search the entire file. `Tab` cycles between UTF-8 and hex search | | `n` | Search next (forward) | | -| `?` | Search (backward) | Search the entire file. `Tab` cycles between ASCII and hex search | +| `?` | Search (backward) | Search the entire file. `Tab` cycles between UTF-8 and hex search | | `N` | Search next (backward) | | | `s` | Open [Strings](#strings) window | | | `Backspace` | Go to the previously visited offset | This is useful after a Go to command, for example | @@ -172,6 +176,19 @@ This is the main view you would expect from a hex editor. It displays file offse | `Alt+m` | Mark a block with a random color | `Alt+m` again to pick another color. `[` and `]` to navigate to block boundaries | | `Esc` | Go back to normal mode | | +#### Hex search mode + +This mode has two sub-modes: UTF-8 and HEX. The first is straightforward: search for UTF-8 strings, +but if `ignorecase` is enabled, it will only work with the ASCII character set. + +The HEX sub-mode is used to search for byte sequences. Examples: + +`f0f0ca` (or `F0F0CA`) matches the sequence `f0f0ca` . + +It also has support for basic pattern matching: + +`b0 ?? ca` - `??` means "any byte", so this would match `b0b0ca`, `b0ffca`, `b042ca`... + #### Hex replace mode | Key | Action | Tips | diff --git a/src/app.rs b/src/app.rs index 74ac231..8198a61 100644 --- a/src/app.rs +++ b/src/app.rs @@ -104,7 +104,9 @@ impl App { hex_mode_non_graphic_char: '.', maximum_strings_to_show: 3000, minimum_string_length: 4, - search_wrap: true, + search_wrapscan: false, + search_ignorecase: false, + search_smartcase: false, theme: DARK, // hex_mode_dword_separator: '-', // text_mode_tab_spaces: 4, diff --git a/src/commands.rs b/src/commands.rs index 99030fd..9575a1c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -235,13 +235,33 @@ pub fn parse_command(app: &mut App, cmdline: &str) { } } } - // saarch wrap - "wrapscan" => { - app.config.search_wrap = true; + // search wrap + "wrapscan" | "ws" => { + app.config.search_wrapscan = true; app.dialog_renderer = None; } - "nowrapscan" => { - app.config.search_wrap = false; + "nowrapscan" | "nows" => { + app.config.search_wrapscan = false; + app.dialog_renderer = None; + } + // search ignorecase + "ignorecase" | "ic" => { + app.config.search_ignorecase = true; + app.dialog_renderer = None; + } + "noignorecase" | "noic" => { + app.config.search_ignorecase = false; + app.dialog_renderer = None; + } + // search smartcase + // if ignorecase == true and needle contains a capital letter, + // then ignorecase will be set to false + "smartcase" | "scs" => { + app.config.search_smartcase = true; + app.dialog_renderer = None; + } + "nosmartcase" | "noscs" => { + app.config.search_smartcase = false; app.dialog_renderer = None; } // view diff --git a/src/config.rs b/src/config.rs index 72d6846..09a4156 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,9 @@ pub struct Config { pub hex_mode_non_graphic_char: char, pub maximum_strings_to_show: usize, pub minimum_string_length: usize, - pub search_wrap: bool, + pub search_wrapscan: bool, + pub search_ignorecase: bool, + pub search_smartcase: bool, pub theme: Theme, // pub hex_mode_dword_separator: char, // pub text_mode_tab_spaces: usize, diff --git a/src/hex/events.rs b/src/hex/events.rs index 85c5c22..f292387 100644 --- a/src/hex/events.rs +++ b/src/hex/events.rs @@ -38,19 +38,20 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { && !app.hex_view.search.input_text.value().is_empty() { let needle = app.hex_view.search.input_text.value().to_string(); - ofs = hex::search::search(app, needle); + + ofs = hex::search::search(app, &needle, true); } else if app.hex_view.search.mode == hex::search::SearchMode::Hex && !app.hex_view.search.input_hex.value().is_empty() { let hex_string = app.hex_view.search.input_hex.value().to_string(); - if let Some(by) = hex::search::hex_string_to_u8(&hex_string) { - ofs = hex::search::search(app, &by) - } + ofs = hex::search::search(app, &hex_string, true) } if let Some(ofs) = ofs { app.goto(ofs); + } else { + beep!(); } } } diff --git a/src/hex/search.rs b/src/hex/search.rs index 04f71df..e0d178f 100644 --- a/src/hex/search.rs +++ b/src/hex/search.rs @@ -1,12 +1,15 @@ -use crate::widgets::{Message, MessageType}; -use crate::{app::App, editor::UIState}; +use std::io::Result; + use ratatui::Frame; use ratatui::crossterm::event::{Event, KeyCode}; use ratatui::widgets::Paragraph; -use std::io::Result; +use regex::bytes::Regex; use tui_input::Input; use tui_input::backend::crossterm::EventHandler; +use crate::widgets::{Message, MessageType}; +use crate::{app::App, editor::UIState}; + #[derive(Default, Debug)] pub struct Search { pub input_text: Input, @@ -41,15 +44,157 @@ pub enum SearchDirection { Backward, } -pub fn hex_string_to_u8(hex_string: &str) -> Option> { - if hex_string.is_empty() || !hex_string.len().is_multiple_of(2) { +fn hex_string_to_u8(hex_string: &str) -> Option> { + let s = hex_string.replace(" ", ""); + + if s.is_empty() || !s.len().is_multiple_of(2) { return None; } - let bytes = hex::decode(hex_string).unwrap(); + let bytes = hex::decode(s).unwrap(); Some(bytes) } -pub fn search>(app: &mut App, needle: T) -> Option { +/// returns the fist occurrence of needle, ignoring case (ASCII only for now) +/// so, it won't work with utf-8 characters for example +fn find_nocase(haystack: &[u8], needle: &[u8]) -> Option { + // get first needle char and return early if we can't / needle.len() == 0 + let first = needle.first()?; + let needle_len = needle.len(); + + // we can't search for a needle bigger than the haystack + if needle_len > haystack.len() { + return None; + } + + if first.is_ascii_alphabetic() { + // if needle's first letter is alphabetic, we use memchr2_iter to find both variants + for pos in memchr::memchr2_iter( + first.to_ascii_lowercase(), + first.to_ascii_uppercase(), + haystack, + ) { + let mut matches = 1; // number of matching characters + + for c in needle.iter().skip(1) { + if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) { + break; + } + + matches += 1; + } + + if matches == needle_len { + return Some(pos); + } + } + } else { + // otherwise we call memchr_iter, which is faster + for pos in memchr::memchr_iter(*first, haystack) { + // same logic + let mut matches = 1; + + for c in needle.iter().skip(1) { + if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) { + break; + } + + matches += 1; + } + + if matches == needle_len { + return Some(pos); + } + } + } + + None +} + +fn rfind_nocase(haystack: &[u8], needle: &[u8]) -> Option { + // get first needle char and return early if we can't / needle.len() == 0 + let first = needle.first()?; + let needle_len = needle.len(); + + // we can't search for a needle bigger than the haystack + if needle_len > haystack.len() { + return None; + } + + if first.is_ascii_alphabetic() { + // if needle's first letter is alphabetic, we use memchr2_iter to find both variants + for pos in memchr::memrchr2_iter( + first.to_ascii_lowercase(), + first.to_ascii_uppercase(), + haystack, + ) { + let mut matches = 1; // number of matching characters + + for c in needle.iter().skip(1) { + // continue early if neddle can't fit at current pos to the end of haystack + if haystack.len() - pos + matches < needle_len { + continue; + } + + // if let Some(v) = haystack.get(pos + matches) { + if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) { + break; + } + + matches += 1; + // } + } + + if matches == needle_len { + return Some(pos); + } + } + } else { + // otherwise we call memchr_iter, which is faster + for pos in memchr::memrchr_iter(*first, haystack) { + // same logic + let mut matches = 1; + + for c in needle.iter().skip(1) { + if haystack.len() - pos + matches < needle_len { + continue; + } + + if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) { + break; + } + + matches += 1; + } + + if matches == needle_len { + return Some(pos); + } + } + } + + None +} + +// main search entrypoint +// it will decide whether to call search_literal or search_pattern +pub fn search(app: &mut App, needle: &str, next: bool) -> Option { + if app.hex_view.search.mode == SearchMode::Hex { + // SearchMode::Hex will use search_pattern() if it contains patterns + if needle.contains(|c| ['?', '[', ']'].contains(&c)) { + return search_pattern(app, needle, next); + // otherwise we use literal search, which is faster + } else if let Some(n) = hex_string_to_u8(needle) { + return search_literal(app, n, next); + } else { + return None; + } + } + // SearchMode::Utf8 + search_literal(app, needle, next) +} + +// search literal strings with memchr +pub fn search_literal>(app: &mut App, needle: T, next: bool) -> Option { let text = needle.as_ref(); let filesize = app.file_info.size; let buffer = app.file_info.get_buffer(); @@ -59,16 +204,40 @@ pub fn search>(app: &mut App, needle: T) -> Option { } let ofs = if app.hex_view.search.direction == SearchDirection::Forward { - let start = app.hex_view.offset.checked_add(1)?; + let start = if next { + app.hex_view.offset.checked_add(1)? + } else { + app.hex_view.offset + }; + + let contais_capital_letter = text.iter().any(|b| b.is_ascii_uppercase()); + let smart_search = contais_capital_letter && app.config.search_smartcase; + if start < filesize { - memchr::memmem::find(buffer.get(start..)?, text).map(|pos| start + pos) + if app.config.search_ignorecase && !smart_search { + find_nocase(buffer.get(start..)?, text).map(|pos| start + pos) + } else { + memchr::memmem::find(buffer.get(start..)?, text).map(|pos| start + pos) + } } else { None } } else { - let end = app.hex_view.offset; - if end > 0 { - memchr::memmem::rfind(buffer.get(..end)?, text) + let start = if next { + app.hex_view.offset.checked_sub(1)? + } else { + app.hex_view.offset + }; + + let contais_capital_letter = text.iter().any(|b| b.is_ascii_uppercase()); + let smart_search = contais_capital_letter && app.config.search_smartcase; + + if start < filesize && start != 0 { + if app.config.search_ignorecase && !smart_search { + rfind_nocase(buffer.get(..start)?, text) + } else { + memchr::memmem::rfind(buffer.get(..start)?, text) + } } else { None } @@ -79,7 +248,7 @@ pub fn search>(app: &mut App, needle: T) -> Option { } // ofs is None, check wrap setting - if app.config.search_wrap { + if app.config.search_wrapscan { let ofs = if app.hex_view.search.direction == SearchDirection::Forward { memchr::memmem::find(buffer, text) } else { @@ -95,6 +264,92 @@ pub fn search>(app: &mut App, needle: T) -> Option { None } +fn translate_pattern(pattern: &str) -> Option { + let mut t = String::new(); + let ptn = pattern.replace(" ", ""); + + // because we don't support nibble-level matching yet, + // # of question marks + # of hex digits should be even + if !ptn + .chars() + .filter(|c| c.is_ascii_hexdigit() || *c == '?') + .count() + .is_multiple_of(2) + { + return None; + } + + // 42 to \x42 + let mut next_is_first = true; + + for c in ptn.chars() { + if c.is_ascii_hexdigit() { + if next_is_first { + t.push_str("\\x"); + next_is_first = false; + } else { + next_is_first = true; + } + + t.push(c); + } else if c == '?' { + if next_is_first { + t.push('.'); + next_is_first = false; + } else { + next_is_first = true; + } + } else if ['[', ']', '-'].contains(&c) { + t.push(c); + next_is_first = true; + } + } + + t.insert_str(0, "(?s-u)"); + Some(t) +} + +// search for patterns +pub fn search_pattern(app: &mut App, pattern: &str, next: bool) -> Option { + if pattern.is_empty() { + return None; + } + + if let Some(ptn) = &translate_pattern(pattern) { + if let Ok(re) = Regex::new(ptn) { + let buffer = app.file_info.get_buffer(); + + if app.hex_view.search.direction == SearchDirection::Forward { + let start = if next { + app.hex_view.offset.checked_add(1)? + } else { + app.hex_view.offset + }; + + if let Some(m) = re.find(buffer.get(start..)?) { + return Some(m.start() + start); + } else if app.config.search_wrapscan + && let Some(m) = re.find(buffer) + { + return Some(m.start() + start); + } + } else { + let start = app.hex_view.offset; + + if let Some(m) = re.find_iter(buffer.get(..start)?).last() { + return Some(m.start()); + } else if app.config.search_wrapscan + && let Some(m) = re.find_iter(buffer).last() + { + return Some(m.start()); + } + } + } + } + + None +} + // string // hex pub fn dialog_search_draw(app: &mut App, frame: &mut Frame) { @@ -154,37 +409,26 @@ pub fn dialog_search_events(app: &mut App, event: &Event) -> Result { } }, KeyCode::Enter => { - match app.hex_view.search.mode { - SearchMode::Utf8 => { - let text = app.hex_view.search.input_text.value().to_string(); - app.state = UIState::Normal; + let needle = match app.hex_view.search.mode { + SearchMode::Utf8 => app.hex_view.search.input_text.value().to_string(), + SearchMode::Hex => app.hex_view.search.input_hex.value().to_string(), + }; - if text.is_empty() { - app.dialog_renderer = None; - return Ok(false); - } + if needle.is_empty() { + app.dialog_renderer = None; + app.state = UIState::Normal; + return Ok(true); + } - if let Some(ofs) = search(app, &text) { - app.goto(ofs); - app.dialog_renderer = None; - } else { - app.dialog_renderer = Some(dialog_search_error_draw); - } - } - SearchMode::Hex => { - let hex_string = app.hex_view.search.input_hex.value().to_string(); - - if let Some(bytes) = hex_string_to_u8(&hex_string) { - if let Some(ofs) = search(app, &bytes) { - app.goto(ofs); - app.dialog_renderer = None; - } else { - app.dialog_renderer = Some(dialog_search_error_draw); - } - app.state = UIState::Normal; - } - } - }; + if let Some(ofs) = search(app, &needle, false) { + app.goto(ofs); + app.dialog_renderer = None; + } else { + app.dialog_renderer = Some(dialog_search_error_draw); + crate::beep!(); + } + // TODO: draw "Searching..." + app.state = UIState::Normal; } KeyCode::Tab => { app.hex_view.search.mode.next(); @@ -194,7 +438,9 @@ pub fn dialog_search_events(app: &mut App, event: &Event) -> Result { match app.hex_view.search.mode { SearchMode::Utf8 => app.hex_view.search.input_text.handle_event(event), SearchMode::Hex => { - if c.is_ascii_hexdigit() { + let allowed = [' ', '?']; + + if c.is_ascii_hexdigit() || allowed.contains(&c) { app.hex_view.search.input_hex.handle_event(event) } else { None @@ -214,7 +460,73 @@ pub fn dialog_search_events(app: &mut App, event: &Event) -> Result { } pub fn dialog_search_error_draw(app: &mut App, frame: &mut Frame) { - let mut dialog = Message::from("Pattern not found"); + let mut dialog = Message::from("Not found"); dialog.kind = MessageType::Error; dialog.render(app, frame); } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_find_nocase() { + // found + for needle in [ + b"opengl", b"OpenGL", b"OpengL", b"opengL", b"Opengl", b"OPENGL", + ] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, Some(8)); + } + for needle in [b"i think", b"I think", b"i Think", b"I thinK"] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, Some(0)); + } + for needle in [b"ce", b"cE", b"Ce", b"CE"] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, Some(20)); + } + for needle in [b"x", b"X"] { + let r = find_nocase(b"I think OpenGL is nicX", needle); + assert_eq!(r, Some(21)); + } + + // not found + for needle in [ + b"Aopengl", b"OpeanGL", b"OpengaL", b"ope-ngL", b"Open2gl", b"OPExNGL", + ] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, None); + } + for needle in [b"i t?hink", b"I thi]nk", b"i Thin/k", b"I |thinK"] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, None); + } + for needle in [b"c]e", b"c,E", b"Ce0", b"CE-"] { + let r = find_nocase(b"I think OpenGL is nice", needle); + assert_eq!(r, None); + } + for needle in [b"*", b"?"] { + let r = find_nocase(b"I think OpenGL is nicX", needle); + assert_eq!(r, None); + } + + assert_eq!( + find_nocase(b"\x00\x80\xCD\x6F\x00\x00\xCE\x6F\x00\x80", b"opengl"), + None + ); + } + + #[test] + fn test_translate_pattern() { + assert_eq!( + translate_pattern("FEC01F803F??07"), + Some(r"(?s-u)\xFE\xC0\x1F\x80\x3F.\x07".to_string()) + ); + assert_eq!( + translate_pattern("FEC01F803F[00-ff]07"), + Some(r"(?s-u)\xFE\xC0\x1F\x80\x3F[\x00-\xff]\x07".to_string()) + ); + } +}