diff --git a/lib/libimhex/include/hex/api/content_registry.hpp b/lib/libimhex/include/hex/api/content_registry.hpp index 481a2c9a9..7835c354a 100644 --- a/lib/libimhex/include/hex/api/content_registry.hpp +++ b/lib/libimhex/include/hex/api/content_registry.hpp @@ -981,12 +981,34 @@ namespace hex { namespace impl { using Callback = std::function; - struct Entry { + struct ExportMenuEntry { UnlocalizedString unlocalizedName; Callback callback; }; - const std::vector& getEntries(); + struct FindOccurrence { + Region region; + enum class DecodeType { ASCII, Binary, UTF16, Unsigned, Signed, Float, Double } decodeType; + std::endian endian = std::endian::native; + bool selected; + }; + + using FindExporterCallback = std::function(const std::vector&, std::function)>; + struct FindExporterEntry { + UnlocalizedString unlocalizedName; + std::string fileExtension; + FindExporterCallback callback; + }; + + /** + * @brief Retrieves a list of all registered data formatters used by the 'File -> Export' menu + */ + const std::vector& getExportMenuEntries(); + + /** + * @brief Retrieves a list of all registered data formatters used in the Results section of the 'Find' view + */ + const std::vector& getFindExporterEntries(); } @@ -996,7 +1018,14 @@ namespace hex { * @param unlocalizedName The unlocalized name of the formatter * @param callback The function to call to format the data */ - void add(const UnlocalizedString &unlocalizedName, const impl::Callback &callback); + void addExportMenuEntry(const UnlocalizedString &unlocalizedName, const impl::Callback &callback); + + /** + * @brief Adds a new data exporter for Find results + * @param unlocalizedName The unlocalized name of the formatter + * @param callback The function to call to format the data + */ + void addFindExportFormatter(const UnlocalizedString &unlocalizedName, const std::string fileExtension, const impl::FindExporterCallback &callback); } diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index 752b4380c..07c647842 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -982,17 +982,28 @@ namespace hex { namespace impl { - static AutoReset> s_entries; - const std::vector& getEntries() { - return *s_entries; + static AutoReset> s_exportMenuEntries; + const std::vector& getExportMenuEntries() { + return *s_exportMenuEntries; + } + + static AutoReset> s_findExportEntries; + const std::vector& getFindExporterEntries() { + return *s_findExportEntries; } } - void add(const UnlocalizedString &unlocalizedName, const impl::Callback &callback) { + void addExportMenuEntry(const UnlocalizedString &unlocalizedName, const impl::Callback &callback) { log::debug("Registered new data formatter: {}", unlocalizedName.get()); - impl::s_entries->push_back({ unlocalizedName, callback }); + impl::s_exportMenuEntries->push_back({ unlocalizedName, callback }); + } + + void addFindExportFormatter(const UnlocalizedString &unlocalizedName, const std::string fileExtension, const impl::FindExporterCallback &callback) { + log::debug("Registered new export formatter: {}", unlocalizedName.get()); + + impl::s_findExportEntries->push_back({ unlocalizedName, fileExtension, callback }); } } diff --git a/plugins/builtin/include/content/export_formatters/export_formatter.hpp b/plugins/builtin/include/content/export_formatters/export_formatter.hpp new file mode 100644 index 000000000..5a5bcebf8 --- /dev/null +++ b/plugins/builtin/include/content/export_formatters/export_formatter.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace hex::plugin::builtin::export_fmt { + + using Occurrence = hex::ContentRegistry::DataFormatter::impl::FindOccurrence; + + /** + * Base class for creating export formatters for different file formats, used in the Results section of the 'Find' view + */ + class ExportFormatter { + public: + explicit ExportFormatter(std::string name) : mName(std::move(name)) {} + + virtual ~ExportFormatter() = default; + + [[nodiscard]] const std::string &getName() const { + return this->mName; + } + + /** + * Main export formatter function + * @param occurrences A list of search occurrences found by the 'Find' view + * @param occurrenceFunc A string formatter function used to transform each occurrence to a string form. May be ignored for custom/binary exporter formats + * @return An array of bytes representing the exported data to be written into the target file + */ + [[nodiscard]] virtual std::vector format(const std::vector &occurrences, std::function occurrenceFunc) = 0; + + private: + std::string mName; + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/export_formatters/export_formatter_csv.hpp b/plugins/builtin/include/content/export_formatters/export_formatter_csv.hpp new file mode 100644 index 000000000..018b3699f --- /dev/null +++ b/plugins/builtin/include/content/export_formatters/export_formatter_csv.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "export_formatter.hpp" + +namespace hex::plugin::builtin::export_fmt { + + class ExportFormatterCsv : public ExportFormatter { + public: + ExportFormatterCsv() : ExportFormatter("csv") {} + + explicit ExportFormatterCsv(std::string name) : ExportFormatter(std::move(name)) {} + + ~ExportFormatterCsv() override = default; + + [[nodiscard]] std::vector format(const std::vector &occurrences, std::function occurrenceFunc) override { + char separator = getSeparatorCharacter(); + + std::string result; + result += fmt::format("offset{}size{}data\n", separator, separator); + + + for (const auto &occurrence : occurrences) { + std::string formattedResult = occurrenceFunc(occurrence); + std::string escapedResult; + escapedResult.reserve(formattedResult.size() * 2); + + for (char ch : formattedResult) { + if (ch == '"') { + escapedResult += "\"\""; + } else if (ch == '\n' || ch == '\r') { + escapedResult += ' '; // Replace newlines with spaces + } else { + escapedResult += ch; + } + } + + bool needsQuotes = escapedResult.find(separator) != std::string::npos || escapedResult.find('"') != std::string::npos; + if (needsQuotes) { + escapedResult.insert(0, 1, '"'); + escapedResult.push_back('"'); + } + + const auto line = fmt::format("0x{:08X}{}0x{}{}{}", + occurrence.region.getStartAddress(), + separator, + occurrence.region.getSize(), + separator, + escapedResult); + result += line; + result += '\n'; + } + + return { result.begin(), result.end() }; + } + + protected: + [[nodiscard]] virtual char getSeparatorCharacter() const { return ','; } + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/export_formatters/export_formatter_json.hpp b/plugins/builtin/include/content/export_formatters/export_formatter_json.hpp new file mode 100644 index 000000000..89e57596b --- /dev/null +++ b/plugins/builtin/include/content/export_formatters/export_formatter_json.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "export_formatter.hpp" + +#include + +namespace hex::plugin::builtin::export_fmt { + + class ExportFormatterJson : public ExportFormatter { + public: + ExportFormatterJson() : ExportFormatter("json") {} + + ~ExportFormatterJson() override = default; + + std::vector format(const std::vector &occurrences, std::function occurrenceFunc) override { + nlohmann::json resultJson; + + for (const auto &occurrence : occurrences) { + std::string formattedResult = occurrenceFunc(occurrence); + + nlohmann::json obj = { + { "offset", occurrence.region.getStartAddress() }, + { "size", occurrence.region.getSize() }, + { "data", formattedResult } + }; + + resultJson.push_back(obj); + } + + auto result = resultJson.dump(4); + return { result.begin(), result.end() }; + } + }; +} \ No newline at end of file diff --git a/plugins/builtin/include/content/export_formatters/export_formatter_tsv.hpp b/plugins/builtin/include/content/export_formatters/export_formatter_tsv.hpp new file mode 100644 index 000000000..a9c71c8ec --- /dev/null +++ b/plugins/builtin/include/content/export_formatters/export_formatter_tsv.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "export_formatter_csv.hpp" + +namespace hex::plugin::builtin::export_fmt { + + class ExportFormatterTsv : public ExportFormatterCsv { + public: + ExportFormatterTsv() : ExportFormatterCsv("tsv") {} + + ~ExportFormatterTsv() override = default; + + protected: + [[nodiscard]] char getSeparatorCharacter() const override { return '\t'; } + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/views/view_find.hpp b/plugins/builtin/include/content/views/view_find.hpp index 25ac94cce..ab7ad6135 100644 --- a/plugins/builtin/include/content/views/view_find.hpp +++ b/plugins/builtin/include/content/views/view_find.hpp @@ -11,6 +11,8 @@ #include +#include + namespace hex::plugin::builtin { class ViewFind : public View::Window { @@ -22,12 +24,7 @@ namespace hex::plugin::builtin { private: - struct Occurrence { - Region region; - enum class DecodeType { ASCII, Binary, UTF16, Unsigned, Signed, Float, Double } decodeType; - std::endian endian = std::endian::native; - bool selected; - }; + using Occurrence = hex::ContentRegistry::DataFormatter::impl::FindOccurrence; struct BinaryPattern { u8 mask, value; diff --git a/plugins/builtin/source/content/data_formatters.cpp b/plugins/builtin/source/content/data_formatters.cpp index 318121e9b..1ec0e79c3 100644 --- a/plugins/builtin/source/content/data_formatters.cpp +++ b/plugins/builtin/source/content/data_formatters.cpp @@ -8,6 +8,10 @@ #include #include +#include +#include +#include + namespace hex::plugin::builtin { static std::string formatLanguageArray(prv::Provider *provider, u64 offset, size_t size, const std::string &start, const std::string &byteFormat, const std::string &end, bool removeFinalDelimiter= false) { @@ -46,57 +50,57 @@ namespace hex::plugin::builtin { void registerDataFormatters() { - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.c", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.c", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, hex::format("const uint8_t data[{0}] = {{", size), "0x{0:02X}, ", "};"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.cpp", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.cpp", [](prv::Provider *provider, u64 offset, size_t size) { AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.copy_as.name"); return formatLanguageArray(provider, offset, size, hex::format("constexpr std::array data = {{", size), "0x{0:02X}, ", "};"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.java", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.java", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "final byte[] data = {", "0x{0:02X}, ", "};"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.csharp", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.csharp", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "const byte[] data = {", "0x{0:02X}, ", "};"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.rust", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.rust", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, hex::format("let data: [u8; 0x{0:02X}] = [", size), "0x{0:02X}, ", "];"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.python", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.python", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "data = bytes([", "0x{0:02X}, ", "])"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.js", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.js", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "const data = new Uint8Array([", "0x{0:02X}, ", "]);"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.lua", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.lua", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "data = {", "0x{0:02X}, ", "}"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.go", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.go", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "data := [...]byte{", "0x{0:02X}, ", "}", false); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.crystal", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.crystal", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "data = [", "0x{0:02X}, ", "] of UInt8"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.swift", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.swift", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, "let data: [Uint8] = [", "0x{0:02X}, ", "]"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.pascal", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.pascal", [](prv::Provider *provider, u64 offset, size_t size) { return formatLanguageArray(provider, offset, size, hex::format("data: array[0..{0}] of Byte = (", size - 1), "${0:02X}, ", ")"); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.base64", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.base64", [](prv::Provider *provider, u64 offset, size_t size) { std::vector data(size, 0x00); provider->read(offset, data.data(), size); @@ -105,11 +109,11 @@ namespace hex::plugin::builtin { return std::string(result.begin(), result.end()); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.hex_view", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.hex_view", [](prv::Provider *provider, u64 offset, size_t size) { return hex::generateHexView(offset, size, provider); }); - ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.html", [](prv::Provider *provider, u64 offset, size_t size) { + ContentRegistry::DataFormatter::addExportMenuEntry("hex.builtin.view.hex_editor.copy.html", [](prv::Provider *provider, u64 offset, size_t size) { std::string result = "
\n" "