From 66efcf91d36961bd5ff46fa2aa33f1e6a9fa862a Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sun, 24 Aug 2025 21:21:34 +0200 Subject: [PATCH] feat: Added file information command line option and fullscreen view --- lib/external/pattern_language | 2 +- lib/libimhex/include/hex/helpers/magic.hpp | 11 ++ lib/libimhex/source/helpers/magic.cpp | 125 ++++++++++++++++ plugins/builtin/CMakeLists.txt | 1 + .../content/command_line_interface.hpp | 1 + .../fullscreen/view_fullscreen_file_info.hpp | 28 ++++ .../content/views/view_pattern_editor.hpp | 9 +- plugins/builtin/romfs/lang/en_US.json | 8 ++ .../source/content/command_line_interface.cpp | 16 +++ .../fullscreen/view_fullscreen_file_info.cpp | 112 +++++++++++++++ .../view_fullscreen_save_editor.cpp | 1 - .../content/views/view_pattern_editor.cpp | 136 ++---------------- plugins/builtin/source/plugin_builtin.cpp | 5 +- 13 files changed, 323 insertions(+), 132 deletions(-) create mode 100644 plugins/builtin/include/content/views/fullscreen/view_fullscreen_file_info.hpp create mode 100644 plugins/builtin/source/content/views/fullscreen/view_fullscreen_file_info.cpp diff --git a/lib/external/pattern_language b/lib/external/pattern_language index c2e4a6752..153d1d9ce 160000 --- a/lib/external/pattern_language +++ b/lib/external/pattern_language @@ -1 +1 @@ -Subproject commit c2e4a6752ff4b4419d45414fb53d65470bfff97c +Subproject commit 153d1d9ce987c1df238403dc781991bba22e26ec diff --git a/lib/libimhex/include/hex/helpers/magic.hpp b/lib/libimhex/include/hex/helpers/magic.hpp index fb0578cc3..28ae3abdd 100644 --- a/lib/libimhex/include/hex/helpers/magic.hpp +++ b/lib/libimhex/include/hex/helpers/magic.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -27,4 +28,14 @@ namespace hex::magic { bool isValidMIMEType(const std::string &mimeType); + struct FoundPattern { + std::fs::path patternFilePath; + std::string author; + std::string description; + std::optional mimeType; + std::optional magicOffset; + }; + + std::vector findViablePatterns(prv::Provider *provider); + } \ No newline at end of file diff --git a/lib/libimhex/source/helpers/magic.cpp b/lib/libimhex/source/helpers/magic.cpp index 3979e3450..07a048bc0 100644 --- a/lib/libimhex/source/helpers/magic.cpp +++ b/lib/libimhex/source/helpers/magic.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include #if defined(_MSC_VER) #include @@ -230,4 +232,127 @@ namespace hex::magic { return true; } + + std::vector findViablePatterns(prv::Provider *provider) { + std::vector result; + + pl::PatternLanguage runtime; + ContentRegistry::PatternLanguage::configureRuntime(runtime, provider); + + bool foundCorrectType = false; + + auto mimeType = getMIMEType(provider, 0, 4_KiB, true); + + std::error_code errorCode; + for (const auto &dir : paths::Patterns.read()) { + for (auto &entry : std::fs::recursive_directory_iterator(dir, errorCode)) { + foundCorrectType = false; + if (!entry.is_regular_file()) + continue; + + wolv::io::File file(entry.path(), wolv::io::File::Mode::Read); + if (!file.isValid()) + continue; + + std::string author, description; + bool matchedMimeType = false; + std::optional magicOffset; + + + const auto pragmaValues = runtime.getPragmaValues(file.readString()); + if (auto it = pragmaValues.find("author"); it != pragmaValues.end()) + author = it->second; + if (auto it = pragmaValues.find("description"); it != pragmaValues.end()) + description = it->second; + + // Format: #pragma MIME type/subtype + for (auto [it, itEnd] = pragmaValues.equal_range("MIME"); it != itEnd; ++it) { + if (isValidMIMEType(it->second) && it->second == mimeType) { + foundCorrectType = true; + matchedMimeType = true; + } + } + // Format: #pragma magic [ AA BB CC DD ] @ 0x12345678 + for (auto [it, itEnd] = pragmaValues.equal_range("magic"); it != itEnd; ++it) { + const auto pattern = [value = it->second]() mutable -> std::optional { + value = wolv::util::trim(value); + + if (value.empty()) + return std::nullopt; + + if (!value.starts_with('[')) + return std::nullopt; + + value = value.substr(1); + + const auto end = value.find(']'); + if (end == std::string::npos) + return std::nullopt; + value.resize(end); + + value = wolv::util::trim(value); + + return BinaryPattern(value); + }(); + + const auto address = [provider, value = it->second]() mutable -> std::optional { + value = wolv::util::trim(value); + + if (value.empty()) + return std::nullopt; + + const auto start = value.find('@'); + if (start == std::string::npos) + return std::nullopt; + + value = value.substr(start + 1); + value = wolv::util::trim(value); + + size_t end = 0; + auto result = std::stoll(value, &end, 0); + if (end != value.length()) + return std::nullopt; + + if (result < 0) { + const auto size = provider->getActualSize(); + if (u64(-result) > size) { + return std::nullopt; + } + + return size + result; + } else { + return result; + } + }(); + + if (address && pattern) { + std::vector bytes(pattern->getSize()); + if (!bytes.empty()) { + provider->read(*address, bytes.data(), bytes.size()); + + if (pattern->matches(bytes)) { + foundCorrectType = true; + magicOffset = *address; + } + } + } + } + + if (foundCorrectType) { + result.emplace_back( + entry.path(), + std::move(author), + std::move(description), + matchedMimeType ? std::make_optional(mimeType) : std::nullopt, + magicOffset + ); + } + + runtime.reset(); + } + } + + return result; + } + } \ No newline at end of file diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 81aecba73..6faea9c60 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -120,6 +120,7 @@ add_imhex_plugin( source/content/views/view_tutorials.cpp source/content/views/fullscreen/view_fullscreen_save_editor.cpp + source/content/views/fullscreen/view_fullscreen_file_info.cpp source/content/text_highlighting/pattern_language.cpp INCLUDES diff --git a/plugins/builtin/include/content/command_line_interface.hpp b/plugins/builtin/include/content/command_line_interface.hpp index d21a0687b..9718f519a 100644 --- a/plugins/builtin/include/content/command_line_interface.hpp +++ b/plugins/builtin/include/content/command_line_interface.hpp @@ -29,6 +29,7 @@ namespace hex::plugin::builtin { void handleDebugModeCommand(const std::vector &args); void handleValidatePluginCommand(const std::vector &args); void handleSaveEditorCommand(const std::vector &args); + void handleFileInfoCommand(const std::vector &args); void registerCommandForwarders(); diff --git a/plugins/builtin/include/content/views/fullscreen/view_fullscreen_file_info.hpp b/plugins/builtin/include/content/views/fullscreen/view_fullscreen_file_info.hpp new file mode 100644 index 000000000..a3320e0ea --- /dev/null +++ b/plugins/builtin/include/content/views/fullscreen/view_fullscreen_file_info.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace hex::plugin::builtin { + + class ViewFullScreenFileInfo : public View::FullScreen { + public: + explicit ViewFullScreenFileInfo(std::fs::path filePath); + + void drawContent() override; + + private: + std::fs::path m_filePath; + FileProvider m_provider; + TaskHolder m_analysisTask; + + std::string m_mimeType; + std::string m_fileDescription; + std::vector m_foundPatterns; + std::optional m_fullDescription; + }; + +} diff --git a/plugins/builtin/include/content/views/view_pattern_editor.hpp b/plugins/builtin/include/content/views/view_pattern_editor.hpp index b44ad884e..091e2863b 100644 --- a/plugins/builtin/include/content/views/view_pattern_editor.hpp +++ b/plugins/builtin/include/content/views/view_pattern_editor.hpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace pl::ptrn { class Pattern; } @@ -114,16 +115,10 @@ namespace hex::plugin::builtin { u32 color; }; - struct PossiblePattern { - std::fs::path path; - std::string author; - std::string description; - }; - std::unique_ptr m_editorRuntime; std::mutex m_possiblePatternFilesMutex; - PerProvider> m_possiblePatternFiles; + PerProvider> m_possiblePatternFiles; bool m_runAutomatically = false; bool m_triggerEvaluation = false; std::atomic m_triggerAutoEvaluate = false; diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 9f5b6881e..b3faefd85 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -794,6 +794,14 @@ "hex.builtin.view.find.value.max": "Maximum Value", "hex.builtin.view.find.value.min": "Minimum Value", "hex.builtin.view.find.value.range": "Ranged Search", + "hex.builtin.view.fullscreen.file_info.error.file_not_readable": "The selected file could not be opened. Please ensure the file exists and is readable.", + "hex.builtin.view.fullscreen.file_info.error.not_identified": "Failed to identify the type of this file.", + "hex.builtin.view.fullscreen.file_info.analyzing": "Analyzing Data...", + "hex.builtin.view.fullscreen.file_info.match_info": "Match Information", + "hex.builtin.view.fullscreen.file_info.match_info.mime": "Matched using MIME Type", + "hex.builtin.view.fullscreen.file_info.match_info.magic": "Matched using Magic value at offset 0x{0:04X}", + "hex.builtin.view.fullscreen.file_info.information": "Information", + "hex.builtin.view.fullscreen.file_info.no_information": "No further information available.", "hex.builtin.view.help.about.commits": "Commit History", "hex.builtin.view.help.about.contributor": "Contributors", "hex.builtin.view.help.about.donations": "Donations", diff --git a/plugins/builtin/source/content/command_line_interface.cpp b/plugins/builtin/source/content/command_line_interface.cpp index 73b6ea377..60ef12067 100644 --- a/plugins/builtin/source/content/command_line_interface.cpp +++ b/plugins/builtin/source/content/command_line_interface.cpp @@ -29,6 +29,7 @@ #include #include +#include namespace hex::plugin::builtin { using namespace hex::literals; @@ -521,6 +522,21 @@ namespace hex::plugin::builtin { } } + void handleFileInfoCommand(const std::vector &args) { + if (args.size() != 1) { + log::println("usage: imhex --file-info "); + std::exit(EXIT_FAILURE); + } + + const auto path = std::fs::path(args[0]); + if (!wolv::io::fs::exists(path)) { + log::println("File '{}' does not exist!", args[0]); + std::exit(EXIT_FAILURE); + } + + ContentRegistry::Views::setFullScreenView(path); + } + void registerCommandForwarders() { hex::subcommands::registerSubCommand("open", [](const std::vector &args){ diff --git a/plugins/builtin/source/content/views/fullscreen/view_fullscreen_file_info.cpp b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_file_info.cpp new file mode 100644 index 000000000..26d0d97f6 --- /dev/null +++ b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_file_info.cpp @@ -0,0 +1,112 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace hex::plugin::builtin { + + using namespace hex::literals; + + ViewFullScreenFileInfo::ViewFullScreenFileInfo(std::fs::path filePath) : m_filePath(std::move(filePath)) { + this->m_provider.setPath(m_filePath); + if (!this->m_provider.open()) { + ui::ToastError::open("hex.builtin.view.fullscreen.file_info.error.file_not_readable"_lang); + } + + m_analysisTask = TaskManager::createBlockingTask("hex.builtin.view.fullscreen.file_info.analyzing", TaskManager::NoProgress, [this] { + m_mimeType = magic::getMIMEType(&m_provider); + if (!magic::isValidMIMEType(m_mimeType)) { + m_mimeType.clear(); + } else { + m_fileDescription = magic::getDescription(&m_provider, 0, 100_KiB, true); + } + + m_foundPatterns = magic::findViablePatterns(&m_provider); + if (!m_foundPatterns.empty()) { + pl::PatternLanguage runtime; + ContentRegistry::PatternLanguage::configureRuntime(runtime, &m_provider); + + constexpr static auto DataDescriptionFunction = "get_data_description"; + if (runtime.executeFile(m_foundPatterns.front().patternFilePath)) { + const auto &evaluator = runtime.getInternals().evaluator; + const auto &functions = evaluator->getCustomFunctions(); + if (const auto function = functions.find(DataDescriptionFunction); function != functions.end()) { + if (const auto value = function->second.func(evaluator.get(), {}); value.has_value()) { + if (value->isString()) { + m_fullDescription = ui::Markdown(value->toString()); + } + } + } + } + } + }); + + } + + void ViewFullScreenFileInfo::drawContent() { + if (!m_provider.isReadable()) { + return; + } + + if (m_analysisTask.isRunning()) { + return; + } + + const bool foundPatterns = !m_foundPatterns.empty(); + const bool hasMimeType = !m_mimeType.empty(); + if (!foundPatterns && !hasMimeType) { + ImGuiExt::TextFormattedCentered("hex.builtin.view.fullscreen.file_info.error.not_identified"_lang); + return; + } + + if (foundPatterns) { + const auto &firstMatch = m_foundPatterns.front(); + + ImGui::BeginGroup(); + + fonts::Default().pushBold(1.2F); + ImGuiExt::TextFormattedCenteredHorizontal("{}", firstMatch.description); + fonts::Default().pop(); + + if (hasMimeType) + ImGuiExt::TextFormattedCenteredHorizontal("{}", m_mimeType); + if (!m_fileDescription.empty()) + ImGuiExt::TextFormattedCenteredHorizontal("{}", m_fileDescription); + + ImGui::EndGroup(); + + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowSize().x - 300_scaled); + + if (ImGuiExt::BeginSubWindow("hex.builtin.view.fullscreen.file_info.match_info"_lang)) { + if (firstMatch.mimeType.has_value()) + ImGuiExt::TextFormattedWrapped("hex.builtin.view.fullscreen.file_info.match_info.mime"_lang); + else if (firstMatch.magicOffset.has_value()) + ImGuiExt::TextFormattedWrapped("hex.builtin.view.fullscreen.file_info.match_info.magic"_lang, *firstMatch.magicOffset); + } + ImGuiExt::EndSubWindow(); + } else { + fonts::Default().pushBold(1.2F); + ImGuiExt::TextFormattedCenteredHorizontal("{}", m_mimeType); + fonts::Default().pop(); + } + + + ImGui::NewLine(); + + if (ImGuiExt::BeginSubWindow("hex.builtin.view.fullscreen.file_info.information"_lang, nullptr, ImGui::GetContentRegionAvail())) { + if (m_fullDescription.has_value()) + m_fullDescription->draw(); + else + ImGuiExt::TextFormattedCentered("hex.builtin.view.fullscreen.file_info.no_information"_lang); + } + ImGuiExt::EndSubWindow(); + } + +} diff --git a/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp index 4167813d5..5e849cbb8 100644 --- a/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp +++ b/plugins/builtin/source/content/views/fullscreen/view_fullscreen_save_editor.cpp @@ -70,7 +70,6 @@ namespace hex::plugin::builtin { if (!this->m_provider.open()) { ui::ToastError::open("The selected file could not be opened. Please ensure the file exists and is readable."); } - this->m_provider.convertToMemoryFile(); ContentRegistry::PatternLanguage::configureRuntime(m_runtime, &m_provider); if (!m_runtime.executeString(this->m_sourceCode)) { diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index 37522de38..e6433d3dc 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -70,7 +70,7 @@ namespace hex::plugin::builtin { if (ImGui::BeginListBox("##patterns_accept", ImVec2(400_scaled, 0))) { u32 index = 0; - for (const auto &[path, author, description] : m_view->m_possiblePatternFiles.get(provider)) { + for (const auto &[path, author, description, mimeType, magicOffset] : m_view->m_possiblePatternFiles.get(provider)) { ImGui::PushID(index + 1); auto fileName = wolv::util::toUTF8String(path.filename()); @@ -105,7 +105,7 @@ namespace hex::plugin::builtin { } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) - m_view->loadPatternFile(m_view->m_possiblePatternFiles.get(provider)[m_selectedPatternFile].path, provider, false); + m_view->loadPatternFile(m_view->m_possiblePatternFiles.get(provider)[m_selectedPatternFile].patternFilePath, provider, false); ImGuiExt::InfoTooltip(wolv::util::toUTF8String(path).c_str()); @@ -127,7 +127,7 @@ namespace hex::plugin::builtin { ImGuiExt::ConfirmButtons("hex.ui.common.yes"_lang, "hex.ui.common.no"_lang, [this, provider] { - m_view->loadPatternFile(m_view->m_possiblePatternFiles.get(provider)[m_selectedPatternFile].path, provider, false); + m_view->loadPatternFile(m_view->m_possiblePatternFiles.get(provider)[m_selectedPatternFile].patternFilePath, provider, false); this->close(); }, [this] { @@ -1495,129 +1495,17 @@ namespace hex::plugin::builtin { if (m_shouldAnalyze) { m_shouldAnalyze = false; - m_analysisTask = TaskManager::createBackgroundTask("hex.builtin.task.analyzing_data", [this, provider](const Task &task) { + m_analysisTask = TaskManager::createBackgroundTask("hex.builtin.task.analyzing_data", [this, provider] { if (!m_autoLoadPatterns) return; - pl::PatternLanguage runtime; - ContentRegistry::PatternLanguage::configureRuntime(runtime, provider); + auto foundPatterns = magic::findViablePatterns(provider); - bool foundCorrectType = false; + if (!foundPatterns.empty()) { + std::scoped_lock lock(m_possiblePatternFilesMutex); - auto mimeType = magic::getMIMEType(provider, 0, 4_KiB, true); - - m_possiblePatternFiles.get(provider).clear(); - - bool popupOpen = false; - std::error_code errorCode; - for (const auto &dir : paths::Patterns.read()) { - for (auto &entry : std::fs::recursive_directory_iterator(dir, errorCode)) { - task.update(); - - foundCorrectType = false; - if (!entry.is_regular_file()) - continue; - - wolv::io::File file(entry.path(), wolv::io::File::Mode::Read); - if (!file.isValid()) - continue; - - std::string author, description; - - const auto pragmaValues = runtime.getPragmaValues(file.readString()); - if (auto it = pragmaValues.find("author"); it != pragmaValues.end()) - author = it->second; - if (auto it = pragmaValues.find("description"); it != pragmaValues.end()) - description = it->second; - - // Format: #pragma MIME type/subtype - if (auto it = pragmaValues.find("MIME"); it != pragmaValues.end()) { - if (magic::isValidMIMEType(it->second) && it->second == mimeType) - foundCorrectType = true; - } - // Format: #pragma magic [ AA BB CC DD ] @ 0x12345678 - if (auto it = pragmaValues.find("magic"); it != pragmaValues.end()) { - const auto pattern = [value = it->second]() mutable -> std::optional { - value = wolv::util::trim(value); - - if (value.empty()) - return std::nullopt; - - if (!value.starts_with('[')) - return std::nullopt; - - value = value.substr(1); - - const auto end = value.find(']'); - if (end == std::string::npos) - return std::nullopt; - value.resize(end); - - value = wolv::util::trim(value); - - return BinaryPattern(value); - }(); - - const auto address = [value = it->second, provider]() mutable -> std::optional { - value = wolv::util::trim(value); - - if (value.empty()) - return std::nullopt; - - const auto start = value.find('@'); - if (start == std::string::npos) - return std::nullopt; - - value = value.substr(start + 1); - value = wolv::util::trim(value); - - size_t end = 0; - auto result = std::stoll(value, &end, 0); - if (end != value.length()) - return std::nullopt; - - if (result < 0) { - const auto size = provider->getActualSize(); - if (u64(-result) > size) { - return std::nullopt; - } - - return size + result; - } else { - return result; - } - }(); - - if (address && pattern) { - std::vector bytes(pattern->getSize()); - if (!bytes.empty()) { - provider->read(*address, bytes.data(), bytes.size()); - - if (pattern->matches(bytes)) - foundCorrectType = true; - } - } - } - - if (foundCorrectType) { - { - std::scoped_lock lock(m_possiblePatternFilesMutex); - - m_possiblePatternFiles.get(provider).emplace_back( - entry.path(), - std::move(author), - std::move(description) - ); - } - - if (!popupOpen) { - PopupAcceptPattern::open(this); - popupOpen = true; - } - } - - runtime.reset(); - } + m_possiblePatternFiles.get(provider) = std::move(foundPatterns); + PopupAcceptPattern::open(this); } }); } @@ -1959,6 +1847,9 @@ namespace hex::plugin::builtin { RequestSetPatternLanguageCode::subscribe(this, [this](const std::string &code) { auto provider = ImHexApi::Provider::get(); + if (provider == nullptr) + return; + m_textEditor.get(provider).setText(wolv::util::preprocessText(code)); m_sourceCode.get(provider) = code; m_hasUnevaluatedChanges.get(provider) = true; @@ -2363,6 +2254,9 @@ namespace hex::plugin::builtin { ContentRegistry::FileTypeHandler::add({ ".hexpat", ".pat" }, [](const std::fs::path &path) -> bool { wolv::io::File file(path, wolv::io::File::Mode::Read); + if (!ImHexApi::Provider::isValid()) + return false; + if (file.isValid()) { RequestSetPatternLanguageCode::post(wolv::util::preprocessText(file.readString())); return true; diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index 32c50206a..17145dae6 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -73,8 +73,8 @@ IMHEX_PLUGIN_SUBCOMMANDS() { { "open", "o", "Open files passed as argument. [default]", hex::plugin::builtin::handleOpenCommand }, { "new", "n", "Create a new empty file", hex::plugin::builtin::handleNewCommand }, - { "select", "", "Select a range of bytes in the Hex Editor", hex::plugin::builtin::handleSelectCommand }, - { "pattern", "", "Sets the loaded pattern", hex::plugin::builtin::handlePatternCommand }, + { "select", "s", "Select a range of bytes in the Hex Editor", hex::plugin::builtin::handleSelectCommand }, + { "pattern", "p", "Sets the loaded pattern", hex::plugin::builtin::handlePatternCommand }, { "calc", "", "Evaluate a mathematical expression", hex::plugin::builtin::handleCalcCommand }, { "hash", "", "Calculate the hash of a file", hex::plugin::builtin::handleHashCommand }, { "encode", "", "Encode a string", hex::plugin::builtin::handleEncodeCommand }, @@ -87,6 +87,7 @@ IMHEX_PLUGIN_SUBCOMMANDS() { { "debug-mode", "", "Enables debugging features", hex::plugin::builtin::handleDebugModeCommand, }, { "validate-plugin", "", "Validates that a plugin can be loaded", hex::plugin::builtin::handleValidatePluginCommand }, { "save-editor", "", "Opens a pattern file for save file editing", hex::plugin::builtin::handleSaveEditorCommand }, + { "file-info", "i", "Displays information about a file", hex::plugin::builtin::handleFileInfoCommand }, }; IMHEX_PLUGIN_SETUP_BUILTIN("Built-in", "WerWolv", "Default ImHex functionality") {