reg key editor plugin
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set "PLUGIN_DIR=%~dp0"
|
||||||
|
set "NATIVE_DIR=%PLUGIN_DIR%native"
|
||||||
|
set "PLUGIN_NAME=regedit"
|
||||||
|
set "ZIP_OUT=%PLUGIN_DIR%%PLUGIN_NAME%.zip"
|
||||||
|
|
||||||
|
if not exist "%NATIVE_DIR%\plugin.cpp" (
|
||||||
|
echo [error] native\plugin.cpp not found in %NATIVE_DIR%
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64"
|
||||||
|
|
||||||
|
for %%T in (%BUILD_TARGETS%) do (
|
||||||
|
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||||
|
set "TARGET_OS=%%A"
|
||||||
|
set "TARGET_ARCH=%%B"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not "!TARGET_OS!"=="windows" (
|
||||||
|
echo [error] regedit plugin only supports windows targets, got !TARGET_OS!-!TARGET_ARCH!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set "OUTFILE=%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll"
|
||||||
|
if exist "!OUTFILE!" del /f /q "!OUTFILE!"
|
||||||
|
|
||||||
|
set "CL_MACHINE=/machine:X64"
|
||||||
|
set "GXX_CMD=x86_64-w64-mingw32-g++"
|
||||||
|
if "!TARGET_ARCH!"=="arm64" (
|
||||||
|
set "CL_MACHINE=/machine:ARM64"
|
||||||
|
set "GXX_CMD=aarch64-w64-mingw32-g++"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [build] cl /LD /EHsc /O2 plugin.cpp /Fe:!OUTFILE!
|
||||||
|
pushd "%NATIVE_DIR%"
|
||||||
|
cl /nologo /LD /EHsc /O2 plugin.cpp /Fe:"!OUTFILE!" advapi32.lib /link !CL_MACHINE! >nul 2>&1
|
||||||
|
set "CL_STATUS=!errorlevel!"
|
||||||
|
popd
|
||||||
|
if not "!CL_STATUS!"=="0" (
|
||||||
|
echo [build] cl failed, trying !GXX_CMD!...
|
||||||
|
!GXX_CMD! -shared -O2 -s -static -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp" -ladvapi32
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [error] build failed for !TARGET_OS!-!TARGET_ARCH!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "%NATIVE_DIR%\plugin.obj" del /f /q "%NATIVE_DIR%\plugin.obj"
|
||||||
|
if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp"
|
||||||
|
if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib"
|
||||||
|
|
||||||
|
echo [ok] !OUTFILE!
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
|
||||||
|
|
||||||
|
set "ZIP_SOURCES="
|
||||||
|
for %%T in (%BUILD_TARGETS%) do (
|
||||||
|
for /f "tokens=1,2 delims=-" %%A in ("%%T") do (
|
||||||
|
set "TARGET_OS=%%A"
|
||||||
|
set "TARGET_ARCH=%%B"
|
||||||
|
)
|
||||||
|
set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.html'"
|
||||||
|
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.css'"
|
||||||
|
if exist "%PLUGIN_DIR%%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.js'"
|
||||||
|
if exist "%PLUGIN_DIR%config.json" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%config.json'"
|
||||||
|
|
||||||
|
set "ZIP_SOURCES=!ZIP_SOURCES:~1!"
|
||||||
|
|
||||||
|
powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [error] zip creation failed
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [ok] %ZIP_OUT%
|
||||||
@@ -0,0 +1,763 @@
|
|||||||
|
/*
|
||||||
|
* Registry Editor Plugin — Windows registry browser/editor.
|
||||||
|
*
|
||||||
|
* Exports the standard Overlord plugin ABI:
|
||||||
|
* PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback,
|
||||||
|
* PluginGetRuntime (returns "cpp")
|
||||||
|
*
|
||||||
|
* Build (Windows / MSVC):
|
||||||
|
* cl /LD /EHsc /O2 plugin.cpp /Fe:regedit-windows-amd64.dll advapi32.lib
|
||||||
|
*
|
||||||
|
* Build (Windows / MinGW):
|
||||||
|
* x86_64-w64-mingw32-g++ -shared -O2 -o regedit-windows-amd64.dll plugin.cpp -ladvapi32
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# define EXPORT extern "C" __declspec(dllexport)
|
||||||
|
# define WIN32_LEAN_AND_MEAN
|
||||||
|
# include <windows.h>
|
||||||
|
# include <winreg.h>
|
||||||
|
# pragma comment(lib, "advapi32.lib")
|
||||||
|
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
|
||||||
|
(void)hDll;
|
||||||
|
(void)reason;
|
||||||
|
(void)reserved;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
# define EXPORT extern "C" __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
typedef void (__stdcall *host_callback_t)(
|
||||||
|
const char *event, uintptr_t eventLen,
|
||||||
|
const char *payload, uintptr_t payloadLen);
|
||||||
|
#else
|
||||||
|
typedef void (*host_callback_t)(
|
||||||
|
uintptr_t ctx,
|
||||||
|
const char *event, int eventLen,
|
||||||
|
const char *payload, int payloadLen);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static std::mutex g_mu;
|
||||||
|
static host_callback_t g_callback = nullptr;
|
||||||
|
#ifndef _WIN32
|
||||||
|
static uintptr_t g_callback_ctx = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void send_event(const char *event, const std::string &payload) {
|
||||||
|
std::lock_guard<std::mutex> lk(g_mu);
|
||||||
|
if (!g_callback) return;
|
||||||
|
#ifdef _WIN32
|
||||||
|
g_callback(event, static_cast<uintptr_t>(strlen(event)),
|
||||||
|
payload.c_str(), static_cast<uintptr_t>(payload.size()));
|
||||||
|
#else
|
||||||
|
g_callback(g_callback_ctx, event, static_cast<int>(strlen(event)),
|
||||||
|
payload.c_str(), static_cast<int>(payload.size()));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string trim(const std::string &s) {
|
||||||
|
size_t a = 0;
|
||||||
|
while (a < s.size() && std::isspace(static_cast<unsigned char>(s[a]))) a++;
|
||||||
|
size_t b = s.size();
|
||||||
|
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) b--;
|
||||||
|
return s.substr(a, b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string json_escape(const std::string &s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size() + 16);
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
switch (c) {
|
||||||
|
case '\\': out += "\\\\"; break;
|
||||||
|
case '"': out += "\\\""; break;
|
||||||
|
case '\b': out += "\\b"; break;
|
||||||
|
case '\f': out += "\\f"; break;
|
||||||
|
case '\n': out += "\\n"; break;
|
||||||
|
case '\r': out += "\\r"; break;
|
||||||
|
case '\t': out += "\\t"; break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
char buf[8];
|
||||||
|
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||||
|
out += buf;
|
||||||
|
} else {
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string ok_payload(const std::string &action, const std::string &extra = "{}") {
|
||||||
|
std::string body = extra;
|
||||||
|
if (body.empty()) body = "{}";
|
||||||
|
if (body.front() == '{') body.erase(body.begin());
|
||||||
|
if (!body.empty() && body.back() == '}') body.pop_back();
|
||||||
|
std::string out = "{\"ok\":true,\"action\":\"" + json_escape(action) + "\"";
|
||||||
|
if (!body.empty()) out += "," + body;
|
||||||
|
out += "}";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string err_payload(const std::string &action, const std::string &error) {
|
||||||
|
return "{\"ok\":false,\"action\":\"" + json_escape(action) +
|
||||||
|
"\",\"error\":\"" + json_escape(error) + "\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string json_string(const char *json, int len, const char *key) {
|
||||||
|
if (!json || len <= 0) return "";
|
||||||
|
std::string s(json, static_cast<size_t>(len));
|
||||||
|
std::string needle = std::string("\"") + key + "\"";
|
||||||
|
size_t p = s.find(needle);
|
||||||
|
if (p == std::string::npos) return "";
|
||||||
|
p = s.find(':', p + needle.size());
|
||||||
|
if (p == std::string::npos) return "";
|
||||||
|
p++;
|
||||||
|
while (p < s.size() && std::isspace(static_cast<unsigned char>(s[p]))) p++;
|
||||||
|
if (p >= s.size() || s[p] != '"') return "";
|
||||||
|
p++;
|
||||||
|
std::string out;
|
||||||
|
while (p < s.size()) {
|
||||||
|
char c = s[p++];
|
||||||
|
if (c == '"') break;
|
||||||
|
if (c == '\\' && p < s.size()) {
|
||||||
|
char e = s[p++];
|
||||||
|
switch (e) {
|
||||||
|
case 'n': out += '\n'; break;
|
||||||
|
case 'r': out += '\r'; break;
|
||||||
|
case 't': out += '\t'; break;
|
||||||
|
case 'b': out += '\b'; break;
|
||||||
|
case 'f': out += '\f'; break;
|
||||||
|
case '\\': out += '\\'; break;
|
||||||
|
case '"': out += '"'; break;
|
||||||
|
default: out += e; break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
static std::wstring widen(const std::string &s) {
|
||||||
|
if (s.empty()) return L"";
|
||||||
|
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
|
||||||
|
std::wstring w(static_cast<size_t>(n), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), &w[0], n);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string narrow(const std::wstring &w) {
|
||||||
|
if (w.empty()) return "";
|
||||||
|
int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
||||||
|
std::string s(static_cast<size_t>(n), '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), &s[0], n, nullptr, nullptr);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegPath {
|
||||||
|
HKEY root;
|
||||||
|
std::wstring subkey;
|
||||||
|
std::string root_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool parse_path(const std::string &path_in, RegPath &out, std::string &error) {
|
||||||
|
std::string path = trim(path_in);
|
||||||
|
std::replace(path.begin(), path.end(), '/', '\\');
|
||||||
|
while (!path.empty() && path.back() == '\\') path.pop_back();
|
||||||
|
size_t slash = path.find('\\');
|
||||||
|
std::string root = slash == std::string::npos ? path : path.substr(0, slash);
|
||||||
|
std::string sub = slash == std::string::npos ? "" : path.substr(slash + 1);
|
||||||
|
std::string up = root;
|
||||||
|
std::transform(up.begin(), up.end(), up.begin(), [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||||
|
|
||||||
|
if (up == "HKCR" || up == "HKEY_CLASSES_ROOT") {
|
||||||
|
out.root = HKEY_CLASSES_ROOT; out.root_name = "HKCR";
|
||||||
|
} else if (up == "HKCU" || up == "HKEY_CURRENT_USER") {
|
||||||
|
out.root = HKEY_CURRENT_USER; out.root_name = "HKCU";
|
||||||
|
} else if (up == "HKLM" || up == "HKEY_LOCAL_MACHINE") {
|
||||||
|
out.root = HKEY_LOCAL_MACHINE; out.root_name = "HKLM";
|
||||||
|
} else if (up == "HKU" || up == "HKEY_USERS") {
|
||||||
|
out.root = HKEY_USERS; out.root_name = "HKU";
|
||||||
|
} else if (up == "HKCC" || up == "HKEY_CURRENT_CONFIG") {
|
||||||
|
out.root = HKEY_CURRENT_CONFIG; out.root_name = "HKCC";
|
||||||
|
} else {
|
||||||
|
error = "Unknown root hive: " + root;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.subkey = widen(sub);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string full_path(const RegPath &p) {
|
||||||
|
std::string s = p.root_name;
|
||||||
|
if (!p.subkey.empty()) s += "\\" + narrow(p.subkey);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string win_error(LSTATUS code) {
|
||||||
|
wchar_t *msg = nullptr;
|
||||||
|
DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||||
|
FORMAT_MESSAGE_FROM_SYSTEM |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||||
|
DWORD len = FormatMessageW(flags, nullptr, static_cast<DWORD>(code),
|
||||||
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
reinterpret_cast<LPWSTR>(&msg), 0, nullptr);
|
||||||
|
std::string text;
|
||||||
|
if (len && msg) {
|
||||||
|
text = narrow(std::wstring(msg, msg + len));
|
||||||
|
LocalFree(msg);
|
||||||
|
while (!text.empty() && (text.back() == '\r' || text.back() == '\n' || text.back() == ' ')) {
|
||||||
|
text.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char buf[64];
|
||||||
|
std::snprintf(buf, sizeof(buf), "Windows error %ld", static_cast<long>(code));
|
||||||
|
return text.empty() ? std::string(buf) : text + " (" + buf + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *type_name(DWORD type) {
|
||||||
|
switch (type) {
|
||||||
|
case REG_SZ: return "REG_SZ";
|
||||||
|
case REG_EXPAND_SZ: return "REG_EXPAND_SZ";
|
||||||
|
case REG_DWORD: return "REG_DWORD";
|
||||||
|
case REG_QWORD: return "REG_QWORD";
|
||||||
|
case REG_BINARY: return "REG_BINARY";
|
||||||
|
case REG_MULTI_SZ: return "REG_MULTI_SZ";
|
||||||
|
default: return "REG_NONE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DWORD type_from_name(const std::string &name) {
|
||||||
|
if (name == "REG_SZ") return REG_SZ;
|
||||||
|
if (name == "REG_EXPAND_SZ") return REG_EXPAND_SZ;
|
||||||
|
if (name == "REG_DWORD") return REG_DWORD;
|
||||||
|
if (name == "REG_QWORD") return REG_QWORD;
|
||||||
|
if (name == "REG_BINARY") return REG_BINARY;
|
||||||
|
if (name == "REG_MULTI_SZ") return REG_MULTI_SZ;
|
||||||
|
return REG_SZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string hex_bytes(const BYTE *data, DWORD bytes) {
|
||||||
|
static const char *digits = "0123456789abcdef";
|
||||||
|
std::string out;
|
||||||
|
for (DWORD i = 0; i < bytes; i++) {
|
||||||
|
if (i) out += ' ';
|
||||||
|
out += digits[(data[i] >> 4) & 0xf];
|
||||||
|
out += digits[data[i] & 0xf];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_hex_bytes(const std::string &text, std::vector<BYTE> &out) {
|
||||||
|
std::string h;
|
||||||
|
for (unsigned char c : text) {
|
||||||
|
if (std::isxdigit(c)) h += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
if (h.size() % 2 != 0) return false;
|
||||||
|
out.clear();
|
||||||
|
for (size_t i = 0; i < h.size(); i += 2) {
|
||||||
|
unsigned int v = 0;
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::hex << h.substr(i, 2);
|
||||||
|
ss >> v;
|
||||||
|
out.push_back(static_cast<BYTE>(v & 0xff));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string value_to_text(DWORD type, const std::vector<BYTE> &data) {
|
||||||
|
if (type == REG_DWORD && data.size() >= 4) {
|
||||||
|
DWORD v = *reinterpret_cast<const DWORD *>(data.data());
|
||||||
|
return std::to_string(v);
|
||||||
|
}
|
||||||
|
if (type == REG_QWORD && data.size() >= 8) {
|
||||||
|
unsigned long long v = *reinterpret_cast<const unsigned long long *>(data.data());
|
||||||
|
return std::to_string(v);
|
||||||
|
}
|
||||||
|
if (type == REG_SZ || type == REG_EXPAND_SZ) {
|
||||||
|
const wchar_t *w = reinterpret_cast<const wchar_t *>(data.data());
|
||||||
|
size_t chars = data.size() / sizeof(wchar_t);
|
||||||
|
if (chars && w[chars - 1] == L'\0') chars--;
|
||||||
|
return narrow(std::wstring(w, w + chars));
|
||||||
|
}
|
||||||
|
if (type == REG_MULTI_SZ) {
|
||||||
|
const wchar_t *w = reinterpret_cast<const wchar_t *>(data.data());
|
||||||
|
size_t chars = data.size() / sizeof(wchar_t);
|
||||||
|
std::string out;
|
||||||
|
size_t start = 0;
|
||||||
|
for (size_t i = 0; i < chars; i++) {
|
||||||
|
if (w[i] == L'\0') {
|
||||||
|
if (i == start) break;
|
||||||
|
if (!out.empty()) out += "\n";
|
||||||
|
out += narrow(std::wstring(w + start, w + i));
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return hex_bytes(data.data(), static_cast<DWORD>(data.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool text_to_value(DWORD type, const std::string &text, std::vector<BYTE> &out, std::string &error) {
|
||||||
|
out.clear();
|
||||||
|
if (type == REG_DWORD) {
|
||||||
|
char *end = nullptr;
|
||||||
|
unsigned long v = std::strtoul(text.c_str(), &end, 0);
|
||||||
|
if (end == text.c_str()) { error = "Invalid DWORD"; return false; }
|
||||||
|
out.resize(4);
|
||||||
|
std::memcpy(out.data(), &v, 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type == REG_QWORD) {
|
||||||
|
char *end = nullptr;
|
||||||
|
unsigned long long v = std::strtoull(text.c_str(), &end, 0);
|
||||||
|
if (end == text.c_str()) { error = "Invalid QWORD"; return false; }
|
||||||
|
out.resize(8);
|
||||||
|
std::memcpy(out.data(), &v, 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type == REG_BINARY) {
|
||||||
|
if (!parse_hex_bytes(text, out)) { error = "Invalid binary hex"; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type == REG_MULTI_SZ) {
|
||||||
|
std::wstring all;
|
||||||
|
std::stringstream ss(text);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(ss, line)) {
|
||||||
|
std::wstring w = widen(line);
|
||||||
|
all.append(w);
|
||||||
|
all.push_back(L'\0');
|
||||||
|
}
|
||||||
|
all.push_back(L'\0');
|
||||||
|
out.resize(all.size() * sizeof(wchar_t));
|
||||||
|
std::memcpy(out.data(), all.data(), out.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::wstring w = widen(text);
|
||||||
|
w.push_back(L'\0');
|
||||||
|
out.resize(w.size() * sizeof(wchar_t));
|
||||||
|
std::memcpy(out.data(), w.data(), out.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string reg_quote(const std::string &s) {
|
||||||
|
std::string out = "\"";
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '\\' || c == '"') out += '\\';
|
||||||
|
out += c;
|
||||||
|
}
|
||||||
|
out += "\"";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string reg_hex_list(const BYTE *data, DWORD size) {
|
||||||
|
static const char *digits = "0123456789abcdef";
|
||||||
|
std::string out;
|
||||||
|
for (DWORD i = 0; i < size; i++) {
|
||||||
|
if (i) out += ",";
|
||||||
|
out += digits[(data[i] >> 4) & 0xf];
|
||||||
|
out += digits[data[i] & 0xf];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string value_to_reg_line(const std::string &name, DWORD type, const std::vector<BYTE> &data) {
|
||||||
|
std::string left = name.empty() ? "@" : reg_quote(name);
|
||||||
|
if (type == REG_DWORD && data.size() >= 4) {
|
||||||
|
DWORD v = *reinterpret_cast<const DWORD *>(data.data());
|
||||||
|
char buf[32];
|
||||||
|
std::snprintf(buf, sizeof(buf), "dword:%08lx", static_cast<unsigned long>(v));
|
||||||
|
return left + "=" + buf + "\r\n";
|
||||||
|
}
|
||||||
|
if (type == REG_SZ) return left + "=" + reg_quote(value_to_text(type, data)) + "\r\n";
|
||||||
|
if (type == REG_EXPAND_SZ) return left + "=hex(2):" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
|
||||||
|
if (type == REG_MULTI_SZ) return left + "=hex(7):" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
|
||||||
|
if (type == REG_QWORD) return left + "=hex(b):" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
|
||||||
|
return left + "=hex:" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool open_key(const std::string &path, REGSAM access, HKEY &key, RegPath *parsed, std::string &error) {
|
||||||
|
RegPath rp;
|
||||||
|
if (!parse_path(path, rp, error)) return false;
|
||||||
|
LSTATUS st = RegOpenKeyExW(rp.root, rp.subkey.empty() ? nullptr : rp.subkey.c_str(), 0, access, &key);
|
||||||
|
if (st != ERROR_SUCCESS) {
|
||||||
|
error = win_error(st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (parsed) *parsed = rp;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_list_key(const std::string &path) {
|
||||||
|
HKEY key = nullptr;
|
||||||
|
RegPath rp;
|
||||||
|
std::string error;
|
||||||
|
REGSAM list_access = KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS;
|
||||||
|
if (!open_key(path, list_access, key, &rp, error)) {
|
||||||
|
send_event("registry_result",
|
||||||
|
err_payload("list_key",
|
||||||
|
"Cannot open " + path + ": " + error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD sub_count = 0, max_sub = 0, val_count = 0, max_val = 0, max_data = 0;
|
||||||
|
RegQueryInfoKeyW(key, nullptr, nullptr, nullptr, &sub_count, &max_sub, nullptr,
|
||||||
|
&val_count, &max_val, &max_data, nullptr, nullptr);
|
||||||
|
|
||||||
|
std::ostringstream subkeys;
|
||||||
|
subkeys << "[";
|
||||||
|
for (DWORD i = 0; i < sub_count; i++) {
|
||||||
|
std::wstring name(max_sub + 2, L'\0');
|
||||||
|
DWORD len = static_cast<DWORD>(name.size());
|
||||||
|
if (RegEnumKeyExW(key, i, &name[0], &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
|
||||||
|
if (i) subkeys << ",";
|
||||||
|
subkeys << "\"" << json_escape(narrow(std::wstring(name.c_str(), len))) << "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subkeys << "]";
|
||||||
|
|
||||||
|
std::ostringstream values;
|
||||||
|
values << "[";
|
||||||
|
bool first = true;
|
||||||
|
for (DWORD i = 0; i < val_count; i++) {
|
||||||
|
std::wstring name(max_val + 2, L'\0');
|
||||||
|
DWORD name_len = static_cast<DWORD>(name.size());
|
||||||
|
DWORD type = 0;
|
||||||
|
DWORD data_len = max_data + 2;
|
||||||
|
std::vector<BYTE> data(data_len ? data_len : 2);
|
||||||
|
LSTATUS st = RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len);
|
||||||
|
if (st == ERROR_MORE_DATA) {
|
||||||
|
data.resize(data_len);
|
||||||
|
name_len = static_cast<DWORD>(name.size());
|
||||||
|
st = RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len);
|
||||||
|
}
|
||||||
|
if (st == ERROR_SUCCESS) {
|
||||||
|
data.resize(data_len);
|
||||||
|
if (!first) values << ",";
|
||||||
|
first = false;
|
||||||
|
values << "{\"name\":\"" << json_escape(narrow(std::wstring(name.c_str(), name_len)))
|
||||||
|
<< "\",\"type\":\"" << type_name(type)
|
||||||
|
<< "\",\"data\":\"" << json_escape(value_to_text(type, data)) << "\"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values << "]";
|
||||||
|
RegCloseKey(key);
|
||||||
|
|
||||||
|
send_event("registry_result",
|
||||||
|
ok_payload("list_key", "{\"path\":\"" + json_escape(full_path(rp)) +
|
||||||
|
"\",\"subkeys\":" + subkeys.str() +
|
||||||
|
",\"values\":" + values.str() + "}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_create_key(const std::string &path) {
|
||||||
|
RegPath rp;
|
||||||
|
std::string error;
|
||||||
|
if (!parse_path(path, rp, error)) {
|
||||||
|
send_event("registry_result", err_payload("create_key", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HKEY key = nullptr;
|
||||||
|
DWORD disp = 0;
|
||||||
|
LSTATUS st = RegCreateKeyExW(rp.root, rp.subkey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, &key, &disp);
|
||||||
|
if (st == ERROR_SUCCESS) {
|
||||||
|
RegCloseKey(key);
|
||||||
|
send_event("registry_result", ok_payload("create_key"));
|
||||||
|
} else {
|
||||||
|
send_event("registry_result", err_payload("create_key", win_error(st)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_delete_key(const std::string &path) {
|
||||||
|
RegPath rp;
|
||||||
|
std::string error;
|
||||||
|
if (!parse_path(path, rp, error)) {
|
||||||
|
send_event("registry_result", err_payload("delete_key", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rp.subkey.empty()) {
|
||||||
|
send_event("registry_result", err_payload("delete_key", "Root hives cannot be deleted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LSTATUS st = RegDeleteTreeW(rp.root, rp.subkey.c_str());
|
||||||
|
if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("delete_key"));
|
||||||
|
else send_event("registry_result", err_payload("delete_key", win_error(st)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_set_value(const char *payload, int payloadLen) {
|
||||||
|
std::string path = json_string(payload, payloadLen, "path");
|
||||||
|
std::string name = json_string(payload, payloadLen, "name");
|
||||||
|
std::string old_name = json_string(payload, payloadLen, "oldName");
|
||||||
|
std::string type_s = json_string(payload, payloadLen, "type");
|
||||||
|
std::string data_s = json_string(payload, payloadLen, "data");
|
||||||
|
HKEY key = nullptr;
|
||||||
|
std::string error;
|
||||||
|
if (!open_key(path, KEY_SET_VALUE, key, nullptr, error)) {
|
||||||
|
send_event("registry_result", err_payload("set_value", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (old_name != name && !old_name.empty()) {
|
||||||
|
RegDeleteValueW(key, widen(old_name).c_str());
|
||||||
|
}
|
||||||
|
DWORD type = type_from_name(type_s);
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
if (!text_to_value(type, data_s, data, error)) {
|
||||||
|
RegCloseKey(key);
|
||||||
|
send_event("registry_result", err_payload("set_value", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::wstring wname = widen(name);
|
||||||
|
LSTATUS st = RegSetValueExW(key, name.empty() ? nullptr : wname.c_str(), 0, type,
|
||||||
|
data.empty() ? nullptr : data.data(), static_cast<DWORD>(data.size()));
|
||||||
|
RegCloseKey(key);
|
||||||
|
if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("set_value"));
|
||||||
|
else send_event("registry_result", err_payload("set_value", win_error(st)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_delete_value(const std::string &path, const std::string &name) {
|
||||||
|
HKEY key = nullptr;
|
||||||
|
std::string error;
|
||||||
|
if (!open_key(path, KEY_SET_VALUE, key, nullptr, error)) {
|
||||||
|
send_event("registry_result", err_payload("delete_value", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::wstring wname = widen(name);
|
||||||
|
LSTATUS st = RegDeleteValueW(key, name.empty() ? nullptr : wname.c_str());
|
||||||
|
RegCloseKey(key);
|
||||||
|
if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("delete_value"));
|
||||||
|
else send_event("registry_result", err_payload("delete_value", win_error(st)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool export_key_recursive(const std::string &path, std::string &out, std::string &error) {
|
||||||
|
HKEY key = nullptr;
|
||||||
|
RegPath rp;
|
||||||
|
if (!open_key(path, KEY_READ, key, &rp, error)) return false;
|
||||||
|
out += "\r\n[" + full_path(rp) + "]\r\n";
|
||||||
|
|
||||||
|
DWORD sub_count = 0, max_sub = 0, val_count = 0, max_val = 0, max_data = 0;
|
||||||
|
RegQueryInfoKeyW(key, nullptr, nullptr, nullptr, &sub_count, &max_sub, nullptr,
|
||||||
|
&val_count, &max_val, &max_data, nullptr, nullptr);
|
||||||
|
for (DWORD i = 0; i < val_count; i++) {
|
||||||
|
std::wstring name(max_val + 2, L'\0');
|
||||||
|
DWORD name_len = static_cast<DWORD>(name.size());
|
||||||
|
DWORD type = 0;
|
||||||
|
DWORD data_len = max_data + 2;
|
||||||
|
std::vector<BYTE> data(data_len ? data_len : 2);
|
||||||
|
if (RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len) == ERROR_SUCCESS) {
|
||||||
|
data.resize(data_len);
|
||||||
|
out += value_to_reg_line(narrow(std::wstring(name.c_str(), name_len)), type, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<std::string> subs;
|
||||||
|
for (DWORD i = 0; i < sub_count; i++) {
|
||||||
|
std::wstring name(max_sub + 2, L'\0');
|
||||||
|
DWORD len = static_cast<DWORD>(name.size());
|
||||||
|
if (RegEnumKeyExW(key, i, &name[0], &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
|
||||||
|
subs.push_back(narrow(std::wstring(name.c_str(), len)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegCloseKey(key);
|
||||||
|
for (const auto &sub : subs) {
|
||||||
|
if (!export_key_recursive(path + "\\" + sub, out, error)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_export_key(const std::string &path) {
|
||||||
|
std::string content = "Windows Registry Editor Version 5.00\r\n";
|
||||||
|
std::string error;
|
||||||
|
if (!export_key_recursive(path, content, error)) {
|
||||||
|
send_event("registry_result", err_payload("export_key", error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send_event("registry_result",
|
||||||
|
ok_payload("export_key", "{\"path\":\"" + json_escape(path) +
|
||||||
|
"\",\"content\":\"" + json_escape(content) + "\"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string unquote_reg(const std::string &s) {
|
||||||
|
std::string t = trim(s);
|
||||||
|
if (t.size() >= 2 && t.front() == '"' && t.back() == '"') t = t.substr(1, t.size() - 2);
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 0; i < t.size(); i++) {
|
||||||
|
if (t[i] == '\\' && i + 1 < t.size()) out += t[++i];
|
||||||
|
else out += t[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool import_value_line(HKEY key, const std::string &line, std::string &error) {
|
||||||
|
size_t eq = line.find('=');
|
||||||
|
if (eq == std::string::npos) return true;
|
||||||
|
std::string left = trim(line.substr(0, eq));
|
||||||
|
std::string right = trim(line.substr(eq + 1));
|
||||||
|
std::string name = left == "@" ? "" : unquote_reg(left);
|
||||||
|
std::wstring wname = widen(name);
|
||||||
|
|
||||||
|
DWORD type = REG_SZ;
|
||||||
|
std::vector<BYTE> data;
|
||||||
|
if (right == "-") {
|
||||||
|
RegDeleteValueW(key, name.empty() ? nullptr : wname.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (right.rfind("dword:", 0) == 0) {
|
||||||
|
type = REG_DWORD;
|
||||||
|
DWORD v = static_cast<DWORD>(std::strtoul(right.substr(6).c_str(), nullptr, 16));
|
||||||
|
data.resize(4);
|
||||||
|
std::memcpy(data.data(), &v, 4);
|
||||||
|
} else if (right.rfind("hex(b):", 0) == 0) {
|
||||||
|
type = REG_QWORD;
|
||||||
|
if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(b) value"; return false; }
|
||||||
|
} else if (right.rfind("hex(7):", 0) == 0) {
|
||||||
|
type = REG_MULTI_SZ;
|
||||||
|
if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(7) value"; return false; }
|
||||||
|
} else if (right.rfind("hex(2):", 0) == 0) {
|
||||||
|
type = REG_EXPAND_SZ;
|
||||||
|
if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(2) value"; return false; }
|
||||||
|
} else if (right.rfind("hex:", 0) == 0) {
|
||||||
|
type = REG_BINARY;
|
||||||
|
if (!parse_hex_bytes(right.substr(4), data)) { error = "Invalid hex value"; return false; }
|
||||||
|
} else {
|
||||||
|
type = REG_SZ;
|
||||||
|
std::wstring w = widen(unquote_reg(right));
|
||||||
|
w.push_back(L'\0');
|
||||||
|
data.resize(w.size() * sizeof(wchar_t));
|
||||||
|
std::memcpy(data.data(), w.data(), data.size());
|
||||||
|
}
|
||||||
|
LSTATUS st = RegSetValueExW(key, name.empty() ? nullptr : wname.c_str(), 0, type,
|
||||||
|
data.empty() ? nullptr : data.data(), static_cast<DWORD>(data.size()));
|
||||||
|
if (st != ERROR_SUCCESS) {
|
||||||
|
error = win_error(st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_import_reg(const std::string &content) {
|
||||||
|
std::stringstream ss(content);
|
||||||
|
std::string raw, current_path, pending;
|
||||||
|
HKEY current = nullptr;
|
||||||
|
int changed = 0;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
auto close_current = [&]() {
|
||||||
|
if (current) RegCloseKey(current);
|
||||||
|
current = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (std::getline(ss, raw)) {
|
||||||
|
if (!raw.empty() && raw.back() == '\r') raw.pop_back();
|
||||||
|
std::string line = trim(raw);
|
||||||
|
if (line.empty() || line[0] == ';') continue;
|
||||||
|
if (!line.empty() && line.back() == '\\') {
|
||||||
|
pending += line.substr(0, line.size() - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!pending.empty()) {
|
||||||
|
line = pending + line;
|
||||||
|
pending.clear();
|
||||||
|
}
|
||||||
|
if (line.rfind("Windows Registry Editor", 0) == 0 || line.rfind("REGEDIT4", 0) == 0) continue;
|
||||||
|
if (line.front() == '[' && line.back() == ']') {
|
||||||
|
close_current();
|
||||||
|
current_path = line.substr(1, line.size() - 2);
|
||||||
|
if (!current_path.empty() && current_path[0] == '-') {
|
||||||
|
RegPath del;
|
||||||
|
if (parse_path(current_path.substr(1), del, error) && !del.subkey.empty()) {
|
||||||
|
RegDeleteTreeW(del.root, del.subkey.c_str());
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
current_path.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RegPath rp;
|
||||||
|
if (!parse_path(current_path, rp, error)) break;
|
||||||
|
LSTATUS st = RegCreateKeyExW(rp.root, rp.subkey.c_str(), 0, nullptr, 0, KEY_SET_VALUE, nullptr, ¤t, nullptr);
|
||||||
|
if (st != ERROR_SUCCESS) { error = win_error(st); break; }
|
||||||
|
changed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
if (!import_value_line(current, line, error)) break;
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_current();
|
||||||
|
if (!error.empty()) send_event("registry_result", err_payload("import_reg", error));
|
||||||
|
else send_event("registry_result", ok_payload("import_reg", "{\"changed\":" + std::to_string(changed) + "}"));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
EXPORT const char *PluginGetRuntime() {
|
||||||
|
return "cpp";
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
EXPORT void PluginSetCallback(uint64_t cb) {
|
||||||
|
std::lock_guard<std::mutex> lk(g_mu);
|
||||||
|
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
|
||||||
|
(void)hostInfo;
|
||||||
|
(void)hostInfoLen;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_mu);
|
||||||
|
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uintptr_t cb, uintptr_t ctx) {
|
||||||
|
(void)hostInfo;
|
||||||
|
(void)hostInfoLen;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_mu);
|
||||||
|
g_callback = reinterpret_cast<host_callback_t>(cb);
|
||||||
|
g_callback_ctx = ctx;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
send_event("ready", "{\"message\":\"registry editor plugin ready\"}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPORT int PluginOnEvent(const char *event, int eventLen, const char *payload, int payloadLen) {
|
||||||
|
std::string ev(event ? event : "", eventLen > 0 ? static_cast<size_t>(eventLen) : 0u);
|
||||||
|
#ifndef _WIN32
|
||||||
|
(void)payload;
|
||||||
|
(void)payloadLen;
|
||||||
|
send_event("registry_result", err_payload(ev, "Registry editor is Windows-only"));
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
if (ev == "list_key") handle_list_key(json_string(payload, payloadLen, "path"));
|
||||||
|
else if (ev == "create_key") handle_create_key(json_string(payload, payloadLen, "path"));
|
||||||
|
else if (ev == "delete_key") handle_delete_key(json_string(payload, payloadLen, "path"));
|
||||||
|
else if (ev == "set_value") handle_set_value(payload, payloadLen);
|
||||||
|
else if (ev == "delete_value") handle_delete_value(json_string(payload, payloadLen, "path"),
|
||||||
|
json_string(payload, payloadLen, "name"));
|
||||||
|
else if (ev == "export_key") handle_export_key(json_string(payload, payloadLen, "path"));
|
||||||
|
else if (ev == "import_reg") handle_import_reg(json_string(payload, payloadLen, "content"));
|
||||||
|
else send_event("registry_result", err_payload(ev, "Unknown event"));
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPORT void PluginOnUnload() {
|
||||||
|
std::lock_guard<std::mutex> lk(g_mu);
|
||||||
|
g_callback = nullptr;
|
||||||
|
#ifndef _WIN32
|
||||||
|
g_callback_ctx = 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-shell {
|
||||||
|
min-height: calc(100vh - 92px);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto minmax(420px, 1fr) 160px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 18px;
|
||||||
|
color: #e8edf4;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-topbar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-topbar p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #9aa7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-box {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: min(340px, 100%);
|
||||||
|
color: #aeb8c6;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #0b1220;
|
||||||
|
color: #f8fafc;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.pathbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pathbar input {
|
||||||
|
flex: 1;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 8px 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #273449;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
border-color: #7f1d1d;
|
||||||
|
background: #5f1f24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(250px, 34%) minmax(360px, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-pane,
|
||||||
|
.value-pane,
|
||||||
|
.console-pane {
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid #243244;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f172a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
height: 38px;
|
||||||
|
border-bottom: 1px solid #243244;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-key {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: #a7f3d0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.loading {
|
||||||
|
color: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.loading::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border: 2px solid #60a5fa;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
animation: regedit-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-tree {
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: #dbe4ef;
|
||||||
|
padding: 7px 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-row:hover,
|
||||||
|
.key-row.active {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-browsing .key-row {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-row .name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon {
|
||||||
|
flex: 0 0 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon.key,
|
||||||
|
.key-icon.hive {
|
||||||
|
background-image: url("data:image/x-icon;base64,AAABAAEAEBAQAAAAAACoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAALu7u7u7u7u7iIi7u7u7u7uIiIi7u7u7u4iIiIu7u7u7iIiIiIu7u7uIiIiIiIu7u4iIiIiIiIu7iIiIiIiIiLuIiIiIiIiIiLu7u7uIiIiIu7u7u7u7u7u7u7u7sAAAALu7u7sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAP8AAP//AAA=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon.root {
|
||||||
|
background-image: url("data:image/x-icon;base64,AAABAAEAEBAQAAAAAACoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AZmZmZmZmYABu7m7ubu5gAG7ubu5u7mAAbu5u7m7uYABmZmZmZmZgAG7uZmZmZmAAbu5mZmZmYABu7mZmZmZgAGZmZmZmZmAAbu5mZmAAYABu7mZmYAZmAG7uZmZgZuZgZmZmZmZu7mYAAAAAAGbmYAAAAAAABmYAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAB3AAAAYwAAAEEAAAAAAAD/wQAA/+MAAP/3AAA=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-icon.up::before {
|
||||||
|
content: "\f062";
|
||||||
|
display: block;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-family: "Font Awesome 6 Free";
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-pane {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-panel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
border: 1px solid #243244;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 10px 11px;
|
||||||
|
color: #b8c8dc;
|
||||||
|
background: #111c2f;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
flex: 0 0 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #60a5fa;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
animation: regedit-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes regedit-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-table th,
|
||||||
|
.value-table td {
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
padding: 9px 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #101b2e;
|
||||||
|
color: #9fb0c4;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-table th:nth-child(1) { width: 26%; }
|
||||||
|
.value-table th:nth-child(2) { width: 140px; }
|
||||||
|
.value-table th:nth-child(4) { width: 130px; }
|
||||||
|
|
||||||
|
.value-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid #8a9bb1;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: linear-gradient(#f7fbff, #dfe8f4);
|
||||||
|
box-shadow: inset 0 0 0 1px #ffffffb0;
|
||||||
|
color: #164a7a;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon.string::before {
|
||||||
|
content: "ab";
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon.number::before {
|
||||||
|
content: "01";
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon.binary::before {
|
||||||
|
content: "10";
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon.unknown::before {
|
||||||
|
content: "?";
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-data {
|
||||||
|
max-height: 96px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #cbd5e1;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions .btn {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-pane pre {
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #b6c2d1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background: rgb(0 0 0 / 58%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: min(520px, 92vw);
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body.wide {
|
||||||
|
min-width: min(760px, 94vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
color: #b8c3d1;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.regedit-shell {
|
||||||
|
padding: 12px;
|
||||||
|
grid-template-rows: auto auto auto minmax(640px, 1fr) 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regedit-topbar,
|
||||||
|
.toolbar,
|
||||||
|
.pathbar {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 260px minmax(360px, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Registry Editor</title>
|
||||||
|
<link rel="stylesheet" href="/plugins/regedit/assets/regedit.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="regedit-shell">
|
||||||
|
<header class="regedit-topbar">
|
||||||
|
<div>
|
||||||
|
<h1>Registry Editor</h1>
|
||||||
|
<p>Browse, edit, import, and export Windows registry keys on the selected client.</p>
|
||||||
|
</div>
|
||||||
|
<label class="client-box">
|
||||||
|
<span>Client ID</span>
|
||||||
|
<input id="client-id" placeholder="clientId" autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="toolbar">
|
||||||
|
<button id="btn-refresh" class="btn primary"><i class="fa-solid fa-rotate"></i><span>Refresh</span></button>
|
||||||
|
<button id="btn-up" class="btn"><i class="fa-solid fa-arrow-up"></i><span>Up</span></button>
|
||||||
|
<button id="btn-new-key" class="btn"><i class="fa-solid fa-folder-plus"></i><span>New Key</span></button>
|
||||||
|
<button id="btn-new-value" class="btn"><i class="fa-solid fa-plus"></i><span>New Value</span></button>
|
||||||
|
<button id="btn-delete-key" class="btn danger"><i class="fa-solid fa-trash"></i><span>Delete Key</span></button>
|
||||||
|
<button id="btn-import" class="btn"><i class="fa-solid fa-file-import"></i><span>Import .reg</span></button>
|
||||||
|
<button id="btn-export" class="btn"><i class="fa-solid fa-file-export"></i><span>Export .reg</span></button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="pathbar">
|
||||||
|
<input id="path-input" spellcheck="false" value="HKCU" />
|
||||||
|
<button id="btn-go" class="btn primary"><i class="fa-solid fa-location-arrow"></i><span>Go</span></button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workspace">
|
||||||
|
<aside class="tree-pane">
|
||||||
|
<div class="pane-title">Keys</div>
|
||||||
|
<div id="key-tree" class="key-tree"></div>
|
||||||
|
</aside>
|
||||||
|
<section class="value-pane">
|
||||||
|
<div class="pane-title">
|
||||||
|
<span id="current-key">HKCU</span>
|
||||||
|
<span id="status-pill" class="status-pill">Idle</span>
|
||||||
|
</div>
|
||||||
|
<table class="value-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="value-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="console-pane">
|
||||||
|
<div class="pane-title">Activity</div>
|
||||||
|
<pre id="log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<dialog id="value-dialog">
|
||||||
|
<form method="dialog" class="dialog-body">
|
||||||
|
<h2 id="value-dialog-title">Edit Value</h2>
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input id="value-name" spellcheck="false" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Type</span>
|
||||||
|
<select id="value-type">
|
||||||
|
<option>REG_SZ</option>
|
||||||
|
<option>REG_EXPAND_SZ</option>
|
||||||
|
<option>REG_DWORD</option>
|
||||||
|
<option>REG_QWORD</option>
|
||||||
|
<option>REG_BINARY</option>
|
||||||
|
<option>REG_MULTI_SZ</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Data</span>
|
||||||
|
<textarea id="value-data" rows="7" spellcheck="false"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button value="cancel" class="btn">Cancel</button>
|
||||||
|
<button id="btn-save-value" value="default" class="btn primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="reg-dialog">
|
||||||
|
<form method="dialog" class="dialog-body wide">
|
||||||
|
<h2 id="reg-dialog-title">.reg Data</h2>
|
||||||
|
<textarea id="reg-text" rows="18" spellcheck="false"></textarea>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button value="cancel" class="btn">Close</button>
|
||||||
|
<button id="btn-run-import" value="default" class="btn primary">Import</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script src="/plugins/regedit/assets/regedit.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
(() => {
|
||||||
|
const PLUGIN_ID = "regedit";
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const clientInput = document.getElementById("client-id");
|
||||||
|
const pathInput = document.getElementById("path-input");
|
||||||
|
const keyTree = document.getElementById("key-tree");
|
||||||
|
const valueBody = document.getElementById("value-body");
|
||||||
|
const currentKey = document.getElementById("current-key");
|
||||||
|
const statusPill = document.getElementById("status-pill");
|
||||||
|
const logEl = document.getElementById("log");
|
||||||
|
const navButtons = [
|
||||||
|
document.getElementById("btn-refresh"),
|
||||||
|
document.getElementById("btn-up"),
|
||||||
|
document.getElementById("btn-go"),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const valueDialog = document.getElementById("value-dialog");
|
||||||
|
const valueTitle = document.getElementById("value-dialog-title");
|
||||||
|
const valueName = document.getElementById("value-name");
|
||||||
|
const valueType = document.getElementById("value-type");
|
||||||
|
const valueData = document.getElementById("value-data");
|
||||||
|
const saveValueBtn = document.getElementById("btn-save-value");
|
||||||
|
|
||||||
|
const regDialog = document.getElementById("reg-dialog");
|
||||||
|
const regTitle = document.getElementById("reg-dialog-title");
|
||||||
|
const regText = document.getElementById("reg-text");
|
||||||
|
const runImportBtn = document.getElementById("btn-run-import");
|
||||||
|
|
||||||
|
const ROOT_PATH = "Computer";
|
||||||
|
const HIVES = [
|
||||||
|
["HKEY_CLASSES_ROOT", "HKCR"],
|
||||||
|
["HKEY_CURRENT_USER", "HKCU"],
|
||||||
|
["HKEY_LOCAL_MACHINE", "HKLM"],
|
||||||
|
["HKEY_USERS", "HKU"],
|
||||||
|
["HKEY_CURRENT_CONFIG", "HKCC"],
|
||||||
|
];
|
||||||
|
|
||||||
|
let activePath = ROOT_PATH;
|
||||||
|
let lastGoodPath = ROOT_PATH;
|
||||||
|
let pendingBrowsePath = null;
|
||||||
|
let isBrowsing = false;
|
||||||
|
let editingOriginalName = null;
|
||||||
|
let pollTimer = null;
|
||||||
|
|
||||||
|
clientInput.value = params.get("clientId") || "";
|
||||||
|
|
||||||
|
function log(line) {
|
||||||
|
const ts = new Date().toLocaleTimeString();
|
||||||
|
logEl.textContent = `${ts} ${line}\n` + logEl.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
statusPill.textContent = text;
|
||||||
|
statusPill.classList.toggle("loading", text === "Loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBrowsing(browsing) {
|
||||||
|
isBrowsing = browsing;
|
||||||
|
document.body.classList.toggle("regedit-browsing", browsing);
|
||||||
|
for (const btn of navButtons) btn.disabled = browsing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClientId() {
|
||||||
|
return clientInput.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEvent(event, payload = {}) {
|
||||||
|
const clientId = getClientId();
|
||||||
|
if (!clientId) {
|
||||||
|
log("Missing clientId");
|
||||||
|
throw new Error("Missing clientId");
|
||||||
|
}
|
||||||
|
setStatus("Working");
|
||||||
|
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ event, payload }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
setStatus("Error");
|
||||||
|
log(`Send failed: ${res.status} ${text}`);
|
||||||
|
throw new Error(text);
|
||||||
|
}
|
||||||
|
log(`Sent ${event}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollEvents() {
|
||||||
|
const clientId = getClientId();
|
||||||
|
if (!clientId) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/events`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
for (const item of data.events || []) handlePluginEvent(item.event, item.payload);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePluginEvent(event, payload) {
|
||||||
|
if (event === "ready") {
|
||||||
|
log(payload?.message || "Registry editor ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event !== "registry_result") return;
|
||||||
|
setStatus(payload?.ok ? "Ready" : "Error");
|
||||||
|
if (!payload?.ok) {
|
||||||
|
const action = payload?.action || "operation";
|
||||||
|
const error = payload?.error || "unknown error";
|
||||||
|
log(`${action} failed: ${error}`);
|
||||||
|
if (action === "list_key") {
|
||||||
|
const failedPath = pendingBrowsePath || pathInput.value || activePath;
|
||||||
|
activePath = lastGoodPath;
|
||||||
|
pendingBrowsePath = null;
|
||||||
|
setBrowsing(false);
|
||||||
|
pathInput.value = activePath;
|
||||||
|
currentKey.textContent = activePath;
|
||||||
|
currentKey.title = activePath;
|
||||||
|
setStatus("Blocked");
|
||||||
|
alert(`Cannot open ${failedPath}.\n\n${error}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.action === "list_key") {
|
||||||
|
activePath = payload.path || activePath;
|
||||||
|
lastGoodPath = activePath;
|
||||||
|
pendingBrowsePath = null;
|
||||||
|
setBrowsing(false);
|
||||||
|
pathInput.value = activePath;
|
||||||
|
currentKey.textContent = activePath;
|
||||||
|
currentKey.title = activePath;
|
||||||
|
renderKeys(payload.subkeys || []);
|
||||||
|
renderValues(payload.values || []);
|
||||||
|
log(`Loaded ${activePath}`);
|
||||||
|
} else if (payload.action === "export_key") {
|
||||||
|
regTitle.textContent = `Export ${payload.path || activePath}`;
|
||||||
|
regText.value = payload.content || "";
|
||||||
|
runImportBtn.style.display = "none";
|
||||||
|
regDialog.showModal();
|
||||||
|
log(`Exported ${payload.path || activePath}`);
|
||||||
|
} else {
|
||||||
|
log(`${payload.action || "operation"} completed`);
|
||||||
|
listKey(activePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKeys(keys) {
|
||||||
|
keyTree.innerHTML = "";
|
||||||
|
if (activePath === ROOT_PATH) {
|
||||||
|
for (const [label, path] of HIVES) {
|
||||||
|
keyTree.appendChild(keyButton(label, path, "hive"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const up = parentPath(activePath);
|
||||||
|
if (up) {
|
||||||
|
const row = keyButton("..", up, "up");
|
||||||
|
keyTree.appendChild(row);
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
keyTree.appendChild(keyButton(key, `${activePath}\\${key}`, "key"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyButton(label, path, iconType) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "key-row";
|
||||||
|
btn.innerHTML = `<span class="key-icon ${iconType}" aria-hidden="true"></span><span class="name"></span>`;
|
||||||
|
btn.querySelector(".name").textContent = label;
|
||||||
|
btn.title = path;
|
||||||
|
btn.addEventListener("click", () => listKey(path));
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderValues(values) {
|
||||||
|
valueBody.innerHTML = "";
|
||||||
|
if (!values.length) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML = `<td colspan="4" class="value-data">No values in this key.</td>`;
|
||||||
|
valueBody.appendChild(tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const value of values) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const displayName = value.name || "(Default)";
|
||||||
|
const type = value.type || "REG_NONE";
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><span class="value-name"><span class="value-icon"></span><span class="value-label"></span></span></td>
|
||||||
|
<td></td>
|
||||||
|
<td><div class="value-data"></div></td>
|
||||||
|
<td><div class="row-actions"></div></td>
|
||||||
|
`;
|
||||||
|
tr.children[0].title = displayName;
|
||||||
|
tr.querySelector(".value-label").textContent = displayName;
|
||||||
|
tr.querySelector(".value-icon").classList.add(valueIconClass(type));
|
||||||
|
tr.children[1].textContent = type;
|
||||||
|
tr.querySelector(".value-data").textContent = value.data ?? "";
|
||||||
|
tr.querySelector(".value-data").title = value.data ?? "";
|
||||||
|
const actions = tr.querySelector(".row-actions");
|
||||||
|
const edit = document.createElement("button");
|
||||||
|
edit.className = "btn";
|
||||||
|
edit.innerHTML = `<i class="fa-solid fa-pen"></i>`;
|
||||||
|
edit.title = "Edit value";
|
||||||
|
edit.addEventListener("click", () => openValueDialog(value));
|
||||||
|
const del = document.createElement("button");
|
||||||
|
del.className = "btn danger";
|
||||||
|
del.innerHTML = `<i class="fa-solid fa-trash"></i>`;
|
||||||
|
del.title = "Delete value";
|
||||||
|
del.addEventListener("click", () => deleteValue(value.name || ""));
|
||||||
|
actions.append(edit, del);
|
||||||
|
valueBody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoading(path) {
|
||||||
|
keyTree.innerHTML = `
|
||||||
|
<div class="loading-panel">
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span>Opening ${escapeHtml(path)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
valueBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="loading-panel table-loading">
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span>Enumerating values and subkeys...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text).replace(/[&<>"']/g, (ch) => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
}[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueIconClass(type) {
|
||||||
|
if (type === "REG_SZ" || type === "REG_EXPAND_SZ" || type === "REG_MULTI_SZ") {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
if (type === "REG_DWORD" || type === "REG_QWORD") return "number";
|
||||||
|
if (type === "REG_BINARY") return "binary";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentPath(path) {
|
||||||
|
if (path === ROOT_PATH) return null;
|
||||||
|
const i = path.lastIndexOf("\\");
|
||||||
|
return i > 0 ? path.slice(0, i) : ROOT_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listKey(path) {
|
||||||
|
if (isBrowsing) return Promise.resolve();
|
||||||
|
const targetPath = normalizePath(path);
|
||||||
|
if (targetPath === ROOT_PATH) {
|
||||||
|
activePath = ROOT_PATH;
|
||||||
|
lastGoodPath = ROOT_PATH;
|
||||||
|
pendingBrowsePath = null;
|
||||||
|
pathInput.value = activePath;
|
||||||
|
currentKey.textContent = activePath;
|
||||||
|
currentKey.title = activePath;
|
||||||
|
setStatus("Ready");
|
||||||
|
setBrowsing(false);
|
||||||
|
renderKeys([]);
|
||||||
|
renderValues([]);
|
||||||
|
log("Loaded Computer");
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
pendingBrowsePath = targetPath;
|
||||||
|
pathInput.value = targetPath;
|
||||||
|
currentKey.textContent = targetPath;
|
||||||
|
currentKey.title = targetPath;
|
||||||
|
setStatus("Loading");
|
||||||
|
setBrowsing(true);
|
||||||
|
renderLoading(targetPath);
|
||||||
|
log(`Opening ${targetPath}`);
|
||||||
|
return sendEvent("list_key", { path: targetPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path) {
|
||||||
|
const raw = (path || "").trim();
|
||||||
|
if (!raw || raw.toLowerCase() === "computer" || raw === "\\") return ROOT_PATH;
|
||||||
|
return raw.replace(/^Computer\\/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openValueDialog(value) {
|
||||||
|
editingOriginalName = value ? value.name || "" : null;
|
||||||
|
valueTitle.textContent = value ? "Edit Value" : "New Value";
|
||||||
|
valueName.value = value ? value.name || "" : "";
|
||||||
|
valueType.value = value ? value.type || "REG_SZ" : "REG_SZ";
|
||||||
|
valueData.value = value ? value.data || "" : "";
|
||||||
|
valueDialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteValue(name) {
|
||||||
|
const display = name || "(Default)";
|
||||||
|
if (!confirm(`Delete value ${display}?`)) return;
|
||||||
|
sendEvent("delete_value", { path: activePath, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("btn-refresh").addEventListener("click", () => listKey(pathInput.value));
|
||||||
|
document.getElementById("btn-go").addEventListener("click", () => listKey(pathInput.value));
|
||||||
|
document.getElementById("btn-up").addEventListener("click", () => {
|
||||||
|
const up = parentPath(activePath);
|
||||||
|
if (up) listKey(up);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-new-key").addEventListener("click", () => {
|
||||||
|
if (activePath === ROOT_PATH) {
|
||||||
|
alert("Select a hive first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = prompt("New key name");
|
||||||
|
if (!name) return;
|
||||||
|
sendEvent("create_key", { path: `${activePath}\\${name}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-delete-key").addEventListener("click", () => {
|
||||||
|
if (activePath === ROOT_PATH || parentPath(activePath) === ROOT_PATH) {
|
||||||
|
alert("Root hives cannot be deleted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Delete key and all subkeys?\n${activePath}`)) return;
|
||||||
|
const next = parentPath(activePath);
|
||||||
|
sendEvent("delete_key", { path: activePath, nextPath: next });
|
||||||
|
activePath = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-new-value").addEventListener("click", () => {
|
||||||
|
if (activePath === ROOT_PATH) {
|
||||||
|
alert("Select a registry key first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openValueDialog(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
saveValueBtn.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
path: activePath,
|
||||||
|
name: valueName.value,
|
||||||
|
oldName: editingOriginalName,
|
||||||
|
type: valueType.value,
|
||||||
|
data: valueData.value,
|
||||||
|
};
|
||||||
|
valueDialog.close();
|
||||||
|
sendEvent("set_value", payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-export").addEventListener("click", () => {
|
||||||
|
if (activePath === ROOT_PATH) {
|
||||||
|
alert("Select a hive or key to export.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendEvent("export_key", { path: activePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-import").addEventListener("click", () => {
|
||||||
|
regTitle.textContent = "Import .reg";
|
||||||
|
regText.value = "Windows Registry Editor Version 5.00\n\n";
|
||||||
|
runImportBtn.style.display = "";
|
||||||
|
regDialog.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
runImportBtn.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!confirm("Import this .reg data on the remote client?")) return;
|
||||||
|
const content = regText.value;
|
||||||
|
regDialog.close();
|
||||||
|
sendEvent("import_reg", { content, refreshPath: activePath });
|
||||||
|
});
|
||||||
|
|
||||||
|
pathInput.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter") listKey(pathInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
clientInput.addEventListener("change", () => {
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
pollTimer = setInterval(pollEvents, 900);
|
||||||
|
listKey(pathInput.value || ROOT_PATH).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
pollTimer = setInterval(pollEvents, 900);
|
||||||
|
listKey(activePath).catch(() => {});
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user