feat: Added file information command line option and fullscreen view

This commit is contained in:
WerWolv
2025-08-24 21:21:34 +02:00
parent 0178ba014a
commit 66efcf91d3
13 changed files with 323 additions and 132 deletions

View File

@@ -3,6 +3,7 @@
#include <hex.hpp>
#include <hex/helpers/literals.hpp>
#include <hex/helpers/fs.hpp>
#include <string>
#include <vector>
@@ -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<std::string> mimeType;
std::optional<u64> magicOffset;
};
std::vector<FoundPattern> findViablePatterns(prv::Provider *provider);
}

View File

@@ -15,6 +15,8 @@
#include <string>
#include <magic.h>
#include <hex/api/content_registry/pattern_language.hpp>
#include <hex/helpers/binary_pattern.hpp>
#if defined(_MSC_VER)
#include <direct.h>
@@ -230,4 +232,127 @@ namespace hex::magic {
return true;
}
std::vector<FoundPattern> findViablePatterns(prv::Provider *provider) {
std::vector<FoundPattern> 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<u64> 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<BinaryPattern> {
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<u64> {
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<u8> 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;
}
}

View File

@@ -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

View File

@@ -29,6 +29,7 @@ namespace hex::plugin::builtin {
void handleDebugModeCommand(const std::vector<std::string> &args);
void handleValidatePluginCommand(const std::vector<std::string> &args);
void handleSaveEditorCommand(const std::vector<std::string> &args);
void handleFileInfoCommand(const std::vector<std::string> &args);
void registerCommandForwarders();

View File

@@ -0,0 +1,28 @@
#pragma once
#include <content/providers/file_provider.hpp>
#include <hex/api/task_manager.hpp>
#include <hex/helpers/magic.hpp>
#include <hex/ui/view.hpp>
#include <ui/markdown.hpp>
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<magic::FoundPattern> m_foundPatterns;
std::optional<ui::Markdown> m_fullDescription;
};
}

View File

@@ -9,6 +9,7 @@
#include <ui/text_editor.hpp>
#include <content/text_highlighting/pattern_language.hpp>
#include <hex/helpers/magic.hpp>
#include <ui/pattern_drawer.hpp>
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<pl::PatternLanguage> m_editorRuntime;
std::mutex m_possiblePatternFilesMutex;
PerProvider<std::vector<PossiblePattern>> m_possiblePatternFiles;
PerProvider<std::vector<magic::FoundPattern>> m_possiblePatternFiles;
bool m_runAutomatically = false;
bool m_triggerEvaluation = false;
std::atomic<bool> m_triggerAutoEvaluate = false;

View File

@@ -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",

View File

@@ -29,6 +29,7 @@
#include <content/providers/file_provider.hpp>
#include <content/views/fullscreen/view_fullscreen_save_editor.hpp>
#include <content/views/fullscreen/view_fullscreen_file_info.hpp>
namespace hex::plugin::builtin {
using namespace hex::literals;
@@ -521,6 +522,21 @@ namespace hex::plugin::builtin {
}
}
void handleFileInfoCommand(const std::vector<std::string> &args) {
if (args.size() != 1) {
log::println("usage: imhex --file-info <file>");
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<ViewFullScreenFileInfo>(path);
}
void registerCommandForwarders() {
hex::subcommands::registerSubCommand("open", [](const std::vector<std::string> &args){

View File

@@ -0,0 +1,112 @@
#include <content/views/fullscreen/view_fullscreen_file_info.hpp>
#include <fonts/fonts.hpp>
#include <hex/api/content_registry/pattern_language.hpp>
#include <hex/helpers/magic.hpp>
#include <hex/helpers/literals.hpp>
#include <pl/pattern_language.hpp>
#include <pl/core/evaluator.hpp>
#include <toasts/toast_notification.hpp>
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();
}
}

View File

@@ -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)) {

View File

@@ -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<BinaryPattern> {
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<u64> {
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<u8> 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;

View File

@@ -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") {