diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d0e7e5fe..9ea02f505 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(imhex ${application_type} source/helpers/project_file_handler.cpp source/helpers/loader_script_handler.cpp source/helpers/plugin_handler.cpp + source/helpers/encoding_file.cpp source/providers/file_provider.cpp diff --git a/external/ImGui/include/imgui_memory_editor.h b/external/ImGui/include/imgui_memory_editor.h index 7de1d9207..709746860 100644 --- a/external/ImGui/include/imgui_memory_editor.h +++ b/external/ImGui/include/imgui_memory_editor.h @@ -50,6 +50,8 @@ #include +#include + #ifdef _MSC_VER #define _PRISizeT "I" #define ImSnprintf _snprintf @@ -75,12 +77,19 @@ struct MemoryEditor DataFormat_COUNT }; + struct DecodeData { + std::string data; + size_t advance; + ImColor color; + }; + // Settings bool ReadOnly; // = false // disable any editing. int Cols; // = 16 // number of columns to display. bool OptShowOptions; // = true // display options button/context menu. when disabled, options will be locked unless you provide your own UI for them. bool OptShowHexII; // = false // display values in HexII representation instead of regular hexadecimal: hide null/zero bytes, ascii values as ".X". bool OptShowAscii; // = true // display ASCII representation on the right side. + bool OptShowAdvancedDecoding; // = true // display advanced decoding data on the right side. bool OptGreyOutZeroes; // = true // display null/zero bytes using the TextDisabled color. bool OptUpperCaseHex; // = true // display hexadecimal values as "FF" instead of "ff". int OptMidColsCount; // = 8 // set to 0 to disable extra spacing between every mid-cols. @@ -90,6 +99,7 @@ struct MemoryEditor void (*WriteFn)(ImU8* data, size_t off, ImU8 d); // = 0 // optional handler to write bytes. bool (*HighlightFn)(const ImU8* data, size_t off, bool next);//= 0 // optional handler to return Highlight property (to support non-contiguous highlighting). void (*HoverFn)(const ImU8 *data, size_t off); + DecodeData (*DecodeFn)(const ImU8 *data, size_t off); // [Internal State] bool ContentsWidthChanged; @@ -114,6 +124,7 @@ struct MemoryEditor OptShowOptions = true; OptShowHexII = false; OptShowAscii = true; + OptShowAdvancedDecoding = true; OptGreyOutZeroes = true; OptUpperCaseHex = true; OptMidColsCount = 8; @@ -123,6 +134,7 @@ struct MemoryEditor WriteFn = NULL; HighlightFn = NULL; HoverFn = NULL; + DecodeFn = NULL; // State/Internals ContentsWidthChanged = false; @@ -155,6 +167,8 @@ struct MemoryEditor float PosHexEnd; float PosAsciiStart; float PosAsciiEnd; + float PosDecodingStart; + float PosDecodingEnd; float WindowWidth; Sizes() { memset(this, 0, sizeof(*this)); } @@ -174,12 +188,27 @@ struct MemoryEditor s.PosHexStart = (s.AddrDigitsCount + 2) * s.GlyphWidth; s.PosHexEnd = s.PosHexStart + (s.HexCellWidth * Cols); s.PosAsciiStart = s.PosAsciiEnd = s.PosHexEnd; - if (OptShowAscii) - { + + if (OptShowAscii && OptShowAdvancedDecoding) { s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1; if (OptMidColsCount > 0) s.PosAsciiStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth; + + s.PosDecodingStart = s.PosAsciiEnd + s.GlyphWidth * 1; + if (OptMidColsCount > 0) + s.PosDecodingStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; + s.PosDecodingEnd = s.PosDecodingStart + Cols * s.GlyphWidth; + } else if (OptShowAscii) { + s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1; + if (OptMidColsCount > 0) + s.PosAsciiStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; + s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth; + } else if (OptShowAdvancedDecoding) { + s.PosDecodingStart = s.PosHexEnd + s.GlyphWidth * 1; + if (OptMidColsCount > 0) + s.PosDecodingStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; + s.PosDecodingEnd = s.PosDecodingStart + Cols * s.GlyphWidth; } s.WindowWidth = s.PosAsciiEnd + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth; } @@ -222,15 +251,6 @@ struct MemoryEditor CalcSizes(s, mem_size, base_display_addr); ImGuiStyle& style = ImGui::GetStyle(); - if (mem_size == 0x00) { - constexpr const char *noDataString = "No data loaded!"; - - auto pos = ImGui::GetCursorScreenPos(); - pos.x += (ImGui::GetWindowWidth() - (ImGui::CalcTextSize(noDataString).x)) / 2; - ImGui::GetWindowDrawList()->AddText(pos, 0xFFFFFFFF, noDataString); - return; - } - // We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in order to prevent click from moving the window. // This is used as a facility since our main click detection code doesn't assign an ActiveId so the click would normally be caught as a window-move. const float height_separator = style.ItemSpacing.y; @@ -327,6 +347,8 @@ struct MemoryEditor ImVec2 window_pos = ImGui::GetWindowPos(); if (OptShowAscii) draw_list->AddLine(ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border)); + if (OptShowAdvancedDecoding) + draw_list->AddLine(ImVec2(window_pos.x + s.PosDecodingStart - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.PosDecodingStart - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border)); const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text); const ImU32 color_disabled = OptGreyOutZeroes ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : color_text; @@ -541,6 +563,86 @@ struct MemoryEditor pos.x += s.GlyphWidth; } + + ImGui::PushID(-1); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(s.GlyphWidth, s.LineHeight)); + + ImGui::PopID(); + } + + if (OptShowAdvancedDecoding && DecodeFn) { + // Draw decoded bytes + ImGui::SameLine(s.PosDecodingStart); + ImVec2 pos = ImGui::GetCursorScreenPos(); + addr = line_i * Cols; + + ImGui::PushID(-1); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(s.GlyphWidth, s.LineHeight)); + + ImGui::PopID(); + + for (int n = 0; n < Cols && addr < mem_size;) + { + auto decodedData = DecodeFn(mem_data, addr); + + auto displayData = decodedData.data; + auto decodedDataLength = displayData.length(); + + if (addr == DataEditingAddr) + { + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_FrameBg)); + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_TextSelectedBg)); + } + + draw_list->AddText(pos, decodedData.color, displayData.c_str(), displayData.c_str() + decodedDataLength); + + // Draw highlight + bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax); + bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr, false)); + bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr <= DataPreviewAddrEnd) || (addr >= DataPreviewAddrEnd && addr <= DataPreviewAddr); + if (is_highlight_from_user_range || is_highlight_from_user_func || is_highlight_from_preview) + { + ImU32 color = HighlightColor; + if ((is_highlight_from_user_range + is_highlight_from_user_func + is_highlight_from_preview) > 1) + color = (ImAlphaBlendColors(HighlightColor, 0x60C08080) & 0x00FFFFFF) | 0x90000000; + + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth * decodedDataLength, pos.y + s.LineHeight), color); + } + + + ImGui::PushID(line_i * Cols + n); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(s.GlyphWidth * decodedDataLength, s.LineHeight)); + + ImGui::PopID(); + + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && !ImGui::GetIO().KeyShift) + { + if (!ReadOnly && ImGui::IsMouseDoubleClicked(0)) { + DataEditingTakeFocus = true; + data_editing_addr_next = addr; + } + + DataPreviewAddr = addr; + DataPreviewAddrEnd = addr; + + } + if (ImGui::IsItemHovered() && ((ImGui::IsMouseClicked(0) && ImGui::GetIO().KeyShift) || ImGui::IsMouseDragging(0))) { + DataPreviewAddrEnd = addr; + } + + pos.x += s.GlyphWidth * decodedDataLength; + + if (addr <= 1) { + n++; + addr++; + } else { + n += decodedData.advance; + addr += decodedData.advance; + } + } } } IM_ASSERT(clipper.Step() == false); @@ -585,6 +687,7 @@ struct MemoryEditor ImGui::PopItemWidth(); ImGui::Checkbox("Show HexII", &OptShowHexII); if (ImGui::Checkbox("Show Ascii", &OptShowAscii)) { ContentsWidthChanged = true; } + if (ImGui::Checkbox("Show Advanced Decoding", &OptShowAdvancedDecoding)) { ContentsWidthChanged = true; } ImGui::Checkbox("Grey out zeroes", &OptGreyOutZeroes); ImGui::Checkbox("Uppercase Hex", &OptUpperCaseHex); diff --git a/include/helpers/encoding_file.hpp b/include/helpers/encoding_file.hpp new file mode 100644 index 000000000..8bd4b5274 --- /dev/null +++ b/include/helpers/encoding_file.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include +#include + +namespace hex { + + template + struct SizeSorter { + bool operator() (const T& lhs, const T& rhs) const { + return lhs.size() < rhs.size(); + } + }; + + class EncodingFile { + public: + enum class Type { + Thingy, + CSV + }; + + EncodingFile() = default; + EncodingFile(Type type, std::string_view path); + + std::pair getEncodingFor(const std::vector &buffer) const; + size_t getLongestSequence() const { return this->m_longestSequence; } + + private: + void parseThingyFile(std::ifstream &content); + + std::map, std::string>> m_mapping; + size_t m_longestSequence = 0; + }; + +} \ No newline at end of file diff --git a/include/views/view_hexeditor.hpp b/include/views/view_hexeditor.hpp index 2f50ee126..f8f8e6b2f 100644 --- a/include/views/view_hexeditor.hpp +++ b/include/views/view_hexeditor.hpp @@ -2,6 +2,7 @@ #include #include +#include "helpers/encoding_file.hpp" #include #include @@ -54,6 +55,8 @@ namespace hex { std::string m_loaderScriptScriptPath; std::string m_loaderScriptFilePath; + hex::EncodingFile m_currEncodingFile; + void drawSearchPopup(); void drawGotoPopup(); void drawEditPopup(); diff --git a/plugins/builtin/source/lang/en_US.cpp b/plugins/builtin/source/lang/en_US.cpp index a64813da0..e2b96d279 100644 --- a/plugins/builtin/source/lang/en_US.cpp +++ b/plugins/builtin/source/lang/en_US.cpp @@ -130,6 +130,7 @@ namespace hex::plugin::builtin { { "hex.view.hexeditor.save_project", "Save Project" }, { "hex.view.hexeditor.save_data", "Save Data" }, { "hex.view.hexeditor.open_base64", "Open Base64 File" }, + { "hex.view.hexeditor.load_enconding_file", "Load custom encoding File" }, { "hex.view.hexeditor.page", "Page %d / %d" }, { "hex.view.hexeditor.save_as", "Save As" }, { "hex.view.hexeditor.save_changes.title", "Save Changes" }, @@ -146,6 +147,7 @@ namespace hex::plugin::builtin { { "hex.view.hexeditor.menu.file.save_as", "Save As..." }, { "hex.view.hexeditor.menu.file.open_project", "Open Project..." }, { "hex.view.hexeditor.menu.file.save_project", "Save Project..." }, + { "hex.view.hexeditor.menu.file.load_encoding_file", "Load custom encoding..." }, { "hex.view.hexeditor.menu.file.import", "Import..." }, { "hex.view.hexeditor.menu.file.import.base64", "Base64 File" }, { "hex.view.hexeditor.base64.import_error", "File is not in a valid Base64 format!" }, diff --git a/plugins/libimhex/include/hex/helpers/utils.hpp b/plugins/libimhex/include/hex/helpers/utils.hpp index f4fced0a3..b8d3388e7 100644 --- a/plugins/libimhex/include/hex/helpers/utils.hpp +++ b/plugins/libimhex/include/hex/helpers/utils.hpp @@ -180,6 +180,23 @@ namespace hex { return bytes; } + inline std::vector parseByteString(std::string_view string) { + auto byteString = std::string(string); + byteString.erase(std::remove(byteString.begin(), byteString.end(), ' '), byteString.end()); + + if ((byteString.length() % 2) != 0) return { }; + + std::vector result; + for (u32 i = 0; i < byteString.length(); i += 2) { + if (!std::isxdigit(byteString[i]) || !std::isxdigit(byteString[i + 1])) + return { }; + + result.push_back(std::strtoul(byteString.substr(i, 2).c_str(), nullptr, 16)); + } + + return result; + } + inline std::string toBinaryString(hex::integral auto number) { if (number == 0) return "0"; @@ -190,6 +207,23 @@ namespace hex { return result; } + inline void trimLeft(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + } + + inline void trimRight(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); + } + + inline void trim(std::string &s) { + trimLeft(s); + trimRight(s); + } + #define SCOPE_EXIT(func) ScopeExit TOKEN_CONCAT(scopeGuard, __COUNTER__)([&] { func }) class ScopeExit { public: diff --git a/plugins/libimhex/source/api/content_registry.cpp b/plugins/libimhex/source/api/content_registry.cpp index 2d217c7a2..4f12c226b 100644 --- a/plugins/libimhex/source/api/content_registry.cpp +++ b/plugins/libimhex/source/api/content_registry.cpp @@ -30,8 +30,6 @@ namespace hex { json[unlocalizedCategory.data()] = nlohmann::json::object(); if (!json[unlocalizedCategory.data()].contains(unlocalizedName.data())) json[unlocalizedCategory.data()][unlocalizedName.data()] = defaultValue; - - Settings::store(); } void ContentRegistry::Settings::add(std::string_view unlocalizedCategory, std::string_view unlocalizedName, std::string_view defaultValue, const std::function &callback) { @@ -43,8 +41,6 @@ namespace hex { json[unlocalizedCategory.data()] = nlohmann::json::object(); if (!json[unlocalizedCategory.data()].contains(unlocalizedName.data())) json[unlocalizedCategory.data()][unlocalizedName.data()] = defaultValue; - - Settings::store(); } void ContentRegistry::Settings::write(std::string_view unlocalizedCategory, std::string_view unlocalizedName, s64 value) { diff --git a/source/helpers/encoding_file.cpp b/source/helpers/encoding_file.cpp new file mode 100644 index 000000000..bc29265dd --- /dev/null +++ b/source/helpers/encoding_file.cpp @@ -0,0 +1,53 @@ +#include "helpers/encoding_file.hpp" + +#include + +#include + +namespace hex { + + EncodingFile::EncodingFile(Type type, std::string_view path) { + std::ifstream encodingFile(path.data()); + + switch (type) { + case Type::Thingy: parseThingyFile(encodingFile); break; + default: throw std::runtime_error("Invalid encoding file type"); + } + } + + std::pair EncodingFile::getEncodingFor(const std::vector &buffer) const { + for (auto iter = this->m_mapping.rbegin(); iter != this->m_mapping.rend(); iter++) { + auto &[size, mapping] = *iter; + + if (size > buffer.size()) continue; + + auto key = std::vector(buffer.begin(), buffer.begin() + size); + if (mapping.contains(key)) + return { mapping.at(key), size }; + } + + return { ".", 1 }; + } + + void EncodingFile::parseThingyFile(std::ifstream &content) { + for (std::string line; std::getline(content, line);) { + auto entry = hex::splitString(line, "="); + + if (entry.size() != 2) return; + + auto &from = entry[0]; + auto &to = entry[1]; + + hex::trim(from); + hex::trim(to); + + auto fromBytes = hex::parseByteString(from); + if (!this->m_mapping.contains(fromBytes.size())) + this->m_mapping.insert({ fromBytes.size(), { } }); + this->m_mapping[fromBytes.size()].insert({ fromBytes, to }); + + this->m_longestSequence = std::max(this->m_longestSequence, fromBytes.size()); + } + } + +} \ No newline at end of file diff --git a/source/views/view_hexeditor.cpp b/source/views/view_hexeditor.cpp index 3cdc2bed1..58aa530e9 100644 --- a/source/views/view_hexeditor.cpp +++ b/source/views/view_hexeditor.cpp @@ -99,6 +99,30 @@ namespace hex { ImGui::EndTooltip(); }; + this->m_memoryEditor.DecodeFn = [](const ImU8 *data, size_t addr) -> MemoryEditor::DecodeData { + ViewHexEditor *_this = (ViewHexEditor *) data; + + if (_this->m_currEncodingFile.getLongestSequence() == 0) + return { ".", 1, 0xFFFF8000 }; + + auto &provider = SharedData::currentProvider; + size_t size = std::min(_this->m_currEncodingFile.getLongestSequence(), provider->getActualSize() - addr); + + std::vector buffer(size); + provider->read(addr, buffer.data(), size); + + auto [decoded, advance] = _this->m_currEncodingFile.getEncodingFor(buffer); + + ImColor color; + if (decoded.length() == 1 && std::isalnum(decoded[0])) color = 0xFFFF8000; + else if (decoded.length() == 1 && advance == 1) color = 0xFF0000FF; + else if (decoded.length() > 1 && advance == 1) color = 0xFF00FFFF; + else if (advance > 1) color = 0xFFFFFFFF; + else color = 0xFFFF8000; + + return { std::string(decoded), advance, color }; + }; + View::subscribeEvent(Events::FileDropped, [this](auto userData) { auto filePath = std::any_cast(userData); @@ -358,6 +382,12 @@ namespace hex { ProjectFile::store(); } + if (ImGui::MenuItem("hex.view.hexeditor.menu.file.load_encoding_file"_lang)) { + View::openFileBrowser("hex.view.hexeditor.load_enconding_file"_lang, imgui_addons::ImGuiFileBrowser::DialogMode::OPEN, "*.*", [this](auto path) { + this->m_currEncodingFile = EncodingFile(EncodingFile::Type::Thingy, path); + }); + } + ImGui::Separator(); if (ImGui::BeginMenu("hex.view.hexeditor.menu.file.import"_lang)) { @@ -515,6 +545,9 @@ namespace hex { if (!provider->isAvailable()) { View::showErrorPopup("hex.view.hexeditor.error.open"_lang); + delete provider; + provider = nullptr; + return; }