From 1cb6311a6fdc5c55089f6cba21efa0db71a58f99 Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Sun, 15 Feb 2026 16:01:08 +0400 Subject: [PATCH 1/7] Added Emscripten and simple page --- README.md | 51 +++++++++++++ src/tool1cd/wasm_api.cpp | 141 +++++++++++++++++++++++++++++++++++ web/app.js | 157 +++++++++++++++++++++++++++++++++++++++ web/build-wasm.sh | 69 +++++++++++++++++ web/index.html | 127 +++++++++++++++++++++++++++++++ 5 files changed, 545 insertions(+) create mode 100644 src/tool1cd/wasm_api.cpp create mode 100644 web/app.js create mode 100755 web/build-wasm.sh create mode 100644 web/index.html diff --git a/README.md b/README.md index 2280582..1c69150 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,54 @@ sudo apt-get install ctool1cd Если в пути содержатся пробелы, его необходимо заключать в кавычки. Пути следует указывать без завершающего слеша `/` и бэкслеша `\` Для команд `-dc`, `-ddc`, `-drc` вместо пути можно указывать имя файла конфигурации (имя файла должно заканчиваться на `.cf`). + +## WebAssembly (браузерный просмотр таблиц) + +В репозитории добавлен минимальный web-пример: + +- `web/index.html` - страница с загрузкой `.1CD` файла +- `web/app.js` - вызовы WASM API и вывод списка таблиц +- `src/tool1cd/wasm_api.cpp` - C API-обертка над `T_1CD` +- `web/build-wasm.sh` - сборка `parser.js`/`parser.wasm` + +### Что нужно для сборки + +1. Активированный Emscripten (`em++` в `PATH`) +2. Boost, собранный под target Emscripten (нужны `filesystem`, `system`, `regex`) + +### Сборка WASM + +```sh +BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh +``` + +После успешной сборки появятся: + +- `web/parser.js` +- `web/parser.wasm` + +### Локальный запуск + +Нужно запускать через HTTP-сервер (не через `file://`): + +```sh +cd web +python3 -m http.server 8080 +``` + +Открыть в браузере: + +`http://localhost:8080` + +### Экспортируемые функции WASM + +`web/app.js` ожидает наличие функций: + +- `onecd_open` +- `onecd_list_tables_json` +- `onecd_free_string` + +Дополнительно в `wasm_api.cpp` есть: + +- `onecd_last_error` +- `onecd_close` diff --git a/src/tool1cd/wasm_api.cpp b/src/tool1cd/wasm_api.cpp new file mode 100644 index 0000000..91081ba --- /dev/null +++ b/src/tool1cd/wasm_api.cpp @@ -0,0 +1,141 @@ +#include "Class_1CD.h" +#include "Table.h" + +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#else +#define EMSCRIPTEN_KEEPALIVE +#endif + +namespace { + +T_1CD* g_db = nullptr; +std::string g_last_error; + +char* dup_cstr(const std::string& s) { + char* out = static_cast(std::malloc(s.size() + 1)); + if (!out) { + return nullptr; + } + + std::memcpy(out, s.c_str(), s.size() + 1); + return out; +} + +std::string json_escape(const std::string& s) { + std::string out; + out.reserve(s.size() + 16); + + for (char ch : s) { + switch (ch) { + 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 (static_cast(ch) < 0x20) { + out += "\\u00"; + const char* hex = "0123456789abcdef"; + out += hex[(static_cast(ch) >> 4) & 0x0F]; + out += hex[static_cast(ch) & 0x0F]; + } else { + out += ch; + } + break; + } + } + + return out; +} + +void set_error(const std::string& message) { + g_last_error = message; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int onecd_open(const char* path) { + if (!path || !*path) { + set_error("onecd_open: empty path"); + return -1; + } + + try { + delete g_db; + g_db = new T_1CD(); + g_db->open(path, false); + g_last_error.clear(); + return 0; + } catch (const std::exception& ex) { + set_error(ex.what()); + delete g_db; + g_db = nullptr; + return -2; + } catch (...) { + set_error("onecd_open: unknown error"); + delete g_db; + g_db = nullptr; + return -3; + } +} + +EMSCRIPTEN_KEEPALIVE const char* onecd_list_tables_json() { + if (!g_db || !g_db->is_open()) { + set_error("Database is not open"); + return dup_cstr("[]"); + } + + try { + const int table_count = g_db->get_numtables(); + std::string json; + json.reserve(static_cast(table_count) * 24 + 2); + json.push_back('['); + + for (int i = 0; i < table_count; ++i) { + if (i > 0) { + json.push_back(','); + } + + Table* table = g_db->get_table(i); + json.push_back('"'); + json += json_escape(table ? table->get_name() : std::string()); + json.push_back('"'); + } + + json.push_back(']'); + g_last_error.clear(); + return dup_cstr(json); + } catch (const std::exception& ex) { + set_error(ex.what()); + return dup_cstr("[]"); + } catch (...) { + set_error("onecd_list_tables_json: unknown error"); + return dup_cstr("[]"); + } +} + +EMSCRIPTEN_KEEPALIVE const char* onecd_last_error() { + return dup_cstr(g_last_error); +} + +EMSCRIPTEN_KEEPALIVE void onecd_close() { + delete g_db; + g_db = nullptr; + g_last_error.clear(); +} + +EMSCRIPTEN_KEEPALIVE void onecd_free_string(const char* ptr) { + std::free(const_cast(ptr)); +} + +} // extern "C" diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..8a20f27 --- /dev/null +++ b/web/app.js @@ -0,0 +1,157 @@ +const statusEl = document.getElementById("status"); +const errorEl = document.getElementById("error"); +const fileInput = document.getElementById("fileInput"); +const openBtn = document.getElementById("openBtn"); +const tablesEl = document.getElementById("tables"); + +let api = null; + +function setStatus(message) { + statusEl.textContent = message; +} + +function showError(message) { + errorEl.style.display = "block"; + errorEl.textContent = message; +} + +function clearError() { + errorEl.style.display = "none"; + errorEl.textContent = ""; +} + +function clearTables() { + tablesEl.replaceChildren(); +} + +function renderTables(tableNames) { + clearTables(); + for (const name of tableNames) { + const li = document.createElement("li"); + li.textContent = String(name); + tablesEl.appendChild(li); + } +} + +function bindApi(Module) { + return { + open: Module.cwrap("onecd_open", "number", ["string"]), + listTablesJsonPtr: Module.cwrap("onecd_list_tables_json", "number", []), + freeString: Module.cwrap("onecd_free_string", null, ["number"]), + utf8ToString: Module.UTF8ToString.bind(Module), + FS: Module.FS, + }; +} + +async function initModule() { + if (typeof Module === "undefined") { + throw new Error("parser.js did not define global Module"); + } + + const runtimeReady = + typeof Module.ready === "object" && typeof Module.ready.then === "function" + ? Module.ready + : new Promise((resolve) => { + const previous = Module.onRuntimeInitialized; + Module.onRuntimeInitialized = () => { + if (typeof previous === "function") previous(); + resolve(); + }; + }); + + await runtimeReady; + api = bindApi(Module); +} + +async function fileToUint8Array(file) { + const buf = await file.arrayBuffer(); + return new Uint8Array(buf); +} + +function readTablesJson() { + const ptr = api.listTablesJsonPtr(); + if (!ptr) { + throw new Error("onecd_list_tables_json returned null pointer"); + } + + try { + const jsonText = api.utf8ToString(ptr); + return JSON.parse(jsonText); + } finally { + api.freeString(ptr); + } +} + +async function onOpenClick() { + clearError(); + clearTables(); + + const file = fileInput.files?.[0]; + if (!file) { + showError("Please choose a .1CD file first."); + return; + } + + const wasmPath = `/tmp/${file.name}`; + + try { + setStatus("Reading selected file..."); + const bytes = await fileToUint8Array(file); + + setStatus("Writing file to WASM FS..."); + api.FS.writeFile(wasmPath, bytes); + + setStatus("Opening 1CD file..."); + const openResult = api.open(wasmPath); + if (openResult !== 0) { + throw new Error(`onecd_open failed with code ${openResult}`); + } + + setStatus("Loading table list..."); + const parsed = readTablesJson(); + const tableNames = Array.isArray(parsed) ? parsed : parsed.tables; + + if (!Array.isArray(tableNames)) { + throw new Error("Table JSON format is unsupported. Expected array or {tables:[...]}"); + } + + renderTables(tableNames); + setStatus(`Loaded ${tableNames.length} tables from ${file.name}`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + showError(message); + setStatus("Failed to open file."); + } finally { + try { + api.FS.unlink(wasmPath); + } catch (_) { + // Ignore cleanup errors. + } + } +} + +async function main() { + clearError(); + clearTables(); + openBtn.disabled = true; + + try { + await initModule(); + setStatus("WASM runtime is ready."); + openBtn.disabled = false; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + showError(`WASM init error: ${message}`); + setStatus("Runtime initialization failed."); + } +} + +fileInput.addEventListener("change", () => { + clearError(); +}); + +openBtn.addEventListener("click", () => { + void onOpenClick(); +}); + +void main(); diff --git a/web/build-wasm.sh b/web/build-wasm.sh new file mode 100755 index 0000000..ef34717 --- /dev/null +++ b/web/build-wasm.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build tool1cd WASM API for browser usage. +# Output: web/parser.js + web/parser.wasm +# +# Prereqs: +# 1) Emscripten activated (em++ available) +# 2) Boost built for Emscripten (filesystem/system/regex) +# +# Example: +#BOOST_WASM_ROOT=$HOME/opt/boost-wasm ./web/build-wasm.sh + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="$ROOT_DIR/web" +SRC_DIR="$ROOT_DIR/src/tool1cd" + +BOOST_WASM_ROOT="${BOOST_WASM_ROOT:-}" +if [[ -z "${BOOST_WASM_ROOT}" ]]; then + echo "ERROR: Set BOOST_WASM_ROOT to your Boost-for-WASM prefix." >&2 + exit 1 +fi + +if ! command -v em++ >/dev/null 2>&1; then + echo "ERROR: em++ not found. Activate emsdk first." >&2 + exit 1 +fi + +TOOL1CD_SOURCES=( + main.cpp MessageRegistration.cpp Class_1CD.cpp + Common.cpp ConfigStorage.cpp Parse_tree.cpp TempStream.cpp Base64.cpp UZLib.cpp Messenger.cpp + V8Object.cpp Field.cpp Index.cpp Table.cpp TableFiles.cpp TableFileStream.cpp + MemBlock.cpp CRC32.cpp Packdata.cpp PackDirectory.cpp FieldType.cpp DetailedException.cpp + BinaryDecimalNumber.cpp save_depot_config.cpp save_part_depot_config.cpp + SupplierConfig.cpp TableRecord.cpp BinaryGuid.cpp TableIterator.cpp SupplierConfigBuilder.cpp + cfapi/V8File.cpp cfapi/V8Catalog.cpp cfapi/TV8FileStream.cpp cfapi/APIcfBase.cpp cfapi/V8Time.cpp + SystemClasses/String.cpp SystemClasses/System.Classes.cpp SystemClasses/System.cpp + SystemClasses/System.IOUtils.cpp SystemClasses/TFileStream.cpp SystemClasses/TMemoryStream.cpp + SystemClasses/TStream.cpp SystemClasses/TStreamReader.cpp SystemClasses/TStreamWriter.cpp + SystemClasses/System.SysUtils.cpp SystemClasses/GetTickCount.cpp + wasm_api.cpp +) + +ABS_SOURCES=() +for src in "${TOOL1CD_SOURCES[@]}"; do + ABS_SOURCES+=("$SRC_DIR/$src") +done + +mkdir -p "$OUT_DIR" + +em++ \ + "${ABS_SOURCES[@]}" \ + -std=c++11 -O2 \ + -I"$SRC_DIR" \ + -I"$BOOST_WASM_ROOT/include" \ + -L"$BOOST_WASM_ROOT/lib" \ + -lboost_filesystem -lboost_system -lboost_regex \ + -sUSE_ZLIB=1 \ + -sFORCE_FILESYSTEM=1 \ + -sALLOW_MEMORY_GROWTH=1 \ + -sENVIRONMENT=web \ + -sMODULARIZE=0 \ + -sEXPORT_ES6=0 \ + -sNO_EXIT_RUNTIME=1 \ + -sEXPORTED_FUNCTIONS="['_malloc','_free','_onecd_open','_onecd_list_tables_json','_onecd_last_error','_onecd_close','_onecd_free_string']" \ + -sEXPORTED_RUNTIME_METHODS="['FS','cwrap','UTF8ToString']" \ + -o "$OUT_DIR/parser.js" + +echo "Built: $OUT_DIR/parser.js and $OUT_DIR/parser.wasm" diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e2a4f10 --- /dev/null +++ b/web/index.html @@ -0,0 +1,127 @@ + + + + + + 1CD Tables Viewer + + + +
+
+

