diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 1977cf830..7d12c5e30 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -58,6 +58,7 @@ set(LIBIMHEX_SOURCES source/ui/banner.cpp source/subcommands/subcommands.cpp + source/init/cli.cpp ) if (APPLE) diff --git a/lib/libimhex/include/hex/api/imhex_api/system.hpp b/lib/libimhex/include/hex/api/imhex_api/system.hpp index f83dcbe20..3522756dc 100644 --- a/lib/libimhex/include/hex/api/imhex_api/system.hpp +++ b/lib/libimhex/include/hex/api/imhex_api/system.hpp @@ -88,6 +88,8 @@ EXPORT_MODULE namespace hex { bool frameRateUnlockRequested(); void resetFrameRateUnlockRequested(); + void setReadOnlyMode(bool enabled); + } /** @@ -186,6 +188,11 @@ EXPORT_MODULE namespace hex { */ std::string getInitArgument(const std::string &key); + /** + * @brief Returns whether ImHex is running in read-only mode (no edits/saves) + */ + bool isReadOnlyMode(); + /** * @brief Sets if ImHex should follow the system theme * @param enabled Whether to follow the system theme diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index 549d13e95..ed9d7f661 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -619,6 +619,11 @@ namespace hex { s_frameRateUnlockRequested = false; } + static bool s_readOnlyMode = false; + void setReadOnlyMode(bool enabled) { + s_readOnlyMode = enabled; + } + } bool isMainInstance() { @@ -1092,6 +1097,10 @@ namespace hex { RequestSetPostProcessingShader::post(vertexShader, fragmentShader); } + bool isReadOnlyMode() { + return impl::s_readOnlyMode; + } + } diff --git a/lib/libimhex/source/init/cli.cpp b/lib/libimhex/source/init/cli.cpp new file mode 100644 index 000000000..c84b1d477 --- /dev/null +++ b/lib/libimhex/source/init/cli.cpp @@ -0,0 +1,15 @@ +#include + +#include + +namespace hex::init::cli_support { + + void applyGlobalFlag(const std::string &arg) { + if (arg == "--readonly" || arg == "-r") { + ImHexApi::System::impl::setReadOnlyMode(true); + } + } + +} + + diff --git a/lib/libimhex/source/providers/provider.cpp b/lib/libimhex/source/providers/provider.cpp index 9ed32f0d0..431f2193a 100644 --- a/lib/libimhex/source/providers/provider.cpp +++ b/lib/libimhex/source/providers/provider.cpp @@ -68,7 +68,7 @@ namespace hex::prv { } void Provider::write(u64 offset, const void *buffer, size_t size) { - if (!this->isWritable()) + if (!this->isWritable() || ImHexApi::System::isReadOnlyMode()) return; EventProviderDataModified::post(this, offset, size, static_cast(buffer)); @@ -76,7 +76,7 @@ namespace hex::prv { } void Provider::save() { - if (!this->isWritable()) + if (!this->isWritable() || ImHexApi::System::isReadOnlyMode()) return; EventProviderSaved::post(this); @@ -100,6 +100,9 @@ namespace hex::prv { } bool Provider::resize(u64 newSize) { + if (ImHexApi::System::isReadOnlyMode()) { + return false; + } if (newSize >> 63) { log::error("new provider size is very large ({}). Is it a negative number ?", newSize); return false; @@ -116,12 +119,14 @@ namespace hex::prv { } void Provider::insert(u64 offset, u64 size) { + if (ImHexApi::System::isReadOnlyMode()) return; EventProviderDataInserted::post(this, offset, size); this->markDirty(); } void Provider::remove(u64 offset, u64 size) { + if (ImHexApi::System::isReadOnlyMode()) return; EventProviderDataRemoved::post(this, offset, size); this->markDirty(); diff --git a/main/gui/source/init/run/cli.cpp b/main/gui/source/init/run/cli.cpp index a5315c264..ce91611a7 100644 --- a/main/gui/source/init/run/cli.cpp +++ b/main/gui/source/init/run/cli.cpp @@ -71,7 +71,24 @@ namespace hex::init { PluginManager::load(dir); } - // Process the arguments + // Process our own global flags first + { + std::vector remaining; + remaining.reserve(args.size()); + + for (size_t i = 0; i < args.size(); i += 1) { + const auto &arg = args[i]; + if (arg == "--readonly" || arg == "-r") { + ImHexApi::System::impl::setReadOnlyMode(true); + continue; + } + remaining.push_back(arg); + } + + args.swap(remaining); + } + + // Process the arguments (subcommands) after handling globals hex::subcommands::processArguments(args); // Explicitly don't unload plugins again here. diff --git a/plugins/builtin/source/content/events.cpp b/plugins/builtin/source/content/events.cpp index ec1b2e79a..9f32c81d4 100644 --- a/plugins/builtin/source/content/events.cpp +++ b/plugins/builtin/source/content/events.cpp @@ -84,7 +84,7 @@ namespace hex::plugin::builtin { EventWindowClosing::subscribe([](GLFWwindow *window) { imhexClosing = false; - if (ImHexApi::Provider::isDirty() && !imhexClosing) { + if (!ImHexApi::System::isReadOnlyMode() && ImHexApi::Provider::isDirty() && !imhexClosing) { glfwSetWindowShouldClose(window, GLFW_FALSE); ui::PopupQuestion::open("hex.builtin.popup.exit_application.desc"_lang, [] { @@ -108,7 +108,7 @@ namespace hex::plugin::builtin { EventCloseButtonPressed::subscribe([]() { if (ImHexApi::Provider::isValid()) { - if (ImHexApi::Provider::isDirty()) { + if (!ImHexApi::System::isReadOnlyMode() && ImHexApi::Provider::isDirty()) { ui::PopupQuestion::open("hex.builtin.popup.exit_application.desc"_lang, [] { for (const auto &provider : ImHexApi::Provider::getProviders()) @@ -134,7 +134,7 @@ namespace hex::plugin::builtin { }); EventProviderClosing::subscribe([](const prv::Provider *provider, bool *shouldClose) { - if (provider->isDirty()) { + if (!ImHexApi::System::isReadOnlyMode() && provider->isDirty()) { *shouldClose = false; PopupUnsavedChanges::open("hex.builtin.popup.close_provider.desc"_lang, []{ diff --git a/plugins/builtin/source/content/main_menu_items.cpp b/plugins/builtin/source/content/main_menu_items.cpp index 88a325ed8..47ee1d977 100644 --- a/plugins/builtin/source/content/main_menu_items.cpp +++ b/plugins/builtin/source/content/main_menu_items.cpp @@ -52,7 +52,7 @@ namespace hex::plugin::builtin { } bool noRunningTaskAndWritableProvider() { - return noRunningTasks() && ImHexApi::Provider::isValid() && ImHexApi::Provider::get()->isWritable(); + return noRunningTasks() && !ImHexApi::System::isReadOnlyMode() && ImHexApi::Provider::isValid() && ImHexApi::Provider::get()->isWritable(); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 979391fd5..ccccdf04b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,4 +5,5 @@ target_compile_definitions(tests_common PUBLIC IMHEX_PROJECT_NAME="${PROJECT_NAM add_subdirectory(helpers) add_subdirectory(algorithms) -add_subdirectory(plugins) \ No newline at end of file +add_subdirectory(plugins) +add_subdirectory(cli) \ No newline at end of file diff --git a/tests/cli/CMakeLists.txt b/tests/cli/CMakeLists.txt new file mode 100644 index 000000000..ec8a64cc6 --- /dev/null +++ b/tests/cli/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.16) + +project(cli_test) +set(TEST_CATEGORY CLI) + +set(AVAILABLE_TESTS + ReadOnlyFlagSetsMode +) + +add_executable(${PROJECT_NAME} + source/test_cli_readonly.cpp +) + +target_include_directories(${PROJECT_NAME} PRIVATE include) +target_link_libraries(${PROJECT_NAME} PRIVATE libimhex tests_common ${FMT_LIBRARIES}) + +set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +foreach (test IN LISTS AVAILABLE_TESTS) + add_test(NAME "${TEST_CATEGORY}/${test}" COMMAND ${PROJECT_NAME} "${test}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +endforeach () + +add_dependencies(unit_tests ${PROJECT_NAME}) + + diff --git a/tests/cli/source/test_cli_readonly.cpp b/tests/cli/source/test_cli_readonly.cpp new file mode 100644 index 000000000..cba524bc7 --- /dev/null +++ b/tests/cli/source/test_cli_readonly.cpp @@ -0,0 +1,33 @@ +#include + +#include + +// We only declare the CLI runner here; it's defined in the GUI main module +namespace hex::init { + void runCommandLine(int argc, char **argv); +} + +static int runReadOnlyCLI(const std::vector &args) { + int argc = static_cast(args.size()); + // const_cast is safe here because CLI layer doesn't mutate program name/args strings + auto argv = const_cast(reinterpret_cast(args.data())); + hex::init::runCommandLine(argc, argv); + return EXIT_SUCCESS; +} + +TEST_SEQUENCE("ReadOnlyFlagSetsMode") { + // Simulate: imhex --readonly somefile + const char *prog = "imhex"; + const char *flag = "--readonly"; + const char *file = "dummy.bin"; + const std::vector argv = { prog, flag, file }; + + (void) runReadOnlyCLI(argv); + + // Expect the global read-only mode to be enabled + TEST_ASSERT(hex::ImHexApi::System::isReadOnlyMode()); + + TEST_SUCCESS(); +}; + +