1CD Tables Viewer

+ +
+ + +
+ +

Loading WASM runtime...

+ + +

Tables

+
    +

    Expected WASM exports: onecd_open, onecd_list_tables_json, onecd_free_string

    +
    +
    + + + + + + From 073ac45e7794efe79d707c80da5f0d031dec283d Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Sun, 15 Feb 2026 16:25:45 +0400 Subject: [PATCH 2/7] added size of tables and sorting --- src/tool1cd/wasm_api.cpp | 27 ++++- web/app.js | 211 ++++++++++++++++++++++++++++++++++++--- web/index.html | 76 +++++++++++++- 3 files changed, 289 insertions(+), 25 deletions(-) diff --git a/src/tool1cd/wasm_api.cpp b/src/tool1cd/wasm_api.cpp index 91081ba..23469ae 100644 --- a/src/tool1cd/wasm_api.cpp +++ b/src/tool1cd/wasm_api.cpp @@ -60,6 +60,20 @@ void set_error(const std::string& message) { g_last_error = message; } +uint64_t object_size(const V8Object* file) { + return file ? file->get_len() : 0; +} + +uint64_t table_total_size(const Table* table) { + if (!table) { + return 0; + } + + return object_size(table->get_file_data()) + + object_size(table->get_file_blob()) + + object_size(table->get_file_index()); +} + } // namespace extern "C" { @@ -98,7 +112,7 @@ EMSCRIPTEN_KEEPALIVE const char* onecd_list_tables_json() { try { const int table_count = g_db->get_numtables(); std::string json; - json.reserve(static_cast(table_count) * 24 + 2); + json.reserve(static_cast(table_count) * 64 + 2); json.push_back('['); for (int i = 0; i < table_count; ++i) { @@ -107,9 +121,14 @@ EMSCRIPTEN_KEEPALIVE const char* onecd_list_tables_json() { } Table* table = g_db->get_table(i); - json.push_back('"'); - json += json_escape(table ? table->get_name() : std::string()); - json.push_back('"'); + const std::string table_name = table ? table->get_name() : std::string(); + const uint64_t table_size = table_total_size(table); + + json += "{\"name\":\""; + json += json_escape(table_name); + json += "\",\"size\":"; + json += std::to_string(table_size); + json.push_back('}'); } json.push_back(']'); diff --git a/web/app.js b/web/app.js index 8a20f27..ce4225f 100644 --- a/web/app.js +++ b/web/app.js @@ -3,8 +3,11 @@ const errorEl = document.getElementById("error"); const fileInput = document.getElementById("fileInput"); const openBtn = document.getElementById("openBtn"); const tablesEl = document.getElementById("tables"); +const sortFieldEl = document.getElementById("sortField"); +const sortDirEl = document.getElementById("sortDir"); let api = null; +let currentTables = []; function setStatus(message) { statusEl.textContent = message; @@ -24,20 +27,193 @@ function clearTables() { tablesEl.replaceChildren(); } -function renderTables(tableNames) { +function formatSize(bytes) { + if (!Number.isFinite(bytes) || bytes < 0) return "0 B"; + + const units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + + return `${value.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`; +} + +function extractName(value, depth = 0) { + if (depth > 10 || value == null) return ""; + if (typeof value === "string") return value; + + if (Array.isArray(value)) { + for (const item of value) { + const candidate = extractName(item, depth + 1); + if (candidate) return candidate; + } + return ""; + } + + if (typeof value === "object") { + const preferredKeys = [ + "name", + "Name", + "tableName", + "table_name", + "table", + "title", + "value", + ]; + for (const key of preferredKeys) { + if (key in value) { + const candidate = extractName(value[key], depth + 1); + if (candidate) return candidate; + } + } + + for (const nestedValue of Object.values(value)) { + const candidate = extractName(nestedValue, depth + 1); + if (candidate) return candidate; + } + } + + return ""; +} + +function extractSize(value, depth = 0) { + if (depth > 3 || value == null) return 0; + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + } + + if (Array.isArray(value)) { + for (const item of value) { + const candidate = extractSize(item, depth + 1); + if (candidate > 0) return candidate; + } + return 0; + } + + if (typeof value === "object") { + const preferredKeys = ["size", "totalSize", "bytes", "value"]; + for (const key of preferredKeys) { + if (key in value) { + const candidate = extractSize(value[key], depth + 1); + if (candidate > 0) return candidate; + } + } + } + + return 0; +} + +function normalizeTables(parsed) { + if (!Array.isArray(parsed)) { + if (parsed && Array.isArray(parsed.tables)) { + return normalizeTables(parsed.tables); + } + throw new Error("Table JSON format is unsupported. Expected array or {tables:[...]}"); + } + + return parsed.map((item) => { + if (typeof item === "string") { + return { name: item, size: 0, raw: item }; + } + + const rawName = item?.name ?? item; + const rawSize = item?.size ?? item; + const normalizedName = extractName(rawName) || extractName(item) || "(unknown)"; + return { + name: normalizedName === "[object Object]" ? "(unknown)" : normalizedName, + size: extractSize(rawSize), + raw: item, + }; + }); +} + +function sortedTables() { + const sortField = sortFieldEl.value; + const isAsc = sortDirEl.value === "asc"; + const sign = isAsc ? 1 : -1; + const rows = [...currentTables]; + + rows.sort((a, b) => { + if (sortField === "size") { + if (a.size !== b.size) { + return (a.size - b.size) * sign; + } + return a.name.localeCompare(b.name) * sign; + } + + const nameCmp = a.name.localeCompare(b.name); + if (nameCmp !== 0) { + return nameCmp * sign; + } + return (a.size - b.size) * sign; + }); + + return rows; +} + +function renderTables() { clearTables(); - for (const name of tableNames) { - const li = document.createElement("li"); - li.textContent = String(name); - tablesEl.appendChild(li); + + for (const table of sortedTables()) { + const tr = document.createElement("tr"); + + const nameTd = document.createElement("td"); + const fallbackName = + extractName(table.raw) || + (table.raw && typeof table.raw === "object" ? JSON.stringify(table.raw) : ""); + const displayName = + typeof table.name === "string" && table.name !== "[object Object]" + ? table.name + : (fallbackName || "(unknown)"); + nameTd.textContent = displayName; + + const sizeTd = document.createElement("td"); + sizeTd.textContent = formatSize(table.size); + sizeTd.className = "size"; + + tr.appendChild(nameTd); + tr.appendChild(sizeTd); + tablesEl.appendChild(tr); } } function bindApi(Module) { + const hasCwrap = typeof Module.cwrap === "function"; + const wrap = (name, returnType, argTypes) => { + if (hasCwrap) { + const fn = Module.cwrap(name, returnType, argTypes); + if (typeof fn === "function") return fn; + } + const direct = Module[`_${name}`]; + return typeof direct === "function" ? direct : null; + }; + + const open = wrap("onecd_open", "number", ["string"]); + const listTablesJsonPtr = wrap("onecd_list_tables_json", "number", []); + const freeString = wrap("onecd_free_string", null, ["number"]) + || (typeof Module._free === "function" ? Module._free : null); + + const missing = []; + if (!open) missing.push("onecd_open"); + if (!listTablesJsonPtr) missing.push("onecd_list_tables_json"); + if (!freeString) missing.push("onecd_free_string"); + if (missing.length) { + throw new Error( + `Missing WASM exports: ${missing.join(", ")}. ` + + "Rebuild and refresh parser.js/parser.wasm." + ); + } + return { - open: Module.cwrap("onecd_open", "number", ["string"]), - listTablesJsonPtr: Module.cwrap("onecd_list_tables_json", "number", []), - freeString: Module.cwrap("onecd_free_string", null, ["number"]), + open, + listTablesJsonPtr, + freeString, utf8ToString: Module.UTF8ToString.bind(Module), FS: Module.FS, }; @@ -109,14 +285,9 @@ async function onOpenClick() { setStatus("Loading table list..."); const parsed = readTablesJson(); - const tableNames = Array.isArray(parsed) ? parsed : parsed.tables; - - if (!Array.isArray(tableNames)) { - throw new Error("Table JSON format is unsupported. Expected array or {tables:[...]}"); - } - - renderTables(tableNames); - setStatus(`Loaded ${tableNames.length} tables from ${file.name}`); + currentTables = normalizeTables(parsed); + renderTables(); + setStatus(`Loaded ${currentTables.length} tables from ${file.name}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); showError(message); @@ -154,4 +325,12 @@ openBtn.addEventListener("click", () => { void onOpenClick(); }); +sortFieldEl.addEventListener("change", () => { + renderTables(); +}); + +sortDirEl.addEventListener("change", () => { + renderTables(); +}); + void main(); diff --git a/web/index.html b/web/index.html index e2a4f10..174b62c 100644 --- a/web/index.html +++ b/web/index.html @@ -49,6 +49,27 @@ margin-bottom: 1rem; } + .sort-controls { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; + margin-bottom: 1rem; + } + + .sort-controls label { + color: var(--muted); + font-size: 0.95rem; + } + + .sort-controls select { + border: 1px solid var(--line); + background: white; + color: var(--text); + border-radius: 8px; + padding: 0.45rem 0.55rem; + } + button { border: 1px solid var(--accent); background: var(--accent); @@ -82,16 +103,39 @@ white-space: pre-wrap; } - #tables { - margin: 0; - padding-left: 1.25rem; + .tables-list { + width: 100%; + border-collapse: collapse; max-height: 60vh; overflow: auto; + display: block; } - #tables li { + .tables-list thead, + .tables-list tbody { + display: table; + width: 100%; + table-layout: fixed; + } + + .tables-list thead th { + text-align: left; + font-size: 0.9rem; + color: var(--muted); + border-bottom: 1px solid var(--line); + padding: 0.45rem 0.25rem; + } + + .tables-list td { margin: 0.25rem 0; font-family: Consolas, Monaco, monospace; + border-bottom: 1px solid #eef2f7; + padding: 0.5rem 0.25rem; + } + + .tables-list td.size, + .tables-list th.size { + text-align: right; } .hint { @@ -115,7 +159,29 @@

    1CD Tables Viewer

    Tables

    -
      +
      + + + + + +
      + + + + + + + + + +
      NameSize

      Expected WASM exports: onecd_open, onecd_list_tables_json, onecd_free_string

      From c0322b4551f8e915783192921ffeead0a0c6b34d Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Sun, 15 Feb 2026 18:22:17 +0400 Subject: [PATCH 3/7] Added viewing of table contents on the web page --- src/tool1cd/wasm_api.cpp | 126 +++++++++++++++++++++++++++++++++++++++ web/app.js | 124 +++++++++++++++++++++++++++++++++++++- web/build-wasm.sh | 2 +- web/index.html | 73 ++++++++++++++++++++++- 4 files changed, 322 insertions(+), 3 deletions(-) diff --git a/src/tool1cd/wasm_api.cpp b/src/tool1cd/wasm_api.cpp index 23469ae..2c84401 100644 --- a/src/tool1cd/wasm_api.cpp +++ b/src/tool1cd/wasm_api.cpp @@ -1,10 +1,12 @@ #include "Class_1CD.h" +#include "Field.h" #include "Table.h" #include #include #include #include +#include #ifdef __EMSCRIPTEN__ #include @@ -74,6 +76,22 @@ uint64_t table_total_size(const Table* table) { + object_size(table->get_file_index()); } +Table* find_table_by_name(const std::string& table_name) { + if (!g_db || !g_db->is_open()) { + return nullptr; + } + + const int table_count = g_db->get_numtables(); + for (int i = 0; i < table_count; ++i) { + Table* table = g_db->get_table(i); + if (table && table->get_name() == table_name) { + return table; + } + } + + return nullptr; +} + } // namespace extern "C" { @@ -143,6 +161,114 @@ EMSCRIPTEN_KEEPALIVE const char* onecd_list_tables_json() { } } +EMSCRIPTEN_KEEPALIVE const char* onecd_get_table_rows_json(const char* table_name, int offset, int limit) { + if (!g_db || !g_db->is_open()) { + set_error("Database is not open"); + return dup_cstr("{\"error\":\"Database is not open\"}"); + } + + if (!table_name || !*table_name) { + set_error("onecd_get_table_rows_json: empty table name"); + return dup_cstr("{\"error\":\"empty table name\"}"); + } + + if (offset < 0) { + offset = 0; + } + if (limit <= 0) { + limit = 100; + } + if (limit > 200) { + limit = 200; + } + + try { + Table* table = find_table_by_name(table_name); + if (!table) { + set_error(std::string("Table not found: ") + table_name); + return dup_cstr("{\"error\":\"table not found\"}"); + } + + const uint32_t total_rows = table->get_phys_numrecords(); + const int field_count = table->get_num_fields(); + const int start = offset > static_cast(total_rows) ? static_cast(total_rows) : offset; + const int end = (start + limit) > static_cast(total_rows) + ? static_cast(total_rows) + : (start + limit); + + std::string json; + json.reserve(static_cast(field_count) * 64 + static_cast(end - start) * 256 + 256); + json += "{\"table\":\""; + json += json_escape(table->get_name()); + json += "\",\"offset\":"; + json += std::to_string(start); + json += ",\"limit\":"; + json += std::to_string(limit); + json += ",\"totalRows\":"; + json += std::to_string(total_rows); + json += ",\"fields\":["; + + for (int i = 0; i < field_count; ++i) { + if (i > 0) { + json.push_back(','); + } + Field* field = table->get_field(i); + json += "{\"name\":\""; + json += json_escape(field ? field->get_name() : std::string()); + json += "\"}"; + } + + json += "],\"rows\":["; + std::vector record_buf(static_cast(table->get_recordlen())); + + for (int row = start; row < end; ++row) { + if (row > start) { + json.push_back(','); + } + + table->get_record(static_cast(row), record_buf.data()); + + json += "{\"row\":"; + json += std::to_string(row); + json += ",\"deleted\":"; + json += (record_buf[0] != '\0') ? "true" : "false"; + json += ",\"values\":["; + + for (int col = 0; col < field_count; ++col) { + if (col > 0) { + json.push_back(','); + } + + Field* field = table->get_field(col); + std::string value; + try { + value = field ? field->get_presentation(record_buf.data()) : std::string(); + } catch (const std::exception& ex) { + value = std::string("{ERROR: ") + ex.what() + "}"; + } catch (...) { + value = "{ERROR}"; + } + + json.push_back('"'); + json += json_escape(value); + json.push_back('"'); + } + + json += "]}"; + } + + json += "]}"; + g_last_error.clear(); + return dup_cstr(json); + } catch (const std::exception& ex) { + set_error(ex.what()); + return dup_cstr("{\"error\":\"internal error\"}"); + } catch (...) { + set_error("onecd_get_table_rows_json: unknown error"); + return dup_cstr("{\"error\":\"unknown error\"}"); + } +} + EMSCRIPTEN_KEEPALIVE const char* onecd_last_error() { return dup_cstr(g_last_error); } diff --git a/web/app.js b/web/app.js index ce4225f..6ce702d 100644 --- a/web/app.js +++ b/web/app.js @@ -5,9 +5,15 @@ const openBtn = document.getElementById("openBtn"); const tablesEl = document.getElementById("tables"); const sortFieldEl = document.getElementById("sortField"); const sortDirEl = document.getElementById("sortDir"); +const tableMetaEl = document.getElementById("tableMeta"); +const tableContentHeadEl = document.getElementById("tableContentHead"); +const tableContentBodyEl = document.getElementById("tableContentBody"); let api = null; let currentTables = []; +let selectedTableName = ""; +let currentWasmPath = ""; +const TABLE_PREVIEW_LIMIT = 100; function setStatus(message) { statusEl.textContent = message; @@ -27,6 +33,12 @@ function clearTables() { tablesEl.replaceChildren(); } +function clearTableContent() { + tableContentHeadEl.replaceChildren(); + tableContentBodyEl.replaceChildren(); + tableMetaEl.textContent = "Select a table to preview first 100 rows."; +} + function formatSize(bytes) { if (!Number.isFinite(bytes) || bytes < 0) return "0 B"; @@ -162,6 +174,7 @@ function renderTables() { for (const table of sortedTables()) { const tr = document.createElement("tr"); + tr.classList.toggle("active", table.name === selectedTableName); const nameTd = document.createElement("td"); const fallbackName = @@ -179,10 +192,61 @@ function renderTables() { tr.appendChild(nameTd); tr.appendChild(sizeTd); + tr.addEventListener("click", () => { + void loadTableContent(table.name); + }); tablesEl.appendChild(tr); } } +function renderTableContent(payload) { + tableContentHeadEl.replaceChildren(); + tableContentBodyEl.replaceChildren(); + + const fields = Array.isArray(payload?.fields) ? payload.fields : []; + const rows = Array.isArray(payload?.rows) ? payload.rows : []; + + const headerRow = document.createElement("tr"); + const rowNumTh = document.createElement("th"); + rowNumTh.className = "rownum"; + rowNumTh.textContent = "#"; + headerRow.appendChild(rowNumTh); + + for (const field of fields) { + const th = document.createElement("th"); + th.textContent = String(field?.name ?? ""); + headerRow.appendChild(th); + } + tableContentHeadEl.appendChild(headerRow); + + for (const row of rows) { + const tr = document.createElement("tr"); + if (row?.deleted) { + tr.classList.add("deleted"); + } + + const rowNumTd = document.createElement("td"); + rowNumTd.className = "rownum"; + rowNumTd.textContent = String(row?.row ?? ""); + tr.appendChild(rowNumTd); + + const values = Array.isArray(row?.values) ? row.values : []; + for (const value of values) { + const td = document.createElement("td"); + td.textContent = String(value ?? ""); + tr.appendChild(td); + } + + tableContentBodyEl.appendChild(tr); + } + + const offset = Number(payload?.offset) || 0; + const totalRows = Number(payload?.totalRows) || 0; + const shownTo = Math.min(offset + rows.length, totalRows); + tableMetaEl.textContent = + `${payload?.table ?? ""}: showing ${offset}-${shownTo} of ${totalRows} rows`; +} + function bindApi(Module) { const hasCwrap = typeof Module.cwrap === "function"; const wrap = (name, returnType, argTypes) => { @@ -196,13 +260,17 @@ function bindApi(Module) { const open = wrap("onecd_open", "number", ["string"]); const listTablesJsonPtr = wrap("onecd_list_tables_json", "number", []); + const getTableRowsJsonPtr = wrap("onecd_get_table_rows_json", "number", ["string", "number", "number"]); + const close = wrap("onecd_close", null, []); const freeString = wrap("onecd_free_string", null, ["number"]) || (typeof Module._free === "function" ? Module._free : null); const missing = []; if (!open) missing.push("onecd_open"); if (!listTablesJsonPtr) missing.push("onecd_list_tables_json"); + if (!getTableRowsJsonPtr) missing.push("onecd_get_table_rows_json"); if (!freeString) missing.push("onecd_free_string"); + if (!close) missing.push("onecd_close"); if (missing.length) { throw new Error( `Missing WASM exports: ${missing.join(", ")}. ` + @@ -213,6 +281,8 @@ function bindApi(Module) { return { open, listTablesJsonPtr, + getTableRowsJsonPtr, + close, freeString, utf8ToString: Module.UTF8ToString.bind(Module), FS: Module.FS, @@ -258,9 +328,44 @@ function readTablesJson() { } } +function readTableRowsJson(tableName, offset = 0, limit = TABLE_PREVIEW_LIMIT) { + const ptr = api.getTableRowsJsonPtr(tableName, offset, limit); + if (!ptr) { + throw new Error("onecd_get_table_rows_json returned null pointer"); + } + + try { + const jsonText = api.utf8ToString(ptr); + const parsed = JSON.parse(jsonText); + if (parsed && typeof parsed === "object" && parsed.error) { + throw new Error(String(parsed.error)); + } + return parsed; + } finally { + api.freeString(ptr); + } +} + +async function loadTableContent(tableName) { + clearError(); + setStatus(`Loading rows from ${tableName}...`); + try { + const payload = readTableRowsJson(tableName, 0, TABLE_PREVIEW_LIMIT); + selectedTableName = tableName; + renderTables(); + renderTableContent(payload); + setStatus(`Loaded ${tableName} preview`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + showError(message); + setStatus("Failed to load table content."); + } +} + async function onOpenClick() { clearError(); clearTables(); + clearTableContent(); const file = fileInput.files?.[0]; if (!file) { @@ -271,6 +376,20 @@ async function onOpenClick() { const wasmPath = `/tmp/${file.name}`; try { + if (currentWasmPath) { + try { + api.close(); + } catch (_) { + // Ignore close errors. + } + try { + api.FS.unlink(currentWasmPath); + } catch (_) { + // Ignore cleanup errors. + } + currentWasmPath = ""; + } + setStatus("Reading selected file..."); const bytes = await fileToUint8Array(file); @@ -286,13 +405,15 @@ async function onOpenClick() { setStatus("Loading table list..."); const parsed = readTablesJson(); currentTables = normalizeTables(parsed); + selectedTableName = ""; renderTables(); + clearTableContent(); + currentWasmPath = wasmPath; setStatus(`Loaded ${currentTables.length} tables from ${file.name}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); showError(message); setStatus("Failed to open file."); - } finally { try { api.FS.unlink(wasmPath); } catch (_) { @@ -304,6 +425,7 @@ async function onOpenClick() { async function main() { clearError(); clearTables(); + clearTableContent(); openBtn.disabled = true; try { diff --git a/web/build-wasm.sh b/web/build-wasm.sh index ef34717..1ffa3a2 100755 --- a/web/build-wasm.sh +++ b/web/build-wasm.sh @@ -62,7 +62,7 @@ em++ \ -sMODULARIZE=0 \ -sEXPORT_ES6=0 \ -sNO_EXIT_RUNTIME=1 \ - -sEXPORTED_FUNCTIONS="['_malloc','_free','_onecd_open','_onecd_list_tables_json','_onecd_last_error','_onecd_close','_onecd_free_string']" \ + -sEXPORTED_FUNCTIONS="['_malloc','_free','_onecd_open','_onecd_list_tables_json','_onecd_get_table_rows_json','_onecd_last_error','_onecd_close','_onecd_free_string']" \ -sEXPORTED_RUNTIME_METHODS="['FS','cwrap','UTF8ToString']" \ -o "$OUT_DIR/parser.js" diff --git a/web/index.html b/web/index.html index 174b62c..b8abbd5 100644 --- a/web/index.html +++ b/web/index.html @@ -133,11 +133,72 @@ padding: 0.5rem 0.25rem; } + .tables-list tbody tr { + cursor: pointer; + } + + .tables-list tbody tr:hover { + background: #f8fafc; + } + + .tables-list tbody tr.active { + background: #ecfeff; + } + .tables-list td.size, .tables-list th.size { text-align: right; } + #tableMeta { + margin: 0.5rem 0; + color: var(--muted); + font-size: 0.95rem; + } + + .table-content-wrap { + overflow: auto; + max-height: 50vh; + border: 1px solid #eef2f7; + border-radius: 8px; + } + + .table-content { + width: max-content; + min-width: 100%; + border-collapse: collapse; + } + + .table-content th, + .table-content td { + border-bottom: 1px solid #eef2f7; + padding: 0.4rem 0.5rem; + font-family: Consolas, Monaco, monospace; + font-size: 0.9rem; + white-space: nowrap; + vertical-align: top; + } + + .table-content th { + position: sticky; + top: 0; + z-index: 1; + background: #f9fafb; + color: var(--muted); + text-align: left; + } + + .table-content .rownum { + color: var(--muted); + text-align: right; + min-width: 4rem; + } + + .table-content tr.deleted td { + color: #9ca3af; + font-style: italic; + } + .hint { color: var(--muted); margin-top: 0.75rem; @@ -182,7 +243,17 @@

      Tables

      -

      Expected WASM exports: onecd_open, onecd_list_tables_json, onecd_free_string

      +

      Click a table name to view rows.

      + +

      Table Content

      +

      Select a table to preview first 100 rows.

      +
      + + + +
      +
      +

      Expected WASM exports: onecd_open, onecd_list_tables_json, onecd_get_table_rows_json, onecd_free_string

      From 916f5a62bdf3caa829def009fd931444ecb35d52 Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Mon, 16 Feb 2026 03:49:54 +0400 Subject: [PATCH 4/7] Added viewing of table contents on the web page --- README.md | 23 -------------- web/build-wasm.sh | 2 +- {src/tool1cd => web}/wasm_api.cpp | 51 +++++++++++++++++++++---------- 3 files changed, 36 insertions(+), 40 deletions(-) rename {src/tool1cd => web}/wasm_api.cpp (84%) diff --git a/README.md b/README.md index 1c69150..60bbe56 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,6 @@ sudo apt-get install ctool1cd ## WebAssembly (браузерный просмотр таблиц) -В репозитории добавлен минимальный web-пример: - -- `web/index.html` - страница с загрузкой `.1CD` файла -- `web/app.js` - вызовы WASM API и вывод списка таблиц -- `src/tool1cd/wasm_api.cpp` - C API-обертка над `T_1CD` -- `web/build-wasm.sh` - сборка `parser.js`/`parser.wasm` - ### Что нужно для сборки 1. Активированный Emscripten (`em++` в `PATH`) @@ -120,7 +113,6 @@ sudo apt-get install ctool1cd ```sh BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh ``` - После успешной сборки появятся: - `web/parser.js` @@ -128,8 +120,6 @@ BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh ### Локальный запуск -Нужно запускать через HTTP-сервер (не через `file://`): - ```sh cd web python3 -m http.server 8080 @@ -138,16 +128,3 @@ python3 -m http.server 8080 Открыть в браузере: `http://localhost:8080` - -### Экспортируемые функции WASM - -`web/app.js` ожидает наличие функций: - -- `onecd_open` -- `onecd_list_tables_json` -- `onecd_free_string` - -Дополнительно в `wasm_api.cpp` есть: - -- `onecd_last_error` -- `onecd_close` diff --git a/web/build-wasm.sh b/web/build-wasm.sh index 1ffa3a2..939a98f 100755 --- a/web/build-wasm.sh +++ b/web/build-wasm.sh @@ -38,13 +38,13 @@ TOOL1CD_SOURCES=( SystemClasses/System.IOUtils.cpp SystemClasses/TFileStream.cpp SystemClasses/TMemoryStream.cpp SystemClasses/TStream.cpp SystemClasses/TStreamReader.cpp SystemClasses/TStreamWriter.cpp SystemClasses/System.SysUtils.cpp SystemClasses/GetTickCount.cpp - wasm_api.cpp ) ABS_SOURCES=() for src in "${TOOL1CD_SOURCES[@]}"; do ABS_SOURCES+=("$SRC_DIR/$src") done +ABS_SOURCES+=("$OUT_DIR/wasm_api.cpp") mkdir -p "$OUT_DIR" diff --git a/src/tool1cd/wasm_api.cpp b/web/wasm_api.cpp similarity index 84% rename from src/tool1cd/wasm_api.cpp rename to web/wasm_api.cpp index 2c84401..58193d1 100644 --- a/src/tool1cd/wasm_api.cpp +++ b/web/wasm_api.cpp @@ -1,3 +1,24 @@ +/* +Tool1CD library provides access to 1CD database files. + Copyright © 2009-2017 awa + Copyright © 2017-2018 E8 Tools contributors + + This file is part of Tool1CD Library. + + Tool1CD Library is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Tool1CD Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Tool1CD Library. If not, see . +*/ + #include "Class_1CD.h" #include "Field.h" #include "Table.h" @@ -7,6 +28,7 @@ #include #include #include +#include #ifdef __EMSCRIPTEN__ #include @@ -16,13 +38,16 @@ namespace { -T_1CD* g_db = nullptr; +std::unique_ptr g_db; std::string g_last_error; -char* dup_cstr(const std::string& s) { - char* out = static_cast(std::malloc(s.size() + 1)); +char *dup_cstr(const std::string &s) { + char *out = static_cast(std::malloc(s.size() + 1)); if (!out) { - return nullptr; + out = static_cast(std::malloc(1)); + if (!out) return nullptr; // is it possible? + out[0] = '\0'; + return out; } std::memcpy(out, s.c_str(), s.size() + 1); @@ -95,28 +120,23 @@ Table* find_table_by_name(const std::string& table_name) { } // namespace extern "C" { - -EMSCRIPTEN_KEEPALIVE int onecd_open(const char* path) { +EMSCRIPTEN_KEEPALIVE int onecd_open(const char *path) { if (!path || !*path) { set_error("onecd_open: empty path"); return -1; } try { - delete g_db; - g_db = new T_1CD(); - g_db->open(path, false); + std::unique_ptr db(new T_1CD()); + db->open(path, false); + g_db = std::move(db); g_last_error.clear(); return 0; - } catch (const std::exception& ex) { + } catch (const std::exception &ex) { set_error(ex.what()); - delete g_db; - g_db = nullptr; return -2; } catch (...) { set_error("onecd_open: unknown error"); - delete g_db; - g_db = nullptr; return -3; } } @@ -274,8 +294,7 @@ EMSCRIPTEN_KEEPALIVE const char* onecd_last_error() { } EMSCRIPTEN_KEEPALIVE void onecd_close() { - delete g_db; - g_db = nullptr; + g_db.reset(); g_last_error.clear(); } From b23ab0735981b6a1ed702c65b29b8fb35f6b37a5 Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Mon, 16 Feb 2026 21:57:58 +0400 Subject: [PATCH 5/7] cleanup docs --- BUILD.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 28 ---------------------------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/BUILD.md b/BUILD.md index 3e01cd2..f07dc91 100644 --- a/BUILD.md +++ b/BUILD.md @@ -278,3 +278,59 @@ Visual Studio 2017 поддерживает работу с cmake без пре } ] ``` + +## WebAssembly (браузерный просмотр таблиц) + +### Что нужно для сборки + +1. Активированный Emscripten (`em++` в `PATH`) +2. Boost, собранный под target Emscripten (нужны `filesystem`, `system`, `regex`) + +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh + +curl -L -o boost1.88.tar.gz +tar xf boost1.88.tar.gz +cd boost1.88 + +./bootstrap.sh +cat > user-config.jam <<'EOF' +using clang : emscripten : em++ ; +EOF + +export BOOST_WASM_ROOT="$HOME/opt/boost-wasm" +mkdir -p "$BOOST_WASM_ROOT" + +./b2 -j"$(nproc)" \ + toolset=clang-emscripten \ + target-os=emscripten \ + variant=release \ + link=static runtime-link=static \ + threading=single \ + --with-system --with-filesystem --with-regex \ + cxxflags="-O3" \ + install --prefix="$BOOST_WASM_ROOT" + +### Сборка WASM + +```sh +BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh +``` +После успешной сборки появятся: + +- `web/parser.js` +- `web/parser.wasm` + +### Локальный запуск + +```sh +cd web +python3 -m http.server 8080 +``` + +Открыть в браузере: + +`http://localhost:8080` diff --git a/README.md b/README.md index 60bbe56..2280582 100644 --- a/README.md +++ b/README.md @@ -100,31 +100,3 @@ sudo apt-get install ctool1cd Если в пути содержатся пробелы, его необходимо заключать в кавычки. Пути следует указывать без завершающего слеша `/` и бэкслеша `\` Для команд `-dc`, `-ddc`, `-drc` вместо пути можно указывать имя файла конфигурации (имя файла должно заканчиваться на `.cf`). - -## WebAssembly (браузерный просмотр таблиц) - -### Что нужно для сборки - -1. Активированный Emscripten (`em++` в `PATH`) -2. Boost, собранный под target Emscripten (нужны `filesystem`, `system`, `regex`) - -### Сборка WASM - -```sh -BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh -``` -После успешной сборки появятся: - -- `web/parser.js` -- `web/parser.wasm` - -### Локальный запуск - -```sh -cd web -python3 -m http.server 8080 -``` - -Открыть в браузере: - -`http://localhost:8080` From 79f3c396509961cce384092bf6a127ddf0927a0d Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Tue, 17 Feb 2026 16:55:48 +0400 Subject: [PATCH 6/7] Use WORKERFS for WebAssembly to support files larger than 4 GB --- web/app.js | 183 ++++++++++++++-------------------------------- web/build-wasm.sh | 3 +- web/index.html | 2 - 3 files changed, 55 insertions(+), 133 deletions(-) diff --git a/web/app.js b/web/app.js index 6ce702d..db9b6c5 100644 --- a/web/app.js +++ b/web/app.js @@ -9,10 +9,11 @@ const tableMetaEl = document.getElementById("tableMeta"); const tableContentHeadEl = document.getElementById("tableContentHead"); const tableContentBodyEl = document.getElementById("tableContentBody"); -let api = null; +let worker = null; +let requestSeq = 1; +const pendingRequests = new Map(); let currentTables = []; let selectedTableName = ""; -let currentWasmPath = ""; const TABLE_PREVIEW_LIMIT = 100; function setStatus(message) { @@ -247,110 +248,53 @@ function renderTableContent(payload) { `${payload?.table ?? ""}: showing ${offset}-${shownTo} of ${totalRows} rows`; } -function bindApi(Module) { - const hasCwrap = typeof Module.cwrap === "function"; - const wrap = (name, returnType, argTypes) => { - if (hasCwrap) { - const fn = Module.cwrap(name, returnType, argTypes); - if (typeof fn === "function") return fn; - } - const direct = Module[`_${name}`]; - return typeof direct === "function" ? direct : null; - }; - - const open = wrap("onecd_open", "number", ["string"]); - const listTablesJsonPtr = wrap("onecd_list_tables_json", "number", []); - const getTableRowsJsonPtr = wrap("onecd_get_table_rows_json", "number", ["string", "number", "number"]); - const close = wrap("onecd_close", null, []); - const freeString = wrap("onecd_free_string", null, ["number"]) - || (typeof Module._free === "function" ? Module._free : null); - - const missing = []; - if (!open) missing.push("onecd_open"); - if (!listTablesJsonPtr) missing.push("onecd_list_tables_json"); - if (!getTableRowsJsonPtr) missing.push("onecd_get_table_rows_json"); - if (!freeString) missing.push("onecd_free_string"); - if (!close) missing.push("onecd_close"); - if (missing.length) { - throw new Error( - `Missing WASM exports: ${missing.join(", ")}. ` + - "Rebuild and refresh parser.js/parser.wasm." - ); - } - - return { - open, - listTablesJsonPtr, - getTableRowsJsonPtr, - close, - freeString, - utf8ToString: Module.UTF8ToString.bind(Module), - FS: Module.FS, - }; -} - -async function initModule() { - if (typeof Module === "undefined") { - throw new Error("parser.js did not define global Module"); - } - - const runtimeReady = - typeof Module.ready === "object" && typeof Module.ready.then === "function" - ? Module.ready - : new Promise((resolve) => { - const previous = Module.onRuntimeInitialized; - Module.onRuntimeInitialized = () => { - if (typeof previous === "function") previous(); - resolve(); - }; - }); - - await runtimeReady; - api = bindApi(Module); -} - -async function fileToUint8Array(file) { - const buf = await file.arrayBuffer(); - return new Uint8Array(buf); -} +function initWorker() { + worker = new Worker("./wasm_worker.js"); -function readTablesJson() { - const ptr = api.listTablesJsonPtr(); - if (!ptr) { - throw new Error("onecd_list_tables_json returned null pointer"); - } + worker.addEventListener("message", (event) => { + const message = event.data || {}; + const request = pendingRequests.get(message.id); + if (!request) { + return; + } + pendingRequests.delete(message.id); + if (message.ok) { + request.resolve(message.result); + } else { + request.reject(new Error(String(message.error || "Unknown worker error"))); + } + }); - try { - const jsonText = api.utf8ToString(ptr); - return JSON.parse(jsonText); - } finally { - api.freeString(ptr); - } + worker.addEventListener("error", (event) => { + const message = event.message || "WASM worker crashed"; + for (const request of pendingRequests.values()) { + request.reject(new Error(message)); + } + pendingRequests.clear(); + }); } -function readTableRowsJson(tableName, offset = 0, limit = TABLE_PREVIEW_LIMIT) { - const ptr = api.getTableRowsJsonPtr(tableName, offset, limit); - if (!ptr) { - throw new Error("onecd_get_table_rows_json returned null pointer"); +function workerCall(method, params = {}) { + if (!worker) { + return Promise.reject(new Error("WASM worker is not initialized")); } - try { - const jsonText = api.utf8ToString(ptr); - const parsed = JSON.parse(jsonText); - if (parsed && typeof parsed === "object" && parsed.error) { - throw new Error(String(parsed.error)); - } - return parsed; - } finally { - api.freeString(ptr); - } + return new Promise((resolve, reject) => { + const id = requestSeq++; + pendingRequests.set(id, { resolve, reject }); + worker.postMessage({ id, method, params }); + }); } async function loadTableContent(tableName) { clearError(); setStatus(`Loading rows from ${tableName}...`); try { - const payload = readTableRowsJson(tableName, 0, TABLE_PREVIEW_LIMIT); + const payload = await workerCall("getTableRows", { + tableName, + offset: 0, + limit: TABLE_PREVIEW_LIMIT, + }); selectedTableName = tableName; renderTables(); renderTableContent(payload); @@ -373,52 +317,24 @@ async function onOpenClick() { return; } - const wasmPath = `/tmp/${file.name}`; - + openBtn.disabled = true; try { - if (currentWasmPath) { - try { - api.close(); - } catch (_) { - // Ignore close errors. - } - try { - api.FS.unlink(currentWasmPath); - } catch (_) { - // Ignore cleanup errors. - } - currentWasmPath = ""; - } - - setStatus("Reading selected file..."); - const bytes = await fileToUint8Array(file); - - setStatus("Writing file to WASM FS..."); - api.FS.writeFile(wasmPath, bytes); - - setStatus("Opening 1CD file..."); - const openResult = api.open(wasmPath); - if (openResult !== 0) { - throw new Error(`onecd_open failed with code ${openResult}`); - } + setStatus("Mounting selected file in WORKERFS..."); + await workerCall("openFile", { file }); setStatus("Loading table list..."); - const parsed = readTablesJson(); + const parsed = await workerCall("listTables"); currentTables = normalizeTables(parsed); selectedTableName = ""; renderTables(); clearTableContent(); - currentWasmPath = wasmPath; setStatus(`Loaded ${currentTables.length} tables from ${file.name}`); } catch (err) { const message = err instanceof Error ? err.message : String(err); showError(message); setStatus("Failed to open file."); - try { - api.FS.unlink(wasmPath); - } catch (_) { - // Ignore cleanup errors. - } + } finally { + openBtn.disabled = false; } } @@ -429,8 +345,9 @@ async function main() { openBtn.disabled = true; try { - await initModule(); - setStatus("WASM runtime is ready."); + initWorker(); + await workerCall("init"); + setStatus("WASM worker runtime is ready."); openBtn.disabled = false; } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -455,4 +372,10 @@ sortDirEl.addEventListener("change", () => { renderTables(); }); +window.addEventListener("beforeunload", () => { + if (worker) { + worker.terminate(); + } +}); + void main(); diff --git a/web/build-wasm.sh b/web/build-wasm.sh index 939a98f..7be79b6 100755 --- a/web/build-wasm.sh +++ b/web/build-wasm.sh @@ -56,9 +56,10 @@ em++ \ -L"$BOOST_WASM_ROOT/lib" \ -lboost_filesystem -lboost_system -lboost_regex \ -sUSE_ZLIB=1 \ + -lworkerfs.js \ -sFORCE_FILESYSTEM=1 \ -sALLOW_MEMORY_GROWTH=1 \ - -sENVIRONMENT=web \ + -sENVIRONMENT=web,worker \ -sMODULARIZE=0 \ -sEXPORT_ES6=0 \ -sNO_EXIT_RUNTIME=1 \ diff --git a/web/index.html b/web/index.html index b8abbd5..b4ed28e 100644 --- a/web/index.html +++ b/web/index.html @@ -257,8 +257,6 @@

      Table Content

      - - From 34afa630727b6de6002d87d96bab768acdc86bcb Mon Sep 17 00:00:00 2001 From: Aleksey Pindiurin Date: Wed, 18 Feb 2026 00:05:09 +0400 Subject: [PATCH 7/7] Add site icon --- web/favicon.ico | Bin 0 -> 4286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 web/favicon.ico diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2e06bbb102295e4673389dc5fef34c8b16d7a42a GIT binary patch literal 4286 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 Z^mWM&EuFc>>6$jRF@OAj!2kF6{{mlYAMpSH literal 0 HcmV?d00001